/*
 * This file is part of muCommander, http://www.mucommander.com
 *
 * muCommander is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * muCommander is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.mucommander;

import java.io.InputStream;

import javax.xml.parsers.SAXParserFactory;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import com.mucommander.commons.file.FileFactory;

/**
 * Retrieves information about the latest release of muCommander.
 * <p>
 * The {@link com.mucommander.RuntimeConstants#VERSION_URL} URL contains information
 * about the latest release of muCommander:<br>
 * - date of latest release.<br>
 * - latest official version.<br>
 * - where to download the latest version from.<br>
 * This class is used to access that information and compare them with what is known
 * of the current one, making it possible to notify users of new releases.
 * </p>
 * <p>
 * Checking for new releases is a fairly straightforward process, and can be done
 * with a few lines of code:
 * <pre>
 * VersionChecker version;
 *
 * try {
 *     version = VersionChecker.getInstance();
 *     if(version.isNewVersionAvailable())
 *         System.out.println("A new version of muCommander is available");
 *     else
 *         System.out.println("You've got the latest muCommander version");
 *    }
 * catch(Exception e) {System.err.println("An error occurred.");}
 * </pre>
 * </p>
 * <p>
 * muCommander is considered up to date if:<br>
 * - the {@link com.mucommander.RuntimeConstants#VERSION local version} is
 *   not smaller than the remote one.<br>
 * - the {@link com.mucommander.RuntimeConstants#BUILD_DATE local release date} is
 *   not smaller than the remote one.<br>
 * While comparing release dates seems a bit odd - after all, if a new version is release,
 * a new version number should be created. However, it's possible to download development
 * versions of the current release, and those might be updated almost daily. Comparing dates
 * makes it possible to automate the whole process without having to worry about out version
 * numbers growing silly.
 * </p>
 * @author Maxence Bernard, Nicolas Rinaudo
 */
public class VersionChecker extends DefaultHandler {
	private static final Logger LOGGER = LoggerFactory.getLogger(VersionChecker.class);
	
    // - XML structure ----------------------------------------------------------
    // --------------------------------------------------------------------------
    /** Root XML element. */
    public static final String ROOT_ELEMENT    = "mucommander";
    /** Version XML element. */
    public static final String VERSION_ELEMENT = "latest_version";
    /** Download URL XML element. */
    public static final String DOWNLOAD_URL_ELEMENT = "download_url";
    /** JAR URL XML element. */
    public static final String JAR_URL_ELEMENT = "jar_url";
    /** Date XML element. */
    public static final String DATE_ELEMENT    = "release_date";



    // - XML parsing states -----------------------------------------------------
    // --------------------------------------------------------------------------
    /** Currently parsing the version tag. */
    public static final int STATE_VERSION      = 1;
    /** Currently parsing the download URL tag. */
    public static final int STATE_DOWNLOAD_URL = 2;
    /** Currently parsing the download URL tag. */
    public static final int STATE_JAR_URL      = 3;
    /** Currently parsing the date tag. */
    public static final int STATE_DATE         = 4;
    /** We're not quite sure what we're parsing. */
    public static final int STATE_UNKNOWN      = 5;



    // - Instance fields --------------------------------------------------------
    // --------------------------------------------------------------------------
    /** Remote version number. */
    private String latestVersion;
    /** Where to download the latest version. */
    private String downloadURL;
    /** URL to the latest JAR file. */
    private String jarURL;
    /** Remote release date. */
    private String releaseDate;
    /** Current state the parser is in. */
    private int    state;



    // - Initialisation ---------------------------------------------------------
    // --------------------------------------------------------------------------
    /**
     * Creates a new version checker instance.
     */
    private VersionChecker() {}

    /**
     * Retrieves a description of the latest released version of muCommander.
     * @return              a description of the latest released version of muCommander.
     * @exception Exception thrown if any error happens while retrieving the remote version.
     */
    public static VersionChecker getInstance() throws Exception {
        VersionChecker instance;

        LOGGER.debug("Opening connection to " + RuntimeConstants.VERSION_URL);

        // Parses the remote XML file using UTF-8 encoding.
        try (InputStream in = FileFactory.getFile(RuntimeConstants.VERSION_URL).getInputStream()) {
            SAXParserFactory.newInstance().newSAXParser().parse(in, instance = new VersionChecker());
        }
        catch(Exception e) {
            LOGGER.debug("Failed to read version XML file at "+RuntimeConstants.VERSION_URL, e);
            throw e;
        }

        // Makes sure we retrieved the information we were looking for.
        // We're not checking the release date as older version of muCommander
        // didn't use it.
        if(instance.latestVersion == null || instance.latestVersion.equals("") ||
           instance.downloadURL == null   || instance.downloadURL.equals(""))
            throw new Exception();

        return instance;
    }


    // - Remote version information ---------------------------------------------
    // --------------------------------------------------------------------------
    /**
     * Checks whether the remote version is newer than the current one.
     * @return <code>true</code> if the remote version is newer than the current one,
     *         <code>false</code> otherwise.
     */
    public boolean isNewVersionAvailable() {
        // If the local and remote versions are the same, compares release dates.
        if(latestVersion.equals(RuntimeConstants.VERSION.trim().toLowerCase())) {
            // This ensures backward compatibility - if the remote version file does not contain
            // release date information, ignore it.
            if(releaseDate.equals(""))
                return true;

            // Checks whether the remote release date is later than the current release date.
            return releaseDate.compareTo(RuntimeConstants.BUILD_DATE) > 0;
        }
        return true;
    }

    /**
     * Returns the version number of the latest muCommander release.
     * @return the version number of the latest muCommander release.
     */
    public String getLatestVersion() {return latestVersion;}

    /**
     * Returns the URL at which the latest version of muCommander can be downloaded.
     * @return the URL at which the latest version of muCommander can be downloaded.
     */
    public String getDownloadURL() {return downloadURL;}

    /**
     * Returns the URL to the latest JAR file, <code>null</code> if not available.
     * @return the URL to the latest JAR file.
     */
    public String getJarURL() {return jarURL;}

    /**
     * Returns the date at which the latest version of muCommander has been released.
     * <p>
     * The date format is YYYYMMDD.
     * </p>
     * @return the date at which the latest version of muCommander has been released.
     */
    public String getReleaseDate() {return releaseDate;}



    // - XML parsing ------------------------------------------------------------
    // --------------------------------------------------------------------------
    /**
     * Called when the XML document parsing has started.
     */
    @Override
    public void startDocument() {
        latestVersion = "";
        downloadURL   = "";
        jarURL        = "";
        releaseDate   = "";
    }

    /**
     * Notifies the parser of CDATA.
     */
    @Override
    public void characters(char[] ch, int offset, int length) {
        switch(state) {
        case STATE_VERSION:
            latestVersion += new String(ch, offset, length);
            break;
        case STATE_DOWNLOAD_URL:
            downloadURL += new String(ch, offset, length);
            break;
        case STATE_JAR_URL:
            jarURL += new String(ch, offset, length);
            break;
        case STATE_DATE:
            releaseDate += new String(ch, offset, length);
            break;
        }
    }

    /**
     * Notifies the parser that a new tag is starting.
     */
    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
        // Checks whether we know the tag and updates the current state.
        if(qName.equals(VERSION_ELEMENT))
            state = STATE_VERSION;
        else if(qName.equals(DOWNLOAD_URL_ELEMENT))
            state = STATE_DOWNLOAD_URL;
        else if(qName.equals(JAR_URL_ELEMENT))
            state = STATE_JAR_URL;
        else if(qName.equals(DATE_ELEMENT))
            state = STATE_DATE;
        else
            state = STATE_UNKNOWN;
    }

    /**
     * Notifies the parser that the current element is finished.
     */
    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {state = STATE_UNKNOWN;}

    /**
     * Notifies the parser that XML parsing is finished.
     */
    @Override
    public void endDocument() {
        // Make sure we're not keep meaningless whitecase characters in the data.
        latestVersion = latestVersion.toLowerCase().trim();
        downloadURL   = downloadURL.trim();
        jarURL        = jarURL.trim();
        if("".equals(jarURL))
            jarURL = null;
        releaseDate   = releaseDate.trim();

        // Logs the data if in debug mode.
        LOGGER.debug("download URL: "  + downloadURL);
        LOGGER.debug("jar URL: "       + jarURL);
        LOGGER.debug("latestVersion: " + latestVersion);
        LOGGER.debug("releaseDate:   " + releaseDate);
    }
}
