package org.fdroid.fdroid.updater;

import android.content.ContentValues;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;

import org.fdroid.fdroid.ProgressListener;
import org.fdroid.fdroid.RepoXMLHandler;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.RepoProvider;
import org.fdroid.fdroid.net.Downloader;
import org.fdroid.fdroid.net.DownloaderFactory;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

abstract public class RepoUpdater {

    public static final String PROGRESS_TYPE_PROCESS_XML = "processingXml";

    public static final String PROGRESS_DATA_REPO_ADDRESS = "repoAddress";

    public static RepoUpdater createUpdaterFor(Context ctx, Repo repo) {
        if (repo.fingerprint == null && repo.pubkey == null) {
            return new UnsignedRepoUpdater(ctx, repo);
        } else {
            return new SignedRepoUpdater(ctx, repo);
        }
    }

    protected final Context context;
    protected final Repo repo;
    private List<App> apps = new ArrayList<App>();
    private List<Apk> apks = new ArrayList<Apk>();
    private RepoUpdateRememberer rememberer = null;
    protected boolean usePubkeyInJar = false;
    protected boolean hasChanged = false;
    protected ProgressListener progressListener;

    public RepoUpdater(Context ctx, Repo repo) {
        this.context = ctx;
        this.repo    = repo;
    }

    public void setProgressListener(ProgressListener progressListener) {
        this.progressListener = progressListener;
    }

    public boolean hasChanged() {
        return hasChanged;
    }

    public List<App> getApps() {
        return apps;
    }

    public List<Apk> getApks() {
        return apks;
    }

    /**
     * For example, you may want to unzip a jar file to get the index inside,
     * or if the file is not compressed, you can just return a reference to
     * the downloaded file.
     *
     * @throws UpdateException All error states will come from here.
     */
    protected abstract File getIndexFromFile(File downloadedFile) throws
            UpdateException;

    protected abstract String getIndexAddress();

    protected Downloader downloadIndex() throws UpdateException {
        Downloader downloader = null;
        try {
            downloader = DownloaderFactory.create(getIndexAddress(), context);
            downloader.setCacheTag(repo.lastetag);

            if (progressListener != null) { // interactive session, show progress
                Bundle data = new Bundle(1);
                data.putString(PROGRESS_DATA_REPO_ADDRESS, repo.address);
                downloader.setProgressListener(progressListener, data);
            }

            downloader.downloadUninterrupted();

            if (downloader.isCached()) {
                // The index is unchanged since we last read it. We just mark
                // everything that came from this repo as being updated.
                Log.d("FDroid", "Repo index for " + getIndexAddress()
                        + " is up to date (by etag)");
            }

        } catch (IOException e) {
            if (downloader != null && downloader.getFile() != null) {
                downloader.getFile().delete();
            }
            throw new UpdateException(
                    repo,
                    "Error getting index file from " + repo.address,
                    e);
        }
        return downloader;
    }

    private int estimateAppCount(File indexFile) {
        int count = -1;
        try {
            // A bit of a hack, this might return false positives if an apps description
            // or some other part of the XML file contains this, but it is a pretty good
            // estimate and makes the progress counter more informative.
            // As with asking the server about the size of the index before downloading,
            // this also has a time tradeoff. It takes about three seconds to iterate
            // through the file and count 600 apps on a slow emulator (v17), but if it is
            // taking two minutes to update, the three second wait may be worth it.
            final String APPLICATION = "<application";
            count = Utils.countSubstringOccurrence(indexFile, APPLICATION);
        } catch (IOException e) {
            // Do nothing. Leave count at default -1 value.
        }
        return count;
    }


    public void update() throws UpdateException {

        File downloadedFile = null;
        File indexFile = null;
        try {

            Downloader downloader = downloadIndex();
            hasChanged = downloader.hasChanged();

            if (hasChanged) {

                downloadedFile = downloader.getFile();
                indexFile = getIndexFromFile(downloadedFile);

                // Process the index...
                SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
                XMLReader reader = parser.getXMLReader();
                RepoXMLHandler handler = new RepoXMLHandler(repo, progressListener);

                if (progressListener != null) {
                    // Only bother spending the time to count the expected apps
                    // if we can show that to the user...
                    handler.setTotalAppCount(estimateAppCount(indexFile));
                }

                reader.setContentHandler(handler);
                InputSource is = new InputSource(
                        new BufferedReader(new FileReader(indexFile)));

                reader.parse(is);
                apps = handler.getApps();
                apks = handler.getApks();

                rememberer = new RepoUpdateRememberer();
                rememberer.context = context;
                rememberer.repo = repo;
                rememberer.values = prepareRepoDetailsForSaving(handler, downloader.getCacheTag());
            }
        } catch (SAXException e) {
            throw new UpdateException(
                    repo, "Error parsing index for repo " + repo.address, e);
        } catch (FileNotFoundException e) {
            throw new UpdateException(
                    repo, "Error parsing index for repo " + repo.address, e);
        } catch (ParserConfigurationException e) {
            throw new UpdateException(
                    repo, "Error parsing index for repo " + repo.address, e);
        } catch (IOException e) {
            throw new UpdateException(
                    repo, "Error parsing index for repo " + repo.address, e);
        } finally {
            if (downloadedFile != null &&
                    downloadedFile != indexFile && downloadedFile.exists()) {
                downloadedFile.delete();
            }
            if (indexFile != null && indexFile.exists()) {
                indexFile.delete();
            }

        }
    }

    private ContentValues prepareRepoDetailsForSaving (RepoXMLHandler handler, String etag) {

        ContentValues values = new ContentValues();

        values.put(RepoProvider.DataColumns.LAST_UPDATED, Utils.DATE_FORMAT.format(new Date()));

        if (repo.lastetag == null || !repo.lastetag.equals(etag)) {
            values.put(RepoProvider.DataColumns.LAST_ETAG, etag);
        }

        /*
         * We read an unsigned index that indicates that a signed version
         * is available. Or we received a repo config that included the
         * fingerprint, so we need to save the pubkey now.
         */
        if (handler.getPubKey() != null &&
                (repo.pubkey == null || usePubkeyInJar)) {
            // TODO: Spend the time *now* going to get the etag of the signed
            // repo, so that we can prevent downloading it next time. Otherwise
            // next time we update, we have to download the signed index
            // in its entirety, regardless of if it contains the same
            // information as the unsigned one does not...
            Log.d("FDroid",
                    "Public key found - switching to signed repo for future updates");
            values.put(RepoProvider.DataColumns.PUBLIC_KEY, handler.getPubKey());
            usePubkeyInJar = false;
        }

        if (handler.getVersion() != -1 && handler.getVersion() != repo.version) {
            Log.d("FDroid", "Repo specified a new version: from "
                    + repo.version + " to " + handler.getVersion());
            values.put(RepoProvider.DataColumns.VERSION, handler.getVersion());
        }

        if (handler.getMaxAge() != -1 && handler.getMaxAge() != repo.maxage) {
            Log.d("FDroid",
                    "Repo specified a new maximum age - updated");
            values.put(RepoProvider.DataColumns.MAX_AGE, handler.getMaxAge());
        }

        if (handler.getDescription() != null && !handler.getDescription().equals(repo.description)) {
            values.put(RepoProvider.DataColumns.DESCRIPTION, handler.getDescription());
        }

        if (handler.getName() != null && !handler.getName().equals(repo.name)) {
            values.put(RepoProvider.DataColumns.NAME, handler.getName());
        }

        return values;
    }

    public RepoUpdateRememberer getRememberer() {
        return rememberer;
    }

    public static class RepoUpdateRememberer {

        private Context context;
        private Repo repo;
        private ContentValues values;

        public void rememberUpdate() {
             RepoProvider.Helper.update(context, repo, values);
        }

    }

    public static class UpdateException extends Exception {

        private static final long serialVersionUID = -4492452418826132803L;
        public final Repo repo;

        public UpdateException(Repo repo, String message) {
            super(message);
            this.repo = repo;
        }

        public UpdateException(Repo repo, String message, Exception cause) {
            super(message, cause);
            this.repo = repo;
        }
    }

}
