/* 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 static fniki.wiki.HtmlUtils.*;

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.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 {
    // Delegate to implement link, image and macro handling in wikitext.
    private final FreenetWikiTextParser.ParserDelegate mParserDelegate;

    private final ChildContainer mDefaultRedirect;
    private final ChildContainer mGotoRedirect;
    private final ChildContainer mQueryError;
    private final ChildContainer mWikiContainer;

    // Containers for asynchronous tasks.
    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 = "http://127.0.0.1:8888/";
    private boolean mAllowImages = true;
    private String mFormPassword;

    // 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();

        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;
    }

    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++;
            }
        }

        System.err.println("WikiApp.routeRequest: " + path);
        if (path.equals("fniki/submit")) {
            System.err.println("BC0");
            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("")) {
            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 implements FreenetWikiTextParser.ParserDelegate {
        // Pedantic.  Explictly copy references instead of making this class non-static
        // so that the code uses well defined interfaces.
        final WikiContext mContext;
        final ArchiveManager mArchiveManager;

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

        public boolean processedMacro(StringBuilder sb, String text) {
            if (text.equals("LocalChanges")) {
                try {
                    FileManifest.Changes changes  = mArchiveManager.getLocalChanges();
                    if (changes.isUnmodified()) {
                        sb.append("<br>No local changes.<br>");
                        return true;
                    }
                    appendChangesHtml(changes, containerPrefix(), sb);
                    return true;
                } catch (IOException ioe) {
                    sb.append("{ERROR PROCESSING LOCALCHANGES MACRO}");
                    return true;
                }
            } else if (text.equals("TitleIndex")) {
                try {
                    for (String name : mArchiveManager.getStorage().getNames()) {
                        appendPageLink(containerPrefix(), sb, name, null, true);
                        sb.append("<br>");
                    }
                } catch (IOException ioe) {
                    sb.append("{ERROR PROCESSING TITLEINDEX MACRO}");
                    return true;
                }

                return true;
            }

            return false;
        }

        // CHK, SSK, USK freenet links.
        public void appendLink(StringBuilder sb, String text) {
            String fproxyPrefix = mContext.getString("fproxy_prefix", null);

            String[] link=split(text, '|');
            if (fproxyPrefix != null &&
                isValidFreenetUri(link[0])) {
                sb.append("<a href=\""+ makeFproxyHref(fproxyPrefix, link[0].trim()) +"\" rel=\"nofollow\">");
                sb.append(escapeHTML(unescapeHTML(link.length>=2 && !isEmpty(link[1].trim())? link[1]:link[0])));
                sb.append("</a>");
                return;
            }
            if (isValidLocalLink(link[0])) {
                // Link to an internal wiki page.
                sb.append("<a href=\""+ makeHref(mContext.makeLink("/" + link[0].trim())) +"\" rel=\"nofollow\">");
                sb.append(escapeHTML(unescapeHTML(link.length>=2 && !isEmpty(link[1].trim())? link[1]:link[0])));
                sb.append("</a>");
                return;
            }

            sb.append("<a href=\"" + makeHref(mContext.makeLink("/ExternalLink")) +"\" rel=\"nofollow\">");
            sb.append(escapeHTML(unescapeHTML(link.length>=2 && !isEmpty(link[1].trim())? link[1]:link[0])));
            sb.append("</a>");
        }

        // Only CHK and SSK freenet links.
        public void appendImage(StringBuilder sb, String text) {
            boolean allowed = mContext.getInt("allow_images", 0) != 0;
            if (!allowed) {
                sb.append("{IMAGES DISABLED. IMAGE WIKITEXT IGNORED}");
                return;
            }

            String fproxyPrefix = mContext.getString("fproxy_prefix", null);
            if (fproxyPrefix == null) {
                sb.append("{FPROXY PREFIX NOT SET. IMAGE WIKITEXT IGNORED}");
                return;
            }

            String[] link=split(text, '|');
            if (fproxyPrefix != null &&
                isValidFreenetUri(link[0]) &&
                !link[0].startsWith("freenet:USK@")) {
                String alt=escapeHTML(unescapeHTML(link.length>=2 && !isEmpty(link[1].trim())? link[1]:link[0]));
                sb.append("<img src=\"" + makeFproxyHref(fproxyPrefix, link[0].trim())
                          + "\" alt=\""+alt+"\" title=\""+alt+"\" />");
                return;
            }
            sb.append("{ERROR PROCESSING IMAGE WIKITEXT}");;
        }
    }

    // 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;
        }

        return defaultValue;
    }

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

    // DCI: Think this through.
    public String makeLink(String containerRelativePath) {
        // Hacks to find bugs
        if (!containerRelativePath.startsWith("/")) {
            containerRelativePath = "/" + containerRelativePath;
            System.err.println("WikiApp.makeLink -- added leading '/': " +
                               containerRelativePath);
        }
        String full = containerPrefix() + containerRelativePath;
        while (full.indexOf("//") != -1) {
            System.err.println("WikiApp.makeLink -- fixing  '//': " +
                               full);
            full = full.replace("//", "/");
        }
        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 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;
    }
}