/*
 * The contents of this file are subject to the terms of the Common Development
 * and Distribution License (the License). You may not use this file except in
 * compliance with the License.
 * 
 * You can obtain a copy of the License at http://www.netbeans.org/cddl.html
 * or http://www.netbeans.org/cddl.txt.
 * 
 * When distributing Covered Code, include this CDDL Header Notice in each file
 * and include the License file at http://www.netbeans.org/cddl.txt.
 * If applicable, add the following below the CDDL Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 * 
 * The Original Software is NetBeans. The Initial Developer of the Original
 * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
 * Microsystems, Inc. All Rights Reserved.
 */

package org.netbeans.modules.autoupdate;

import java.io.*;
import java.net.URLConnection;
import java.text.MessageFormat;
import java.util.Iterator;
import java.util.List;
import org.netbeans.api.progress.ProgressHandle;
import org.netbeans.api.progress.ProgressHandleFactory;
import org.netbeans.updater.UpdateTracking;

import org.openide.DialogDisplayer;
import org.openide.ErrorManager;

import org.openide.util.NbBundle;
import org.openide.NotifyDescriptor;
import org.openide.util.RequestProcessor;

/** This class downloads modules and verifies the digital signatures
 * this class also copyies the downloaded modules.
 * @author  phrebejk
 */
class Downloader extends Object {

    /** The update check progress panel */
    DownloadProgressPanel progressDialog;
    /** Total size of the download */
    private int downloadSize;
    /** KBytes downloaded */
    private int totalDownloaded;
    /** KBytes downloaded */
    private int moduleDownloaded;
    /** Number of modules to downaload */
    private long modulesCount;
    /** Shoud internet download be performed */
    private boolean urlDownload;
    
    static private int TIME_TO_CONNECTION_CHECK = 20000;

    /** Extension of the distribution files */
    private static final String NBM_EXTENSION = "nbm"; // NOI18N

    /** Wizard validator, enables the Next button in wizard */
    private Wizard.Validator validator;
    
    private static final ErrorManager err = ErrorManager.getDefault ().getInstance ("org.netbeans.modules.autoupdate"); // NOI18N
    
    private static RequestProcessor.Task READ_TIMEOUT_CHECKER;
    private static RequestProcessor.Task DOWNLOAD_TASK;
    private static RequestProcessor AU_REQUEST_PROCESSOR = null;
    
    private static int DOWNLOADER_IS_INIT = -1;
    private static int DOWNLOADER_RUNNING = 0;
    private static int DOWNLOADER_WAITING = 1;
    private static int DOWNLOADER_CANCELED = 2;
    private static int DOWNLOADER_STOPPED = 3;
    private static int DOWNLOADER_FINISHED = 4;
    private static int DOWNLOADER_VERIFIED = 5;
    
    private int downloadStatus = DOWNLOADER_IS_INIT;
    
    // progress
    ProgressHandle partialHandle;
    ProgressHandle overallHandle;

    /** Creates new Downloader */
    public Downloader (DownloadProgressPanel progressDialog, Wizard.Validator validator, boolean urlDownload) {
        this.validator = validator;
        this.progressDialog = (DownloadProgressPanel)progressDialog;
        this.urlDownload = urlDownload;
    }
    
    private static RequestProcessor getAutoupdateRequestProcessor () {
        if (AU_REQUEST_PROCESSOR == null) {
            AU_REQUEST_PROCESSOR = new RequestProcessor ("org-netbeans-modules-autoupdate", 10, true); // NOI18N
        }
        return AU_REQUEST_PROCESSOR;
    }

    void doDownload() {
        if (READ_TIMEOUT_CHECKER != null) {
            READ_TIMEOUT_CHECKER.cancel ();
        }        
        
        assert READ_TIMEOUT_CHECKER == null || READ_TIMEOUT_CHECKER.isFinished () : "Only one READ_TIMEOUT_CHECKER can be active";
        
        // if the task isn't finished -> interrupt and left it
        if (DOWNLOAD_TASK != null) {
            DOWNLOAD_TASK.cancel ();
            DOWNLOAD_TASK = null;
        }

        assert DOWNLOAD_TASK == null || DOWNLOAD_TASK.isFinished () : "Only one DOWNLOAD_TASK can be active";
        
        downloadSize = getTotalDownloadSize();
        
        progressDialog.setPartialLabel (""); // NOI18N
        progressDialog.setOverallLabel (""); // NOI18N
        progressDialog.setExtraLabel (""); // NOI18N
        
        Runnable task = new Runnable () {
                            public void run() {

                                progressDialog.setPartialLabel (getBundle( "CTL_PreparingDownload_Label")); // NOI18N

                                err.log ("Start downloading " + modulesCount + " modules [" + downloadSize + "]");
                                
                                downloadAll();
                                
                                if (DOWNLOADER_STOPPED == getStatus ()) {
                                    return ;
                                }

                                progressDialog.setExtraLabel (getBundle ("DownloadProgressPanel.jLabel1.doneText")); // NOI18N

                                validator.setValid( true );
                            }
                        };

        DOWNLOAD_TASK = getAutoupdateRequestProcessor ().post( task );
    }
    
    /** Total size of download in KBytes */
    private int getTotalDownloadSize( ) {
        long result = 0L;
        modulesCount = 0;

        Iterator it = Wizard.getAllModules().iterator();

        while( it.hasNext() ) {
            ModuleUpdate mu = (ModuleUpdate)it.next();

            if ( mu.isSelected() && !mu.isDownloadOK() ) {
                result += mu.getDownloadSize();
                modulesCount++;
            }
        }
        return (int) result;
    }


    /** Downloads the modules from web */
    private void downloadAll() {

        // show fake component during the real length is obtained from connection
        getPartialHandle (1);
        overallHandle = getOverallHandle (downloadSize);

        int currentModule = 0;
        totalDownloaded = 0;

        Iterator it = Wizard.getAllModules().iterator();

        while( it.hasNext() ) {

            if (DOWNLOADER_STOPPED == getStatus ()) {
                return;
            }

            ModuleUpdate mu = (ModuleUpdate)it.next();
            if ( mu.isSelected() && !mu.isDownloadOK() ) {
                progressDialog.setPartialLabel (mu.getName () + " [" + (currentModule + 1) + "/" + modulesCount + "]"); // NOI18N
                if ( urlDownload ) {
                    if (DOWNLOADER_FINISHED == getStatus () || DOWNLOADER_IS_INIT == getStatus ()) {
                        err.log ("Do download " + mu.getName ());
                        setStatus (DOWNLOADER_IS_INIT);
                        downloadModule (mu);
                        err.log ("Download of " + mu.getName () + " ends with status " + getStatus ());
                    } else {
                        err.log ("Don't download " + mu.getName () + " due to incorrect status " + getStatus ());
                    }
                } else {
                    downloadModuleFromLocal (mu);
                }
                updateOverall ();
                if (partialHandle != null && mu.isDownloadOK ()) {
                    partialHandle.finish ();
                    currentModule++;
                }
            }
        }
        
        if (DOWNLOADER_STOPPED == getStatus ()) {
            return ;
        }

        overallHandle.progress (downloadSize);
        overallHandle.finish ();
        String mssgTotal = MessageFormat.format( getBundle( "FMT_DownloadedTotal" ), // NOI18N
                           new Object[] { new Integer( downloadSize / 1024 ),
                                          new Integer( downloadSize / 1024 ) } );
        progressDialog.setOverallLabel (mssgTotal);
        progressDialog.setEnableStop (false);
        
        runVerifier ();
    }
    
    private void runVerifier () {
        
        err.log ("Prepare SignVerifier.");
        if (DOWNLOADER_VERIFIED == getStatus ()) {
            err.log ("Error: SignVerifier has run before.");
            return ;
        }

        SignVerifier signVerifier = new SignVerifier (progressDialog, validator);
        progressDialog.setExtraLabel (getBundle("DownloadProgressPanel.jLabel1.securityText")); // NOI18N
        signVerifier.doVerify();
        
        setStatus (DOWNLOADER_VERIFIED);
    }
    
    private int getStatus () {
        return downloadStatus;
    }
    
    private void setStatus (int status) {
        downloadStatus = status;
    }
    
    private void ioCopy (BufferedInputStream in, BufferedOutputStream out, int flen) throws IOException {
        int c;
        
        try {
            
            if (DOWNLOADER_IS_INIT == getStatus ()) {
                setStatus (DOWNLOADER_RUNNING);
            }
            
            while ((DOWNLOADER_RUNNING == getStatus () || DOWNLOADER_WAITING == getStatus ()) && ( c = in.read () ) != -1 ) {

                if (READ_TIMEOUT_CHECKER.getDelay () == 0) {
                    READ_TIMEOUT_CHECKER.waitFinished ();
                } else if (READ_TIMEOUT_CHECKER.getDelay () < TIME_TO_CONNECTION_CHECK) {
                    READ_TIMEOUT_CHECKER.schedule (TIME_TO_CONNECTION_CHECK * 2);
                }

                out.write( c );

                moduleDownloaded++;
                totalDownloaded++;

                if ( moduleDownloaded % 4096 == 0 ) {
                    updateOverall();
                    partialHandle.progress (moduleDownloaded < flen ? moduleDownloaded : flen);
                }
            }
            
            if (DOWNLOADER_RUNNING == getStatus ()) {
                setStatus (DOWNLOADER_FINISHED);
            }
            
        } finally {
            if (in != null) in.close();
            if (out != null) out.close();
        }
        
    }

    /** Downloads a .NBM file into download directory
    */
    private void downloadModule (final ModuleUpdate moduleUpdate) {
        int flen = 0;

        final File destFile = getNBM( moduleUpdate );

        try {

            moduleDownloaded = 0;
            partialHandle = getPartialHandle (1);

            moduleUpdate.setDownloadStarted( true );
            progressDialog.setEnableStop (true);

            err.log ("Setup checker of download " + moduleUpdate.getName ());
            READ_TIMEOUT_CHECKER = getAutoupdateRequestProcessor ().post (new Runnable () {
                public void run () {
                    confirmConnectionFailed (moduleUpdate);
                }
            }, TIME_TO_CONNECTION_CHECK * 4);

            err.log ("Try to estabilish a conncetion to " + moduleUpdate.getDistribution ());
            progressDialog.setExtraLabel (getBundle ("DownloadProgressPanel.jLabel1.Establish")); // NOI18N

            URLConnection distrConnection = moduleUpdate.getDistribution().openConnection();
            moduleUpdate.setRemoteDistributionFilename(distrConnection);

            final BufferedInputStream bsrc = new BufferedInputStream (distrConnection.getInputStream ());
            BufferedOutputStream bdest = new BufferedOutputStream( new FileOutputStream( destFile ) );

            flen = distrConnection.getContentLength();
            // #65099: some servers/proxies can forbid to say content length of URLConnection
            if (flen == -1) {
                flen = (int) moduleUpdate.getDownloadSize ();
            }
            assert flen > 0 : "Content of distrConnection of update " + moduleUpdate.getName () + " must be known and more then 0, but was " + flen;
            partialHandle = getPartialHandle (flen);
            progressDialog.setExtraLabel (getBundle ("DownloadProgressPanel.jLabel1.downloadText")); //NOI18N

            int x = 0;

            try {
                
                ioCopy (bsrc, bdest, flen);
                
                if (DOWNLOADER_FINISHED == getStatus ()) {
                    moduleUpdate.setDownloadOK (true);
                    err.log (moduleUpdate.getName () + " was downloaded correctly.");
                } else {
                    moduleUpdate.setDownloadOK (false);
                    err.log (moduleUpdate.getName () + " wasn't download correctly.");
                    moduleUpdate.setSecurity (SignVerifier.BAD_DOWNLOAD);
                }
                
                moduleUpdate.setDownloadStarted (false);
                READ_TIMEOUT_CHECKER.cancel ();
                
            } finally {
                if (DOWNLOADER_FINISHED != getStatus ()) {
                    getNBM (moduleUpdate).delete ();
                }
            }
        } catch ( IOException e ) {
            if (DOWNLOADER_STOPPED == getStatus ()) {
                return ;
            }

            // cannot decrease count of downloaded bytes due Progress API
            // totalDownloaded-=moduleDownloaded;
            
            validator.setValid (false);

            ErrorManager.getDefault ().notify (ErrorManager.INFORMATIONAL, e);

            // Download failed
            confirmConnectionFailed (moduleUpdate);
        }
        
        return;
    }
    
    private void confirmConnectionFailed (ModuleUpdate moduleUpdate) {
        if (DOWNLOADER_RUNNING != getStatus () && DOWNLOADER_IS_INIT != getStatus ()) {
            return;
        }
        err.log ("Connection failed during download " + moduleUpdate.getName ());
        setStatus (DOWNLOADER_WAITING);
        String mssg = NbBundle.getMessage( Downloader.class, "FMT_DownloadFailed", // NOI18N
                                            moduleUpdate.getName() );
        NotifyDescriptor nd = new NotifyDescriptor.Confirmation (mssg,
                              getBundle ("CTL_DownloadFailed"), // NOI18N
                              NotifyDescriptor.YES_NO_CANCEL_OPTION);
        DialogDisplayer.getDefault().notify( nd );

        if ( nd.getValue().equals( NotifyDescriptor.CANCEL_OPTION ) ) {
            err.log ("Confirm of the failed connection was canceled, continue downloading.");
            
            // set status RUNNING back
            setStatus (DOWNLOADER_RUNNING);
            
            moduleUpdate.setDownloadStarted (true);
            moduleUpdate.setSecurity (SignVerifier.BAD_DOWNLOAD);
            if (READ_TIMEOUT_CHECKER != null) {
                READ_TIMEOUT_CHECKER.cancel ();
                READ_TIMEOUT_CHECKER.schedule (TIME_TO_CONNECTION_CHECK * 2);
            }
            validator.setValid (false);
            return ;
        } else if (nd.getValue ().equals (NotifyDescriptor.NO_OPTION)) {
            err.log ("Interrupt download of " + moduleUpdate.getName ());
            validator.setValid (true);
            setStatus (DOWNLOADER_CANCELED);
            moduleUpdate.setDownloadOK (false);
            moduleUpdate.setDownloadStarted (false);
            progressDialog.setExtraLabel (getBundle ("DownloadProgressPanel.jLabel1.Broken")); //NOI18N
            moduleUpdate.setSecurity (SignVerifier.BAD_DOWNLOAD);

            // cannot decrease count of downloaded bytes due Progress API
            // totalDownloaded-=moduleDownloaded;
            
            DOWNLOAD_TASK.cancel ();
            READ_TIMEOUT_CHECKER.cancel ();
            
            runVerifier ();
            
            return ;
        }
        
        READ_TIMEOUT_CHECKER.cancel ();
        DOWNLOAD_TASK.cancel ();
        setStatus (DOWNLOADER_STOPPED);
        
        // try again
        
        moduleUpdate.setDownloadStarted (false);
        err.log ("Failed connection was confirmed, start download " + moduleUpdate.getName () + " again.");
        progressDialog.setExtraLabel (getBundle ("DownloadProgressPanel.jLabel1.Restart")); // NOI18N
        RequestProcessor.getDefault ().post (new Runnable () {
           public void run () {
               new Downloader (progressDialog, validator, urlDownload).doDownload ();
           } 
        }, 100);
        
        return ;
    }
    
    private void downloadModuleFromLocal (ModuleUpdate moduleUpdate) {
        partialHandle = getPartialHandle (1);
        if (downloadFromLocal (moduleUpdate)) {
            totalDownloaded += moduleUpdate.getDownloadSize();
        }
        updateOverall();
    }
    
    private void updateOverall() {
        String mssgTotal = NbBundle.getMessage( Downloader.class, "FMT_DownloadedTotal", // NOI18N
                           new Object[] { new Integer( (int)(totalDownloaded / 1024) ),
                                          new Integer( (int)(downloadSize / 1024) ) } );

        // ignore higher sizes then planned downloadSize 
        overallHandle.progress (downloadSize > totalDownloaded ? totalDownloaded : downloadSize);
        progressDialog.setOverallLabel (mssgTotal);
    }

    private ProgressHandle getPartialHandle (int units) {
        assert units >= 0 : "Count of units " + units + " must be positive.";        
        assert progressDialog != null;
        ProgressHandle handle = ProgressHandleFactory.createHandle (getBundle ("DownloadProgressPanel_partialHandle_name")); // NOI18N
        handle.setInitialDelay (0);
        progressDialog.setPartialProgressComponent (handle);
        handle.start (units);
        return handle;
    }
    
    private ProgressHandle getOverallHandle (int units) {
        assert units >= 0 : "Count of units " + units + " must be positive.";        
        assert progressDialog != null;
        ProgressHandle handle = ProgressHandleFactory.createHandle (getBundle ("DownloadProgressPanel_overallHandle_name")); // NOI18N
        handle.setInitialDelay (0);
        progressDialog.setOverallProgressComponent (handle);
        handle.start (units);
        return handle;
    }
    
    // utility methods
    // TODO: could be moved in a utility class ?
    
    void cancelDownload() {
        setStatus (DOWNLOADER_STOPPED);
        validator.setValid (false);
    }
    
    /** Copies the external NBM file into the appropriate location where 
     * Downloader can work with.
     *
     * @param moduleUpdate moduleupdate for local nbm
     * @return false if there was a problem
     */
    /*no private due to unit test*/
    static boolean downloadFromLocal (ModuleUpdate moduleUpdate) {
        if ( tryCopy( moduleUpdate.getDistributionFile(), getNBM( moduleUpdate ) ) ) {
            moduleUpdate.setDownloadOK( true );
            return true;
        }
        else {
            getNBM( moduleUpdate ).delete();
            return false;
        }
    }
    
    static File getNBM( ModuleUpdate mu ) {
        File destFile = new File( Autoupdater.Support.getDownloadDirectory (null), mu.getDistributionFilename() );
        return destFile;
    }
    
    static File getMovedNBM( ModuleUpdate mu ) {
        File destFile = null;
        if (mu.isToInstallDir ())
            destFile = Autoupdater.Support.getDownloadDirectory (mu.findInstallDirectory ());
        else
            destFile = Autoupdater.Support.getDownloadDirectory (null);
        
        return new File (destFile, mu.getDistributionFilename());
    }

    static boolean tryMove( ModuleUpdate mu ) {            
        File inst = new File ( 
            Autoupdater.Support.getDownloadDirectory (mu.findInstallDirectory ()), 
            mu.getDistributionFilename()
        );
        
        boolean ok = getNBM( mu ).renameTo( inst );
        if ( ok )
            return true;
        
        // issue 21607
        ok = tryCopy( getNBM( mu ), inst );
        if ( ok )
            getNBM( mu ).delete();
        
        return ok;
    }
    
    static void saveCopy( ModuleUpdate mu, File target ) {
        tryCopy( getNBM( mu ), target );
    }
    
    private static boolean tryCopy( File src, File dest ) {
        BufferedInputStream bsrc = null;
        BufferedOutputStream bdest = null;

        try {
            try {
                bsrc = new BufferedInputStream( new FileInputStream( src ), 4096 );
                bdest = new BufferedOutputStream( new FileOutputStream( dest ), 4096 );

                int c;
                while( ( c = bsrc.read() ) != -1 ) {
                    bdest.write( c );
                }
            }
            finally {
                // workaround #39006, don't close a not opened stream
                if (bsrc != null) bsrc.close();
                if (bdest != null) bdest.close();
            }
        }
        catch ( IOException e ) {
            return false;
        }
        return true;
    }
    
    static void deleteModuleNBM( ModuleUpdate mu ) {
        getNBM( mu ).delete();
    }

    // Deletes all files in download directory
    static void deleteDownload() {
        boolean noTestPrepared = true;
        PreparedModules prepared = null;
        
        List/*<File>*/ clusters = UpdateTracking.clusters (true);
        assert clusters != null : "Clusters cannot be empty."; // NOI18N
        Iterator it =  clusters.iterator ();
        while (it.hasNext ()) {
            if (Autoupdater.Support.getInstall_Later ((File)it.next ()).exists ()) {
                noTestPrepared = false;
                prepared = PreparedModules.getPrepared();
            }
        }
        
        File[] nbms = getNBMFiles();

        for( int i = 0; i < nbms.length; i++ ) {
            if ( noTestPrepared || !prepared.hasNBM( nbms[i].getName() ) )
                nbms[i].delete();
        }
    }
    
    static boolean bannedWriteToInstall(ModuleUpdate mu) {
        File f = mu.findInstallDirectory ();
        
        return f == null || !f.canWrite();
    }

    private static File[] getNBMFiles() {
        File dirList[] = Autoupdater.Support.getDownloadDirectory (null).listFiles( new FilenameFilter() {
                             public boolean accept( File dir, String name ) {
                                 return name.endsWith( NBM_EXTENSION );
                             }
                         });

        return dirList;
    }

    private String getBundle( String key ) {
        return NbBundle.getMessage( Downloader.class, key );
    }
    
}
