/*
 * 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.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.text.SimpleDateFormat;
import java.text.ParsePosition;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.zip.GZIPInputStream;
import javax.swing.SwingUtilities;
import org.openide.ErrorManager;
import org.openide.util.RequestProcessor;

import org.w3c.dom.*;
import org.xml.sax.*;
import org.xml.sax.SAXException;


/** Serves for building an UpdateCache from XML Document
 * @author  Petr Hrebejk
 */
class XMLUpdates extends Updates {

    /** XML Element tag names */
    private static final String TAG_MODULE_UPDATES = "module_updates"; // NOI18N
    private static final String TAG_MODULE = "module"; // NOI18N
    private static final String TAG_MODULE_GROUP = "module_group"; // NOI18N
    private static final String TAG_NOTIFICATION = "notification"; // NOI18N
    private static final String ATTR_NOTIFICATION_URL = "url"; // NOI18N
    private static final String TAG_ERROR = "error"; // NOI18N
    private static final String TAG_AUTH_ERROR = "auth_error"; // NOI18N
    private static final String TAG_OTHER_ERROR = "other_error"; // NOI18N
    private static final String ATTR_MESSAGE_ERROR = "message"; // NOI18N
    
    private static final ErrorManager err = ErrorManager.getDefault ().getInstance ("org.netbeans.modules.autoupdate"); // NOI18N
    
    /** List of all modules */
    private ArrayList modules;
    private HashMap checkOnceMore;

    /** The tree structure of Modules and Groups */
    private ModuleGroup rootGroup;
    
    private int pError = NO_ERROR;
    
    private String errorMess = null;

    /** The timeStamp of downloaded xml */
    private Date timeStamp = null;

    /** Text of the notification if any */
    private String notificationText = null;

    /** URL of the notification if any */
    private URL notificationURL = null;

    /** The URL of the document */
    private URL xmlURL;

    /** The list of files in case of installing downloaded modules */
    private File[] files;
    
    /** Temporary list of files to be installed as group */
    private static ArrayList groupFiles = new ArrayList();
    /** Is timer process to install group of selected files set? */
    private static boolean timerSet = false;
    /** Does installation of group of selected files start? */
    private static boolean groupStarted = false;
    private static int oldGroupSize = 0;
    
    /** The XML Document */
    //private Document document = null;
    
    private InputSource xmlInputSource = null;

    private AutoupdateType currentAT = null;

    private static String GZIP_EXTENSION = ".gz"; // NOI18N
    
    /** Creates new Updates
     */
    XMLUpdates(URL xmlURL) {
        this.xmlURL = xmlURL;        
    }

    /** Create new Updates for files list - used
     * for installing downloaded modules
     */
    XMLUpdates(File[] files) {
        this.files = files;        
    }
    
    /** Start wizard for whole group of files requesting
     * installation at once.
     * Timer process is used to wait for all requests.
     */
    static void startGroupUpdates( File file ) {
        if ( groupStarted )
            new XMLUpdates( new File[]{ file } ).go();
        else {
            groupFiles.add( file );
            setTimer();
        }
    }
    
    private static void setTimer() {
        if ( ! timerSet ) {
            timerSet = true;
            oldGroupSize = groupFiles.size();
            org.openide.util.RequestProcessor.getDefault().post( new Runnable() {
                public void run() {
                    if (! SwingUtilities.isEventDispatchThread ()) {
                        SwingUtilities.invokeLater (this);
                        return ;
                    }
                    if ( oldGroupSize < groupFiles.size() ) {
                        oldGroupSize = groupFiles.size();
                        org.openide.util.RequestProcessor.getDefault().post(
                                this,
                                500
                            );
                    }
                    else {
                        timerSet  = false;
                        groupStarted = true;
                        File[] arrFiles = new File[ groupFiles.size() ];
                        Iterator it = groupFiles.iterator();
                        for ( int j = 0; it.hasNext(); j++ ) {
                            File fn = (File)it.next();
                            if ( fn != null ) {
                                arrFiles[j] = fn;
                            }
                        }
                        groupFiles.clear();
                        groupStarted = false;
                        new XMLUpdates( arrFiles ).go();
                    }
                }
            },
            500);
        }            
    }

    private void go() {
        HashMap allUpdates = new HashMap();
        allUpdates.put(this, this);
        Wizard wiz = Wizard.go( allUpdates, 1 );
        if ( wiz != null ) {
            checkDownloadedModules();
            wiz.refreshUpdatePanel();
        }
    }
    
    /** Checks for updates in separate thread. Displays progress in a dialog
     */
    public void checkUpdates( final Wizard.Validator validator ) {
        checkUpdates(validator, "");  // NOI18N
    }

    /** Checks for updates in separate thread. Displays progress in a dialog
     */
    public void checkUpdates( final Wizard.Validator validator, String ucname ) {
        checkUpdates( validator, AutoupdateType.find( ucname ) );
    }
    
    public void checkUpdates( final Wizard.Validator validator, final AutoupdateType at ) {
        checkUpdates (validator, at, false);
    }
        
    /** Checks for updates in separate thread. Displays progress in a dialog
     */
    public void checkUpdates (final Wizard.Validator validator, final AutoupdateType at, boolean hidden) {

        currentAT = at;
        pError = NO_ERROR;
        checkCanceled = false;

        final java.awt.Dialog connDialog = ConnectingDialog.getDialog( at != null ? at.getName() : null );
        Runnable runConnect = new Runnable () {
                            public void run() {
                                try {
                                    Document document = parseDocument ();
                                    if ( pError == NO_ERROR && document != null ) {
                                        buildStructures (document);
                                        if (! checkCanceled) {
                                            Settings.getShared().setLastCheck( new Date() );
                                        }
                                    } else {
                                        if (pError == NO_ERROR) {
                                            assert document != null || checkCanceled : "Document is not null or connecting is canceled if no error caught.";
                                        }
                                    }
                                    validator.setValid( true );
                                    int countOfAvailableModules = 0;
                                    if (getModules () != null) countOfAvailableModules = getModules ().size ();
                                    if (countOfAvailableModules == 0 && pError == NO_ERROR) {
                                        // wrong, display warning and don't forward
                                        errorMess = at.getName ();
                                        pError = NO_AVAILABLE_MODULES;
                                    }
                                } catch (Exception x) {
                                    // bugfix #39049, check if a proxy is available
                                    pError = NO_NETWORK;
                                    ErrorManager.getDefault ().notify (ErrorManager.INFORMATIONAL, x);
                                } finally {
                                    if (connDialog != null) {
                                        ConnectingDialog.closeDialog(connDialog);
                                    }
                                }
                            }
                        };
        RequestProcessor.Task task = RequestProcessor.getDefault ().post (runConnect);
        if (!hidden) {
            connDialog.setVisible (true);
            if (ConnectingDialog.isCanceled()) {
                cancelCheck();
            }
        } else {
            task.waitFinished ();
        }
    }
    
    public void cancelCheck() {
        checkCanceled = true;
        if ( xmlInputSource != null) {
            try {
                if (xmlInputSource.getByteStream() != null)
                    xmlInputSource.getByteStream().close();         
            } catch (java.io.IOException e) {                
            }
        }
    }


    /** Gets the root of the module/module group tree
     * @return The group in the root of the tree.
     */
    public ModuleGroup getRootGroup() {
        return rootGroup;
    }

    /** Gets the linear structure of all module updates i.e. Collection
     */
    public Collection getModules() {
        return modules;
    }

    /** Gets the state of pError the file was not parsed */
    public boolean isError() {
        return ( pError > NO_ERROR );
    }
    
    /** Gets the state of pError the file was not parsed */
    public int getError() {
        return pError;
    }
    
    /** Gets the server error message if there is any */
    public String getErrorMessage() {
        return errorMess;
    }
    
    /** Returns the time stamp of the downloaded XML file */
    public Date getTimeStamp() {
        return timeStamp;
    }

    /** Returns notification text if specified otherwise null */
    public String getNotificationText() {
        return notificationText;
    }

    /** Returns notification URL if specified otherwise null */
    public URL getNotificationURL() {
        return notificationURL;
    }

    /** Builds structures for downloaded modules, the structures are only
     * linear 
     */
    void checkDownloadedModules() {
        modules = new ArrayList();        
        rootGroup = new ModuleGroup();
        checkOnceMore = new HashMap();

        for ( int i = 0; i < files.length; i++ ) {
            ModuleUpdate update = ModuleUpdate.getModuleUpdate( files[i] );

            if ( update != null ) {

                if ( update.isUpdateAvailable() ) {
                    modules.add( update );
                    rootGroup.addItem( update );
                }
                else if ( update instanceof L10NUpdate ) {
                    checkOnceMore.put( update, rootGroup );
                }
            }
        }
        checkAvailablesOnceMore();
    }
    
    void checkAvailablesOnceMore() {
        Iterator it = checkOnceMore.entrySet().iterator();
        while ( it.hasNext() ) {
            Map.Entry entry = (Map.Entry) it.next();
            L10NUpdate update = (L10NUpdate) entry.getKey();
            if ( update.isRemoteModuleAvailable( modules ) ) {
                modules.add( update );
                ( (ModuleGroup) entry.getValue() ).addItem( update );
            }
        }
    }

    /** Calls static parsing method in XMLDataObject to parse the
     * document
     */ 
    private Document parseDocument() {
        Document document = null;

        if ( checkCanceled )
            return null;

        String showStr = System.getProperty("autoupdate.show.url"); // NOI18N
        if ( showStr != null && Boolean.valueOf( showStr ).booleanValue() )
            System.out.println("URL : " + xmlURL ); // NOI18N
        
        try {
            document = parseDocumentImpl ();
        } catch ( SAXException e ) {
            pError = PARSE_ERROR;
            showParseError(e);
        } catch ( IOException e ) {
            pError = NO_NETWORK;
            
            if (!checkCanceled) {
                showParseError (e);
            }

        }
        
        return document;
    }

    private BufferedReader reader = null;
    private URL urlToGZip = null;
    
    private Document parseDocumentImpl() throws SAXException, IOException {
        Document document = null;
        java.net.HttpURLConnection.setFollowRedirects( true );
        
        err.log ("Processing URL : " + xmlURL); // NOI18N

        try {
            
            String gzipFile = xmlURL.getPath () + GZIP_EXTENSION;
            String query = xmlURL.getQuery ();
            if (query != null && query.trim ().length () > 0) {
                gzipFile = gzipFile + '?' + query; // NOI18N
            }
            urlToGZip = new URL (xmlURL.getProtocol (), xmlURL.getHost (), xmlURL.getPort (), gzipFile);
            
            reader = new BufferedReader (new InputStreamReader (new GZIPInputStream (urlToGZip.openStream ())));
            xmlInputSource = new InputSource (reader);
            err.log ("Successfully read URL " + urlToGZip); // NOI18N
            
        } catch (IOException ioe) {
            err.log ("Reading GZIP URL " + urlToGZip + " failed (" + ioe + "). Try read XML directly " + xmlURL); // NOI18N
            xmlInputSource = new InputSource( xmlURL.toExternalForm() );
            err.log ("Successfully read URL " + xmlURL); // NOI18N
        }
        
        if ( checkCanceled ) {
            err.log ("Connection canceled on user's demand."); // NOI18N
            return null;
        }
        
        document = org.netbeans.updater.XMLUtil.parse( xmlInputSource, false, false, new ErrorCatcher(), org.netbeans.updater.XMLUtil.createAUResolver () );            
        
        assert document != null : "Parser in XMLUtil return not null document for input " + xmlInputSource;
        
        return document;
    }
    
    private void showParseError(Throwable t) {
        ErrorManager.getDefault().annotate(t, ErrorManager.UNKNOWN, "URL: " + xmlURL, null, null, null); // NOI18N
        ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, t);
    }

    /** Builds the linear and the tree structure of module updates.
     */
    private void buildStructures (Document document) {

        if ( checkCanceled )
            return;

        if ( document.getDocumentElement() == null ) {
            // System.out.println( "WARNING <MODULE> is not element tag" ); // NOI18N
        }
        else {
            modules = new ArrayList();
            rootGroup = new ModuleGroup();
            checkOnceMore = new HashMap();

            // check if there is error tag in XML
            if ( detectErrorType (document) )
                return;
            
            NodeList allModules = document.getElementsByTagName( TAG_MODULE );
            
            processElement( document, document.getDocumentElement(), rootGroup );

            // Try to read timestamp
            Node attr = document.getDocumentElement().getAttributes().getNamedItem( "timestamp" ); // NOI18N
            if ( attr != null ) {
                String timeString = attr.getNodeValue() + "/GMT"; // NOI18N
                SimpleDateFormat formatter = new SimpleDateFormat( "ss/mm/HH/dd/MM/yyyy/zzz" ); // NOI18N
                ParsePosition pos = new ParsePosition(0);
                timeStamp = formatter.parse(timeString, pos);
            }
            checkAvailablesOnceMore();
        }
    }
    
    private boolean detectErrorType (Document document) {
        NodeList errors = document.getElementsByTagName(TAG_ERROR);
        
        // document contains error section
        if ( errors.getLength() > 0 ) {    
            NodeList auth_errors = document.getElementsByTagName(TAG_AUTH_ERROR);
            if ( auth_errors.getLength() > 0 ) {
                pError = AUTH_ERROR;
                assert false : "Unexcepted " + TAG_AUTH_ERROR + " in document " + document;
            } else {
                pError = PARSE_ERROR;
                NodeList other_errors = document.getElementsByTagName(TAG_OTHER_ERROR);
                if ( other_errors.getLength() > 0 )
                    errorMess = other_errors.item( 0 ).getAttributes().
                        getNamedItem(ATTR_MESSAGE_ERROR).getNodeValue();                                          
            }
        }
        return ( pError > NO_ERROR );
    }

    /** Finds module and module_group elements in the node's children and
     * process them
     *@param element The DOM Element node to be read.
     */
    private void processElement( Document document, Element element, ModuleGroup moduleGroup ) {

        NodeList nodeList = element.getChildNodes();
        for( int i = 0; i < nodeList.getLength(); i++ ) {

            if ( checkCanceled )
                return;

            Node node = nodeList.item( i );

            if ( node.getNodeType() != Node.ELEMENT_NODE ) {
                continue;
            }

            if ( ((Element)node).getTagName().equals( TAG_MODULE ) ) {
                ModuleUpdate update = ModuleUpdate.getModuleUpdate( xmlURL, node, document.getDocumentElement(), currentAT );

                if ( update != null ) {
                    if ( update.isUpdateAvailable() ) {
                        modules.add( update );
                        moduleGroup.addItem( update );
                    }
                    else if ( update instanceof L10NUpdate ) {
                        checkOnceMore.put( update, moduleGroup );
                    }
                }
            }
            else if ( ((Element)node).getTagName().equals( TAG_MODULE_GROUP ) ) {
                ModuleGroup group = new ModuleGroup( node );
                moduleGroup.addItem( group );
                processElement( document, (Element)node, group );
            }
            else if ( ((Element)node).getTagName().equals( TAG_NOTIFICATION ) ) {
                readNotification( node );
            }
        }
    }    
    
    /** Reads the notification */
    private void readNotification( Node node ) {

        if ( getNotificationText() != null ) {
            return;
        }

        try {
            Node attr = node.getAttributes().getNamedItem( ATTR_NOTIFICATION_URL );
            String textURL = attr == null ? null : attr.getNodeValue();

            if ( textURL != null )
                notificationURL = new URL( textURL );
        }
        catch ( java.net.MalformedURLException e ) {
            // TopManager.getDefault().notifyException( e );
            // let homepage set to null
        }

        StringBuffer sb = new StringBuffer();

        NodeList innerList = node.getChildNodes();

        for( int i = 0; i < innerList.getLength(); i++ ) {
            if ( innerList.item( i ).getNodeType() == Node.TEXT_NODE )  {
                sb.append( innerList.item( i ).getNodeValue() );
            }
        }

        if ( sb.length() > 0 )
            notificationText = sb.toString();
        else
            notificationText = null;

    }


    class ErrorCatcher implements org.xml.sax.ErrorHandler {
        
        public void error (SAXParseException e) throws SAXParseException {
            // normally a validity error (though we are not validating currently)
            throw e;
        }

        public void warning (SAXParseException e) throws SAXParseException {
            showParseError(e);
            // but continue...
        }

        public void fatalError (SAXParseException e) throws SAXParseException {
            throw e;
        }
        
    } //end of inner class ErrorPrinter
    
}
