/*
 *  PHEX - The pure-java Gnutella-servent.
 *  Copyright (C) 2001 - 2007 Phex Development Group
 *
 *  This program 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 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program 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, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 * 
 *  --- CVS Information ---
 *  $Id: UpdateCheckRunner.java 4231 2008-07-15 16:01:10Z gregork $
 */
package phex.update;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.SocketException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.List;

import org.apache.commons.lang.SystemUtils;
import org.apache.commons.lang.time.DateUtils;

import phex.common.Environment;
import phex.common.LongObj;
import phex.common.log.NLogger;
import phex.event.UpdateNotificationListener;
import phex.gui.common.GUIRegistry;
import phex.prefs.core.PhexCorePrefs;
import phex.prefs.core.StatisticPrefs;
import phex.prefs.core.UpdatePrefs;
import phex.servent.Servent;
import phex.share.SharedFilesService;
import phex.statistic.StatisticProvider;
import phex.statistic.StatisticProviderConstants;
import phex.statistic.StatisticsManager;
import phex.utils.IOUtil;
import phex.utils.VersionUtils;
import phex.xml.sax.DPhex;
import phex.xml.sax.DUpdateRequest;
import phex.xml.sax.DUpdateResponse;
import phex.xml.sax.XMLBuilder;
import phex.xml.sax.DUpdateResponse.VersionType;

/**
 * The UpdateCheckRunner handles regular update check against the phex website.
 * It is also used to collect Phex statistics to identify possible problem areas
 * with new versions.<br/>
 * Phex informations are transmitted in an XML structure, the update information
 * response is also returned in an XML structure.
 */
public class UpdateCheckRunner implements Runnable
{
    private static final String UPDATE_CHECK_URL = "http://www.phex.org/update/update.php";
    
    private Throwable updateCheckError;
    private UpdateNotificationListener listener;
    private String releaseVersion;
    private String betaVersion;
    private boolean isBetaInfoShown;
    
    public UpdateCheckRunner( UpdateNotificationListener updateListener, boolean showBetaInfo )
    {
        listener = updateListener;
        this.isBetaInfoShown = showBetaInfo;
    }
    
    /**
     * Trigger a automated background update check. This is the standard Phex
     * check done every week to display the update dialog or just collect Phex
     * statistics.
     * The call is not blocking.
     */
    public static void triggerAutoBackgroundCheck(
        final UpdateNotificationListener updateListener, 
        final boolean showBetaInfo )
    {
        if ( UpdatePrefs.LastUpdateCheckTime.get().longValue() >
             System.currentTimeMillis() - DateUtils.MILLIS_PER_DAY * 7 )
        {
            NLogger.debug( UpdateCheckRunner.class, "No update check necessary." );
            return;
        }
        NLogger.debug( UpdateCheckRunner.class, "Triggering update check." );
        UpdateCheckRunner runner = new UpdateCheckRunner( updateListener, showBetaInfo );
        Environment.getInstance().executeOnThreadPool( runner, "UpdateCheckRunner" );
    }
    
    public String getReleaseVersion()
    {
        return releaseVersion;
    }

    public String getBetaVersion()
    {
        return betaVersion;
    }

    /**
     * Returns a possible Throwable that could be thrown during the update check
     * or null if no error was caught.
     * @return a possible Throwable that could be thrown during the update check
     * or null if no error was caught.
     */
    public Throwable getUpdateCheckError()
    {
        return updateCheckError;
    }
    
    /**
     * @see java.lang.Runnable#run()
     */
    public void run()
    {
        try
        {            
            performUpdateCheck();
        }
        catch ( Throwable exp )
        {
            updateCheckError = exp;
            NLogger.warn( UpdateCheckRunner.class, exp, exp );
        }
    }
    
    private void performUpdateCheck()
    {
        URL url;
        DPhex dPhex;
        try
        {
            url = new URL( UPDATE_CHECK_URL );
            HttpURLConnection connection = (HttpURLConnection)url.openConnection();
            connection.setUseCaches( false );
            connection.setRequestProperty( "User-Agent", Environment.getPhexVendor() );
            connection.setRequestMethod( "POST" );
            connection.setDoOutput( true );
            connection.setRequestProperty( "Content-Type",
                "text/xml; charset=UTF-8" );
                        
            OutputStream outStream = connection.getOutputStream();
            byte[] data = buildXMLUpdateRequest();
            if ( data == null )
            {
                throw new IOException( "Missing XML update data" );
            }
            outStream.write( data );
            if ( NLogger.isDebugEnabled( UpdateCheckRunner.class ) )
            {
                NLogger.debug( UpdateCheckRunner.class, new String(data) );
            }
            
            // dont need to buffer stream already done by Properties.load()
            InputStream inStream = connection.getInputStream();
            if ( NLogger.isDebugEnabled( UpdateCheckRunner.class ) )
            {
                byte[] respData = IOUtil.toByteArray( inStream );
                NLogger.debug( UpdateCheckRunner.class, new String(respData) );
                inStream = new ByteArrayInputStream( respData );
            }
            dPhex = XMLBuilder.readDPhexFromStream( inStream );
        }
        catch ( MalformedURLException exp )
        {
            updateCheckError = exp;
            NLogger.warn( UpdateCheckRunner.class, exp, exp );
            assert false;
            throw new RuntimeException( );
        }
        catch ( UnknownHostException exp )
        {
            // can't find way to host
            // this maybe means we have no internet connection
            updateCheckError = exp;
            NLogger.warn( UpdateCheckRunner.class, exp, exp );
            return;
        }
        catch ( SocketException exp )
        {
            // can't connect... maybe a proxy is in the way...
            updateCheckError = exp;
            NLogger.warn( UpdateCheckRunner.class, exp, exp );
            return;
        }
        catch ( IOException exp )
        {
            updateCheckError = exp;
            NLogger.warn( UpdateCheckRunner.class, exp, exp );
            return;
        }

        UpdatePrefs.LastUpdateCheckTime.set( Long.valueOf(System.currentTimeMillis()) );
        
        DUpdateResponse response = dPhex.getUpdateResponse();
        List<VersionType> versionList = response.getVersionList();
        VersionType latestReleaseVersion = null;
        VersionType latestBetaVersion = null;
        
        for ( VersionType currentVersion : versionList )
        {
            if ( currentVersion.isBeta() )
            {
                if ( latestBetaVersion == null || VersionUtils.compare(
                    currentVersion.getId(), latestBetaVersion.getId() ) > 0 )
                {
                    latestBetaVersion = currentVersion;
                }
            }
            else
            {
                if ( latestReleaseVersion == null || VersionUtils.compare(
                    currentVersion.getId(), latestReleaseVersion.getId() ) > 0 )
                {
                    latestReleaseVersion = currentVersion;
                }
            }
        }
        
        
        betaVersion = "0";
        releaseVersion = "0";
        if ( latestBetaVersion != null )
        {
            betaVersion = latestBetaVersion.getId();
        }
        if ( latestReleaseVersion != null )
        {
            releaseVersion = latestReleaseVersion.getId();
        }
        
        int releaseCompare = 0;
        int betaCompare = 0;
        betaCompare = VersionUtils.compare( betaVersion,
            VersionUtils.getFullProgramVersion() );
        releaseCompare = VersionUtils.compare( releaseVersion,
            VersionUtils.getFullProgramVersion() );
        
        if ( releaseCompare <= 0 && betaCompare <= 0 )
        {
            PhexCorePrefs.save( false );
            return;
        }
        
        betaCompare = VersionUtils.compare( betaVersion, 
            UpdatePrefs.LastBetaUpdateCheckVersion.get() );        
        releaseCompare = VersionUtils.compare( releaseVersion,
            UpdatePrefs.LastUpdateCheckVersion.get() );

        int verDiff = VersionUtils.compare( betaVersion,
            releaseVersion );

        boolean triggerUpdateNotification = false;
        if ( releaseCompare > 0 )
        {
            UpdatePrefs.LastUpdateCheckVersion.set( releaseVersion );
            triggerUpdateNotification = true;
        }
        if ( betaCompare > 0 )
        {
            UpdatePrefs.LastBetaUpdateCheckVersion.set( betaVersion );
            triggerUpdateNotification = true;
        }

        if ( verDiff > 0 )
        {
            // reset release version since beta is more up-to-date
            releaseVersion = null;
        }
        else
        {
            // reset beta version since release is the one to go.
            betaVersion = null;
        }

        PhexCorePrefs.save( false );
        if ( triggerUpdateNotification )
        {
            fireUpdateNotification();
        }
    }
    
    private void fireUpdateNotification()
    {
        listener.updateNotification( this );
    }
    
    private byte[] buildXMLUpdateRequest()
    {
        Servent servent = Servent.getInstance();
        try
        {
            DPhex dPhex = new DPhex();
            DUpdateRequest dRequest = new DUpdateRequest();
            dPhex.setUpdateRequest( dRequest );
            
            dRequest.setCurrentVersion( VersionUtils.getFullProgramVersion() );
            dRequest.setStartupCount( StatisticPrefs.TotalStartupCounter.get().intValue() );
            dRequest.setLafUsed( GUIRegistry.getInstance().getUsedLAFClass() );
            dRequest.setJavaVersion( System.getProperty( "java.version" ) );
            dRequest.setOperatingSystem( SystemUtils.OS_NAME );
            
            dRequest.setHostid( servent.getServentGuid().toHexString() );
            dRequest.setShowBetaInfo( isBetaInfoShown );
            dRequest.setLastInfoId( UpdatePrefs.LastShownUpdateInfoId.get().intValue() );
            
            String lastCheckVersion;
            if ( VersionUtils.compare( UpdatePrefs.LastUpdateCheckVersion.get(),
                UpdatePrefs.LastBetaUpdateCheckVersion.get() ) > 0 )
            {
                lastCheckVersion = UpdatePrefs.LastUpdateCheckVersion.get();
            }
            else
            {
                lastCheckVersion = UpdatePrefs.LastBetaUpdateCheckVersion.get();
            }
            dRequest.setLastCheckVersion( lastCheckVersion );
            
            StatisticsManager statMgr = servent.getStatisticsService();
            
            StatisticProvider uptimeProvider = statMgr.getStatisticProvider(
                StatisticsManager.UPTIME_PROVIDER );
            dRequest.setAvgUptime(
                ((LongObj)uptimeProvider.getAverageValue()).value );
            
            StatisticProvider dailyUptimeProvider = statMgr.getStatisticProvider(
                StatisticsManager.DAILY_UPTIME_PROVIDER );
            dRequest.setDailyAvgUptime(
                ((Integer)dailyUptimeProvider.getValue()).intValue() );
                
            StatisticProvider downloadProvider = statMgr.getStatisticProvider(
                StatisticProviderConstants.TOTAL_DOWNLOAD_COUNT_PROVIDER );
            dRequest.setDownloadCount(
                (int)((LongObj)downloadProvider.getValue()).value );
                
            StatisticProvider uploadProvider = statMgr.getStatisticProvider(
                StatisticProviderConstants.TOTAL_UPLOAD_COUNT_PROVIDER );
            dRequest.setUploadCount(
                (int)((LongObj)uploadProvider.getValue()).value );
                
            SharedFilesService sharedFilesService = Servent.getInstance().getSharedFilesService();
            dRequest.setSharedFiles( sharedFilesService.getFileCount() );
            dRequest.setSharedSize( sharedFilesService.getTotalFileSizeInKb() );
            
            dRequest.setErrorLog( getErrorLogFileTail() );
            
            return phex.xml.sax.XMLBuilder.serializeToBytes( dPhex );
        }
        catch ( IOException exp )
        {
            NLogger.error( UpdateCheckRunner.class, exp, exp );
            return null;
        }
    }
    
    private String getErrorLogFileTail()
    {
        try
        {
            File logFile = Environment.getInstance().getPhexConfigFile( "phex.error.log" );
            if ( !logFile.exists() )
            {
                return null;
            }
            RandomAccessFile raf = new RandomAccessFile( logFile, "r" );
            long pos = Math.max( raf.length() - 10 * 1024, 0 );
            raf.seek(pos);
            byte[] buffer = new byte[ (int)Math.min( 10*1024, raf.length() ) ];
            int lenRead = raf.read( buffer );
            return new String( buffer, 0, lenRead );
        }
        catch ( IOException exp )
        {
            NLogger.error( UpdateCheckRunner.class, exp, exp );
            return exp.toString();
        }
    }
}
