/* A web application to read, edit and submit changes to a jfniki wiki in Freenet.
 *
 * 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 static ys.wikiparser.Utils.*; // DCI: clean up

import java.io.IOException;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;

import wormarc.FileManifest;
import wormarc.IOUtil;

import static fniki.wiki.HtmlUtils.*;
import static fniki.wiki.Validations.*;

import fniki.wiki.child.AsyncTaskContainer;
import fniki.wiki.child.DefaultRedirect;
import fniki.wiki.child.GotoRedirect;
import fniki.wiki.child.LoadingArchive;
import fniki.wiki.child.LoadingChangeLog;
import fniki.wiki.child.LoadingVersionList;
import fniki.wiki.child.QueryError;
import fniki.wiki.child.ResetToEmptyWiki;
import fniki.wiki.child.SettingConfig;
import fniki.wiki.child.Submitting;
import fniki.wiki.child.WikiContainer;

import fniki.freenet.filter.ContentFilterFactory;

// Aggregates a bunch of other ChildContainers and runs UI state machine.
public class WikiApp implements ChildContainer, WikiContext {
    public final static int LISTEN_PORT = 8083;
    private final static String FPROXY_PREFIX = "http://127.0.0.1:8888/";
    private final static boolean ALLOW_IMAGES = false;
    private final static Configuration DEFAULT_CONFIG =
        new Configuration(LISTEN_PORT,
                          ArchiveManager.FCP_HOST,
                          ArchiveManager.FCP_PORT,
                          FPROXY_PREFIX,
                          ALLOW_IMAGES,
                          ArchiveManager.FMS_HOST,
                          ArchiveManager.FMS_PORT,
                          "",
                          "",
                          ArchiveManager.FMS_GROUP,
                          ArchiveManager.BISS_NAME);

    // Time to wait for FCP before giving up on inverting private key.
    private final static int INVERT_TIMEOUT_MS = 30 * 1000;

    // Delegate to implement link, image and macro handling in wikitext.
    private final FreenetWikiTextParser.ParserDelegate mParserDelegate;

    // ChildContainers for non-modal UI elements.
    private final ChildContainer mDefaultRedirect;
    private final ChildContainer mGotoRedirect;
    private final ChildContainer mQueryError;
    private final ChildContainer mWikiContainer;
    private final ChildContainer mResetToEmptyWiki;

    // ChildContainers for modal UI states.
    private final ChildContainer mSettingConfig;
    private final ChildContainer mLoadingVersionList;
    private final ChildContainer mLoadingArchive;
    private final ChildContainer mSubmitting;
    private final ChildContainer mLoadingChangeLog;

    // The current default UI state.
    private ChildContainer mState;

    // Transient, per request state.
    private Request mRequest;

    private ArchiveManager mArchiveManager;

    // Belt and braces. Run the ContentFilter from the Freenet fred codebase
    // over all output before serving it.
    private ContentFilter mFilter;

    private String mFproxyPrefix = FPROXY_PREFIX;
    private boolean mAllowImages = ALLOW_IMAGES;
    private String mFormPassword;
    private int mListenPort = LISTEN_PORT;

    // final because it is called from the ctor.
    private final void resetContentFilter() {
        mFilter = ContentFilterFactory.create(mFproxyPrefix, containerPrefix());
    }

    public WikiApp(ArchiveManager archiveManager) {
        mParserDelegate = new LocalParserDelegate(this, archiveManager);

        mDefaultRedirect = new DefaultRedirect();
        mGotoRedirect = new GotoRedirect();
        mQueryError = new QueryError();
        mWikiContainer = new WikiContainer();
        mResetToEmptyWiki = new ResetToEmptyWiki(archiveManager);

        mSettingConfig = new SettingConfig();
        mLoadingVersionList = new LoadingVersionList(archiveManager);
        mLoadingArchive = new LoadingArchive(archiveManager);
        mSubmitting = new Submitting(archiveManager);
        mLoadingChangeLog = new LoadingChangeLog(archiveManager);

        mState = mWikiContainer;
        mArchiveManager = archiveManager;

        resetContentFilter();
    }

    public void setFproxyPrefix(String value) {
        if (!value.startsWith("http") && value.endsWith("/")) {
            throw new IllegalArgumentException("Expected a value starting with 'http' and ending with '/'");
        }
        mFproxyPrefix = value;
        resetContentFilter();
    }

    public void setAllowImages(boolean value) {
        mAllowImages = value;
    }

    public void setFormPassword(String value) {
        mFormPassword = value;
    }

    // Doesn't change port, just sets value returned by getInt("listen_port", -1)
    public void setListenPort(int value) {
        mListenPort = value;
    }

    private ChildContainer setState(WikiContext context, ChildContainer container) {
        if (mState == container) {
            return mState;
        }

        System.err.println(String.format("[%s] => [%s]",
                                         mState.getClass().getName(),
                                         container.getClass().getName()));
        if (mState != null && mState instanceof ModalContainer) {
            ((ModalContainer)mState).exited();
        }

        mState = container;

        if (mState instanceof ModalContainer) {
            ((ModalContainer)mState).entered(context);
        }

        return mState;
    }

    // This function defines the UI state machine.
    private ChildContainer routeRequest(WikiContext request)
        throws IOException {

        // Fail immediately if there are problems in the glue code.
        if (request.getPath() == null) {
            throw new RuntimeException("Assertion Failure: path == null");
        }
        if (request.getQuery() == null) {
            throw new RuntimeException("Assertion Failure: query == null");
        }
        if (request.getAction() == null) {
            throw new RuntimeException("Assertion Failure: action == null");
        }
        if (request.getTitle() == null) {
            throw new RuntimeException("Assertion Failure: title == null");
        }

        String action = request.getAction();

        if (mState instanceof ModalContainer) {
            // Handle transitions out of modal UI states.
            ModalContainer state = (ModalContainer)mState;
            if (action.equals("finished")) {
                System.err.println("finished");
                if (!state.isFinished()) {
                    System.err.println("canceling");
                    state.cancel();
                    try {
                        Thread.sleep(250); // HACK
                    }
                    catch (InterruptedException ie) {
                    }
                }
                // No "else" because it might have finished while sleeping.
                if (state.isFinished()) {
                    System.err.println("finished");
                    setState(request, mWikiContainer);
                    return mGotoRedirect;
                }
            }
            return state;  // Don't leave the modal UI state until finished.
        }

        String path = request.getPath();
        int slashCount = 0;
        for (int index = 0; index < path.length(); index++) {
            if (path.charAt(index) == '/') {
                slashCount++;
            }
        }

        // DCI: Fix. Use a hashmap of paths -> instances for static paths
        System.err.println("WikiApp.routeRequest: " + path);
        if (path.equals("fniki/config")) {
            return setState(request, mSettingConfig);
        } else if (path.equals("fniki/submit")) {
            return setState(request, mSubmitting);
        } else if (path.equals("fniki/changelog")) {
            return setState(request, mLoadingChangeLog);
        } else if (path.equals("fniki/getversions")) {
            return setState(request, mLoadingVersionList);
        } else if (path.equals("fniki/loadarchive")) {
            return setState(request, mLoadingArchive);
        } else if (path.equals("fniki/resettoempty")) {
            return setState(request, mResetToEmptyWiki);
        } else if (path.equals("")) {
            return mDefaultRedirect;
        } else if (slashCount != 0) {
            return mQueryError;
        } else {
            setState(request, mWikiContainer);
        }

        return mState;
    }

    // All requests are serialized! Hmmmm....
    public synchronized String handle(WikiContext context) throws ChildContainerException {
        try {
            ChildContainer childContainer = routeRequest(context);
            System.err.println("Request routed to: " + childContainer.getClass().getName());

            return mFilter.filter(childContainer.handle(context));
        } catch (ChildContainerException cce) {
            // Normal, used to do redirection.
            throw cce;
        } catch (Exception e) {
            context.logError("WikiApp.handle -- untrapped!:", e);
            throw new ServerErrorException("Coding error. Sorry :-(");
        }
    }

    ////////////////////////////////////////////////////////////
    private static class LocalParserDelegate extends WikiParserDelegate {
        final WikiContext mContext;

        LocalParserDelegate(WikiContext context, ArchiveManager archiveManager) {
            super(archiveManager);
            mContext = context;
        }

        // Implement base class abstract methods to supply the functionality
        // specific to live wikis, mostly by delegating to the WikiContext.
        protected String getContainerPrefix() {
            return containerPrefix(); // LATER: revisit.
        }

        protected boolean getFreenetLinksAllowed(){
            return mContext.getString("fproxy_prefix", null) != null;
        }

        protected boolean getImagesAllowed() {
            return mContext.getInt("allow_images", 0) != 0;
        }

        protected String makeLink(String containerRelativePath) {
            return mContext.makeLink(containerRelativePath);
        }

        protected String makeFreenetLink(String uri) {
            String prefix = mContext.getString("fproxy_prefix", null);
            if (prefix == null) {
                throw new RuntimeException("fproxy_prefix is null!");
            }
            return makeFproxyHref(prefix, uri);
        }
    }

    // Hmmmm... kind of weird. I can't remember why I used this static method instead of a constant.
    // NO trailing slash.
    private static String containerPrefix() { return "/plugins/fniki.freenet.plugin.Fniki"; }

    ////////////////////////////////////////////////////////////
    // Wiki context implementations.
    public WikiTextStorage getStorage() throws IOException { return mArchiveManager.getStorage(); }

    public FreenetWikiTextParser.ParserDelegate getParserDelegate() { return mParserDelegate; }

    public String getString(String keyName, String defaultValue) {
        if (keyName.equals("default_page")) {
            return "Front_Page";
        } else if (keyName.equals("fproxy_prefix")) {
            if (mFproxyPrefix == null) {
                return defaultValue;
            }
            return mFproxyPrefix;
        } else if (keyName.equals("parent_uri")) {
            if (mArchiveManager.getParentUri() == null) {
                // Can be null
                return defaultValue;
            }
            return mArchiveManager.getParentUri();
        } else if (keyName.equals("container_prefix")) {
            return containerPrefix();
        } else if (keyName.equals("form_password") && mFormPassword != null) {
            return mFormPassword;
        } else if (keyName.equals("default_wikitext")) {
            return getDefaultWikiText();
        } else if (keyName.equals("wikiname")) {
            if (mArchiveManager.getBissName() != null) {
                return mArchiveManager.getBissName();
            }
        } else if (keyName.equals("fms_group")) {
            if (mArchiveManager.getFmsGroup() != null) {
                return mArchiveManager.getFmsGroup();
            }
        }

        return defaultValue;
    }

    public int getInt(String keyName, int defaultValue) {
        if (keyName.equals("allow_images")) {
            return mAllowImages ? 1 : 0;
        }
        if (keyName.equals("listen_port")) {
            return mListenPort;
        }

        return defaultValue;
    }

    // Can return an invalid configuration. e.g. if fms id and private ssk are not set.
    public Configuration getConfiguration() {
        // Converts null values to ""
        return new Configuration(getInt("listen_port", LISTEN_PORT),
                                 mArchiveManager.getFcpHost(),
                                 mArchiveManager.getFcpPort(),
                                 getString("fproxy_prefix", FPROXY_PREFIX),
                                 mAllowImages,
                                 mArchiveManager.getFmsHost(),
                                 mArchiveManager.getFmsPort(),
                                 mArchiveManager.getFmsId(),
                                 mArchiveManager.getPrivateSSK(),
                                 mArchiveManager.getFmsGroup(),
                                 mArchiveManager.getBissName());
    }

    public Configuration getDefaultConfiguration() { return DEFAULT_CONFIG; }

    public String getPublicFmsId(String fmsId, String privateSSK) {
        if (fmsId == null || privateSSK == null || fmsId.indexOf("@") != -1) {
            return "???";
        }
        try {
            try {
                String publicKey = mArchiveManager.invertPrivateSSK(privateSSK, INVERT_TIMEOUT_MS);
                int pos = publicKey.indexOf(",");
                if (pos == -1 || pos < 5) {
                    return "???";
                }
                return fmsId + publicKey.substring("SSK".length(), pos);

            } catch (IllegalArgumentException iae) {
                // Was called with an invalid privateSSK value
                return "???";
            }
        } catch (IOException ioe) {
            logError("getPublicFmsId failed", ioe);
            return "???";
        }
    }

    // For setting data from forms and restoring saved settings.
    // throws unchecked Configuration.ConfigurationException
    public void setConfiguration(Configuration config) {
        config.validate();
        setListenPort(config.mListenPort);
        mArchiveManager.setFcpHost(config.mFcpHost);
        mArchiveManager.setFcpPort(config.mFcpPort);
        setFproxyPrefix(config.mFproxyPrefix);
        setAllowImages(config.mAllowImages);
        mArchiveManager.setFmsHost(config.mFmsHost);
        mArchiveManager.setFmsPort(config.mFmsPort);
        mArchiveManager.setFmsId(config.mFmsId);
        mArchiveManager.setPrivateSSK(config.mFmsSsk);
        mArchiveManager.setFmsGroup(config.mFmsGroup);
        mArchiveManager.setBissName(config.mWikiName);
    }

    public String makeLink(String containerRelativePath) {
        // Hacks to find bugs
        if (!containerRelativePath.startsWith("/")) {
            containerRelativePath = "/" + containerRelativePath;
            System.err.println("WikiApp.makeLink -- added leading '/': " +
                               containerRelativePath);
            (new RuntimeException("find missing /")).printStackTrace();

        }
        String full = containerPrefix() + containerRelativePath;
        while (full.indexOf("//") != -1) {
            System.err.println("WikiApp.makeLink -- fixing  '//': " +
                               full);
            full = full.replace("//", "/");
            (new RuntimeException("find extra /")).printStackTrace();
        }
        return full;
    }

    public void raiseRedirect(String toLocation, String msg) throws RedirectException {
        throw new RedirectException(toLocation, msg);
    }

    public void raiseNotFound(String msg) throws NotFoundException {
        throw new NotFoundException(msg);
    }

    public void raiseAccessDenied(String msg) throws AccessDeniedException {
        throw new AccessDeniedException(msg);
    }

    public void raiseServerError(String msg) throws ServerErrorException {
        throw new ServerErrorException(msg);
    }

    public void raiseDownload(byte[] data, String filename, String mimeType) throws DownloadException {
        throw new DownloadException(data, filename, mimeType);
    }

    public void logError(String msg, Throwable t) {
        if (msg == null) {
            msg = "null";
        }
        if (t == null) {
            t = new RuntimeException("FAKE EXCEPTION: logError called with t == null!");
        }
        System.err.println("Unexpected error: " + msg + " : " + t.toString());
        t.printStackTrace();
    }

    // Delegate to the mRequest helper instance set with setRequest().
    public String getPath() { return mRequest.getPath(); }
    public Query getQuery() { return mRequest.getQuery(); }

    public String getAction() { return mRequest.getQuery().get("action"); }
    public String getTitle() { return mRequest.getQuery().get("title"); }

    ////////////////////////////////////////////////////////////
    public void setRequest(Request request) {
        if (request == null) {
            throw new IllegalArgumentException("request == null");
        }
        mRequest = request;
    }

    private static String getDefaultWikiText() {
        try {
            return IOUtil.readUtf8StringAndClose(WikiApp.class.getResourceAsStream("/quickstart.txt"));
        } catch (IOException ioe) {
            return "Couldn't load default wikitext from jar???";
        }
    }
}