package com.limegroup.gnutella.updates;

import java.io.File;
import java.io.IOException;
import java.util.StringTokenizer;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.xml.sax.SAXException;

import com.limegroup.gnutella.Assert;
import com.limegroup.gnutella.Connection;
import com.limegroup.gnutella.ErrorService;
import com.limegroup.gnutella.http.HTTPHeaderName;
import com.limegroup.gnutella.http.HttpClientManager;
import com.limegroup.gnutella.util.CommonUtils;
import com.limegroup.gnutella.util.FileUtils;
import com.limegroup.gnutella.util.ManagedThread;
import com.limegroup.gnutella.util.ThreadFactory;

/**
 * Used for parsing the signed_update_file.xml and updating any values locally.
 * Has a singleton pattern.
 */
public class UpdateManager {
    
    private static final Log LOG = LogFactory.getLog(UpdateManager.class);
    
    /**
     * Used when handshaking with other LimeWires. 
     */
    private String latestVersion;
    /**
     * The language specific string that contains the new features of the 
     * version discovered in the network
     */ 
    private String message = "";
    /**
     * true if message is as per the user's language  preferences.
     */
    private boolean usesLocale;
    
    private static UpdateManager INSTANCE=null;

    public static final String SPECIAL_VERSION = "@version@";
    
    /**
     * Whether or not we think we have a valid file on disk.
     */ 
    private boolean isValid;

    /**
     * Constructor, reads the latest update.xml file from the last run on the
     * network, and srores the values in latestVersion, message and usesLocale.
     * latestVersion is the only variable whose value is used after start up. 
     * The other two message and usesLocale are used only once when showing the 
     * user a message at start up. So although this class is a singleton, it's 
     * safe for the constructor to set these two values for the whole session.
     */
    private UpdateManager() {
        latestVersion = "0.0.0";
        
        byte[] content = FileUtils.readFileFully(new File(CommonUtils.getUserSettingsDir(),"update.xml"));
        if(content != null) {
            //we dont really need to verify, but we may as well...so here goes.
            UpdateMessageVerifier verifier = new UpdateMessageVerifier(content, true);//from disk
            boolean verified = verifier.verifySource();
            if(verified) {
                try {
                    String xml = new String(verifier.getMessageBytes(),"UTF-8");
                    UpdateFileParser parser = new UpdateFileParser(xml);
                    latestVersion = parser.getVersion();
                    message = parser.getMessage();
                    usesLocale = parser.usesLocale();
                    isValid = true;
                } catch(SAXException sax) {
                    LOG.error("invalid update xml", sax);
                } catch(IOException iox) {
                    LOG.error("iox updating", iox);
                }
            }
        }
    }
    
    public static synchronized UpdateManager instance() {
        if(INSTANCE==null)
            INSTANCE = new UpdateManager();
        return INSTANCE;
    }
    
    public synchronized String getVersion() {
        Assert.that(latestVersion!=null,"version not initilaized");
        return latestVersion;
    }
    
    /**
     * Returns whether or not we have a valid file on disk.
     */
    public boolean isValid() {
        return isValid;
    }

    public void checkAndUpdate(Connection connection) {
		String nv = connection.getVersion();
		if(LOG.isTraceEnabled())
            LOG.trace("Update check: myVersion: "+
                      latestVersion+", theirs: "+nv);
        String myVersion = null;
        if(latestVersion.equals(SPECIAL_VERSION))
            myVersion = "0.0.0"; // consider special to be empty for this purpose.
        else //use the original value of latestVersion
            myVersion = latestVersion;
        if(!isGreaterVersion(nv,myVersion))
            return;        
        if(nv.equals(SPECIAL_VERSION))// should never see this on the network!!
            return;//so this should never happen
        final Connection c = connection;
        final String myversion = myVersion;
        ThreadFactory.startThread(new Runnable() {
            public void run() {
                LOG.trace("Getting update file");
                final String UPDATE = "/update.xml";
                //if we get host or port incorrectly, we will not be able to 
                //establish a connection and just return, its fail safe. 
                String ip = c.getAddress();
                int port = c.getPort();
                String connectTo = "http://" + ip + ":" + port + UPDATE;
                HttpClient client = HttpClientManager.getNewClient();
                HttpMethod get = new GetMethod(connectTo);
                get.addRequestHeader("Cache-Control", "no-cache");
                get.addRequestHeader("User-Agent", CommonUtils.getHttpServer());
                get.addRequestHeader(HTTPHeaderName.CONNECTION.httpStringValue(),
                                     "close");
                try {
                    client.executeMethod(get);
                    byte[] data = get.getResponseBody();
                    if( data == null )
                        return;
                    UpdateMessageVerifier verifier =
                         new UpdateMessageVerifier(data, false);// from network
                    boolean verified = false;
                    try {
                        verified = verifier.verifySource();
                    } catch (ClassCastException ccx) {
                        verified = false;
                    }
                    if(!verified)
                        return;
                    LOG.trace("Verified file contents");
                    String xml = new String(verifier.getMessageBytes(),"UTF-8");
                    UpdateFileParser parser = new UpdateFileParser(xml);
                    if(LOG.isTraceEnabled())
                        LOG.trace("New version: "+parser.getVersion());
                    //we checked for new version while handshaking, but we
                    //should check again with the authenticated xml data.
                    String newVersion = parser.getVersion();
                    if(newVersion==null)
                        return;
                    if(isGreaterVersion(newVersion,myversion)) {
                        LOG.trace("committing new update file");
                        synchronized(UpdateManager.this) {
                            commitVersionFile(data);//could throw an exception
                            //committed file, update the value of latestVersion
                            latestVersion = newVersion;
                            if(LOG.isTraceEnabled())
                                LOG.trace("commited file. Latest is:" +
                                          latestVersion);
                        }
                    }
                } catch(IOException iox) {
                    LOG.warn("iox on network, on disk, who knows??", iox);
                    //IOException - reading from socket, writing to disk etc.
                    return;
                } catch(SAXException sx) {
                    LOG.error("invalid xml", sx);
                    //SAXException - parsing the xml
                    return; //We can't continue...forget it.
                } catch(Throwable t) {
                    ErrorService.error(t);
                } finally {
                    if( get != null )
                        get.releaseConnection();
                }
            }//end of run
        }, "UpdateFileRequestor");
    }

    /**
     * compares newVer with oldVer. and returns true iff newVer is a newer 
     * version, false if neVer <= older.
     * <p>
     * <pre>
     * treats @version@ as the highest version possible. The danger is that 
     * we may try to get updates from all files that have  @version@ in the 
     * field. This is undesirable. So if we think the latest version is 
     * @version@ we do not put an X-Version header in the handshaking.
     * </pre>
     */
    public static boolean isGreaterVersion(String newVer, String oldVer) {
        if(newVer==null && oldVer==null)
            return false;
        if(newVer==null)//old is newer
            return false;
        if(oldVer==null) //new is newer
            return true;
        if(newVer.equals(oldVer))//same
            return false;
        if(newVer.equals(SPECIAL_VERSION)) //new is newer
            return true;
        if(oldVer.equals(SPECIAL_VERSION)) //old is newer
            return false;
        //OK. Now lets look at numbers
        int o1, o2 = -1;
        int n1, n2 = -1;
        try {
            StringTokenizer tokenizer = new StringTokenizer(oldVer,".");
            if(tokenizer.countTokens() < 2)
                return false;
            o1 = (new Integer(tokenizer.nextToken())).intValue();
            o2 = (new Integer(tokenizer.nextToken())).intValue();
            tokenizer = new StringTokenizer(newVer,".");
            if(tokenizer.countTokens() < 2)
                return false;
            n1 = (new Integer(tokenizer.nextToken())).intValue();
            n2 = (new Integer(tokenizer.nextToken())).intValue();
        } catch(NumberFormatException nfe) {
            return false;
        }
        if(n1>o1)
            return true;
        else if(n1==o1 && n2>o2)
            return true;        
        return false;
    }

    /**
     *  writes data to signed_updateFile
     */ 
    private void commitVersionFile(byte[] data) throws IOException {
        boolean ret = FileUtils.verySafeSave(CommonUtils.getUserSettingsDir(), "update.xml", data);
        if(!ret)
            throw new IOException("couldn't safely save!");
    }
}
