/* Class to manage reading from and writing Freenet WORM Archives.
 *
 * Copyright (C) 2010, 2011 Darrell Karbott
 *
 * This library 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.0 of the License, or (at your option) any later version.
 *
 * This library 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 library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 *
 * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
 *
 *  This file was developed as component of
 * "fniki" (a wiki implementation running over Freenet).
 */

package fniki.wiki;

import java.io.IOException;
import java.io.PrintStream;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import fmsutil.FMSUtil;
import wormarc.Archive;
import wormarc.AuditArchive;
import wormarc.ExternalRefs;
import wormarc.FileManifest;
import wormarc.IOUtil;
import wormarc.LinkDigest;
import wormarc.RootObjectKind;
import wormarc.io.FreenetIO;

public class ArchiveManager {
    public final static String FCP_HOST = "127.0.0.1";
    public final static int FCP_PORT = 9481;

    public final static String FMS_HOST = "127.0.0.1";
    public final static int FMS_PORT = 1119;
    public final static String FMS_GROUP = "biss.test000";
    public final static String BISS_NAME = "testwiki";
    // Maximum number of versions to read from FMS.
    private final static int MAX_VERSIONS = 50;

    String mFcpHost = FCP_HOST;
    int mFcpPort = FCP_PORT;
    String mFmsHost = FMS_HOST;
    int mFmsPort = FMS_PORT;
    String mFmsGroup = FMS_GROUP;
    String mBissName= BISS_NAME;

    String mPrivateSSK;
    String mFmsId;

    // Base64 SSK public key hash to FMS name. i.e. the part before '@'.
    Map<String, String> mNymLut = new HashMap<String, String>();

    // LATER: Revisit. This was HACK to work around the fact that GetCHKOnly=true
    //        is broken for splitfiles.
    //
    // Block hex digest to CHK key map.
    Map<String, String> mSha1ToChk = new HashMap<String, String>();

    String mParentUri;
    Archive mArchive;
    FileManifest mFileManifest;
    LocalWikiChanges mOverlay;

    public void setDebugOutput(PrintStream out) {
        FreenetIO.setDebugOutput(out);
    }

    private static void validatePrivateSSK(String value) {
        if (!value.startsWith("SSK@") || !value.endsWith(",AQECAAE/")) {
            throw new IllegalArgumentException("That doesn't look like a private SSK. " +
                                           "Did you forget the trailing '/'?");
        }
    }

    public void setPrivateSSK(String value) {
        validatePrivateSSK(value);
        mPrivateSSK = value;
    }

    public String invertPrivateSSK(String value, int timeoutMs) throws IOException {
        validatePrivateSSK(value);
        return makeIO().invertPrivateSSK(value, timeoutMs);
    }

    public String getPrivateSSK() { return mPrivateSSK; }
    public String getParentUri() { return mParentUri; }

    public void setFmsId(String value) {
        if (value.indexOf("@") != -1) {
            throw new IllegalArgumentException("FMS Id Should only include the part before the '@'!");
        }

        mFmsId = value;
    }

    public String getFmsId() { return mFmsId; }

    public void setFcpHost(String value) {mFcpHost = value; }
    public String getFcpHost() { return mFcpHost; }

    public void setFcpPort(int value) {mFcpPort = value; }
    public int getFcpPort() { return mFcpPort; }

    public void setFmsHost(String value) { mFmsHost = value; }
    public String getFmsHost() { return mFmsHost; }

    public void setFmsPort(int value) { mFmsPort = value; }
    public int getFmsPort() { return mFmsPort; }

    public void setFmsGroup(String value) { mFmsGroup = value; }
    public String getFmsGroup() { return mFmsGroup; }

    public void setBissName(String value) { mBissName= value; }
    public String getBissName() { return mBissName; }

    // DCI: Fix this to roll back state on exceptions.
    public void load(String uri) throws IOException {
        FreenetIO io = makeIO();
        io.setRequestUri(uri);
        Archive archive = Archive.load(io);
        validateUriHashes(archive, uri, true);

        mArchive = archive;
        mFileManifest = FileManifest.fromArchiveRootObject(mArchive);
        mOverlay = new LocalWikiChanges(mArchive, mFileManifest); // DCI: why copy ?
        mParentUri = uri;
    }

    public void createEmptyArchive() throws IOException {
        mArchive = new Archive();
        mFileManifest = FileManifest.fromArchiveRootObject(mArchive);
        mOverlay = new LocalWikiChanges(mArchive, mFileManifest); // DCI: why copy ?
        mParentUri = null;
    }

    public static class UpToDateException extends IOException {
        public UpToDateException() {
            super("There are no local changes to submit.");
        }
    }

    private FreenetIO makeIO() {
        return new FreenetIO(mFcpHost, mFcpPort, null, mSha1ToChk);
    }

    // The name of a jfniki archive includes the hash of the
    // full archive manifest file, and hashes of the SSK of
    // it's parent(s).
    //
    // use '_' instead of '|' because '|' gets percent escaped.
    // <sha1_of_am_file>[<underbar>sha1_of_parent_ssk]
    private static String makeUriNamePart(Archive archive) throws IOException {
        // Generate a unique SSK.
        LinkDigest digest = archive.getRootObject(RootObjectKind.ARCHIVE_MANIFEST);
        // The hash of the actual file, not just the chain head SHA.
        LinkDigest fileHash = IOUtil.getFileDigest(archive.getFile(digest));

        String parentKeyHashes = "";
        LinkDigest refsDigest = archive.getRootObject(RootObjectKind.PARENT_REFERENCES);
        if (!refsDigest.isNullDigest()) {
            ExternalRefs refs = ExternalRefs.fromBytes(archive.getFile(refsDigest));
            for (ExternalRefs.Reference ref : refs.mRefs) {
                if (ref.mKind != ExternalRefs.KIND_FREENET) {
                    continue;
                }
                parentKeyHashes += "_";
                parentKeyHashes += IOUtil.getFileDigest(IOUtil.toStreamAsUtf8(ref.mExternalKey))
                    .hexDigest(8);
            }
        }

        return fileHash.hexDigest(8) + parentKeyHashes;
    }

    private static void validateUriHashes(Archive archive,
                                          String uri,
                                          boolean allowNoParents) throws IOException {
        String[] fields = uri.split("/");
        if (fields.length != 2) {
            throw new IOException("Couldn't parse uri: " + uri);
        }
        fields = fields[1].split("_");
        if (fields.length < 1) {
            throw new IOException("Couldn't parse uri: " + uri);
        }

        String[] expected  = makeUriNamePart(archive).split("_");

        for (int index = 0; index < expected.length; index++) {
            if (index >= fields.length) {
                continue;
            }
        }

        if (fields.length == 1 && fields[0].equals(expected[0])) {
            // LATER: tighten up.
            // For now, allow old URIs that don't have parent info hashes.
            return;
        }

        if (expected.length != fields.length) {
            throw new IOException("Hash validation failed(0)! Inserter is lying about contents.");
        }

        for (int index = 0; index < expected.length; index++) {
            if (!expected[index].equals(fields[index])) {
                throw new IOException("Hash validation failed(1)! Inserter is lying about contents.");
            }
        }
    }

    private String getInsertUri(Archive archive) throws IOException {
        String uri = mPrivateSSK + makeUriNamePart(archive);
        validateUriHashes(archive, uri, false);
        return uri;
    }

    // DCI: commitAndPushToFreenet() ?
    public String pushToFreenet(PrintStream out) throws IOException {
        FileManifest.Changes changes = mFileManifest.diffTo(mArchive, mOverlay);
        if (changes.isUnmodified()) {
            throw new IOException("Didn't find any local changes to submit.");
        }

        // Copy so that we can cleanly role back if an exception is raised.
        Archive copy = mArchive.deepCopy();
        FileManifest files = FileManifest.fromArchiveRootObject(copy);

        // Commit local changes to the Archive.
        copy.unsetRootObject(RootObjectKind.PARENT_REFERENCES);

        // Update the archive
        copy.startUpdate();
        files.updateFrom(copy, mOverlay);

        LinkDigest digest = copy.updateRootObject(files.toBytes(), RootObjectKind.FILE_MANIFEST);

        if (mParentUri != null) {
            out.println("Set PARENT_REFERENCES: " + mParentUri);
            List<String> keys = Arrays.asList(mParentUri);
            LinkDigest refs =
                copy.updateRootObject(ExternalRefs.create(keys, ExternalRefs.KIND_FREENET)
                                      .toBytes(),
                                      RootObjectKind.PARENT_REFERENCES);
        }

        copy.commitUpdate();
        copy.compressAndUpdateArchiveManifest();

        // Generate a unique SSK based on the SHA hash of the archive manifest.
        String insertUri = getInsertUri(copy);

        out.println("Insert URI: " + insertUri);

        // Push the updated version into Freenet.
        FreenetIO io = makeIO();
        io.setInsertUri(insertUri);
        out.println("Writing to Freenet...");
        copy.write(io);

        if (mFmsId != null) {
            try {
                out.println("Sending FMS update notification to: " + mFmsGroup);
                FMSUtil.sendBISSMsg(mFmsHost, mFmsPort, mFmsId, mFmsGroup,
                                    mBissName, io.getRequestUri());
            } catch (IOException ioe) {
                out.println("FMS send failed: " + ioe.getMessage());
            }
        }

        // Don't update any state until all calls which could raise have finished.
        mArchive = copy;
        mFileManifest = FileManifest.fromArchiveRootObject(mArchive);
        mOverlay = new LocalWikiChanges(mArchive, mFileManifest);
        mParentUri = io.getRequestUri();
        out.println("Request URI: " + mParentUri);

        return mParentUri;
    }

    public FileManifest.Changes getLocalChanges() throws IOException {
        return mFileManifest.diffTo(mArchive, mOverlay);
    }

    public void readChangeLog(PrintStream out,
                              AuditArchive.ChangeLogCallback callback) throws IOException {

        if (mParentUri == null) {
            throw new IOException("URI not set!");
        }

        ExternalRefs.Reference head =
            new ExternalRefs.Reference(ExternalRefs.KIND_FREENET, mParentUri);

        FreenetIO freenetResolver = makeIO();
        Archive archive = freenetResolver.resolve(head);
        AuditArchive.getManifestChangeLog(head, archive, freenetResolver, callback);
    }

    public List<FMSUtil.BISSRecord> getRecentWikiVersions(PrintStream out) throws IOException {
        List<FMSUtil.BISSRecord> records =
            FMSUtil.getBISSRecords(mFmsHost, mFmsPort, mFmsId, mFmsGroup, mBissName, MAX_VERSIONS);

        // LATER: do better.
        for (FMSUtil.BISSRecord record : records) {
            String fields[] = record.mFmsId.split("@");
            if (fields.length != 2) {
                continue;
            }
            mNymLut.put(fields[1].trim(), fields[0].trim());
        }

        return records;
    }

    public WikiTextStorage getStorage() throws IOException {
        if (mOverlay == null) {
            throw new IllegalStateException("No archive loaded!");
        }
        return mOverlay;
    }

    public String getNym(String sskRequestUri, boolean showPublicKey) {
        int start = sskRequestUri.indexOf("@");
        int end = sskRequestUri.indexOf(",");
        if (start == -1 || end == -1 || start >= end) {
            return "???";
        }

        String publicKeyHash = sskRequestUri.substring(start + 1, end - start + 3);

        // SSK@THIS_PART,
        String nym = mNymLut.get(publicKeyHash);
        if (nym == null) {
            nym = "???";
        }
        if (!showPublicKey) {
            return nym;
        }
        return nym + "@" + publicKeyHash;
    }
}