package freenet.clients.http;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import freenet.client.DefaultMIMETypes;
import freenet.client.HighLevelSimpleClient;
import freenet.client.filter.ContentFilter;
import freenet.client.filter.FilterOperation;
import freenet.client.filter.UnsafeContentTypeException;
import freenet.client.filter.ContentFilter.FilterStatus;
import freenet.l10n.NodeL10n;
import freenet.node.NodeClientCore;
import freenet.support.HTMLNode;
import freenet.support.Logger;
import freenet.support.MultiValueTable;
import freenet.support.api.Bucket;
import freenet.support.api.HTTPRequest;
import freenet.support.api.HTTPUploadedFile;
import freenet.support.io.Closer;
import freenet.support.io.FileBucket;
import freenet.support.io.FileUtil;

/**
 * Allows the user to run the content filter on a file and view the result.
 */
public class ContentFilterToadlet extends Toadlet implements LinkEnabledCallback {
    public final static String PATH = "/filterfile/";

    /**
     * What to do the the output from the content filter.
     */
    public enum ResultHandling {
        DISPLAY,
        SAVE
    }

    private final NodeClientCore core;

    public ContentFilterToadlet(HighLevelSimpleClient client, NodeClientCore clientCore) {
        super(client);
        this.core = clientCore;
    }

    @Override
    public String path() {
        return PATH;
    }

    public boolean isEnabled (ToadletContext ctx) {
        if(ctx == null) return false;
        boolean fullAccess = !container.publicGatewayMode() || ctx.isAllowedFullAccess();
        return ctx.isAdvancedModeEnabled() && fullAccess;
    }

    public void handleMethodGET(URI uri, final HTTPRequest request, final ToadletContext ctx)
            throws ToadletContextClosedException, IOException, RedirectException {
        if (container.publicGatewayMode() && !ctx.isAllowedFullAccess()) {
            sendUnauthorizedPage(ctx);
            return;
        }

        PageMaker pageMaker = ctx.getPageMaker();

        PageNode page = pageMaker.getPageNode(l10n("pageTitle"), ctx);
        HTMLNode pageNode = page.outer;
        HTMLNode contentNode = page.content;

        contentNode.addChild(ctx.getAlertManager().createSummary());

        contentNode.addChild(createContent(pageMaker, ctx));

        writeHTMLReply(ctx, 200, "OK", null, pageNode.generate());
    }

    public void handleMethodPOST(URI uri, final HTTPRequest request, final ToadletContext ctx)
            throws ToadletContextClosedException, IOException, RedirectException {
        if (container.publicGatewayMode() && !ctx.isAllowedFullAccess()) {
            sendUnauthorizedPage(ctx);
            return;
        }
        try {
            // Browse... button on filter page
            if (request.isPartSet("filter-local")) {
                try {
                    FilterOperation filterOperation = getFilterOperation(request);
                    ResultHandling resultHandling = getResultHandling(request);
                    String mimeType = request.getPartAsStringFailsafe("mime-type", 100);
                    String location = LocalFileFilterToadlet.PATH
                        + "?filter-operation=" + filterOperation
                        + "&result-handling=" + resultHandling
                        + "&mime-type=" + mimeType;
                    MultiValueTable<String, String> responseHeaders = MultiValueTable.from("Location", location);
                    ctx.sendReplyHeaders(302, "Found", responseHeaders, null, 0);
                } catch (BadRequestException e) {
                    String invalidPart = e.getInvalidRequestPart();
                    if ("filter-operation".equals(invalidPart)) {
                        writeBadRequestError(l10n("errorMustSpecifyFilterOperationTitle"), l10n("errorMustSpecifyFilterOperation"), ctx, true);
                    } else if ("result-handling".equals(invalidPart)) {
                        writeBadRequestError(l10n("errorMustSpecifyResultHandlingTitle"), l10n("errorMustSpecifyResultHandling"), ctx, true);
                    } else {
                        writeBadRequestError(l10n("errorBadRequestTitle"), l10n("errorBadRequest"), ctx, true);
                    }
                    return;
                }
            // Filter button on local file browser
            } else if (request.isPartSet(LocalFileBrowserToadlet.selectFile)) {
                handleFilterRequest(request, ctx, core, true);
            // Filter File button on filter page
            } else if (request.isPartSet("filter-upload")) {
                handleFilterRequest(request, ctx, core, false);
            } else {
                handleMethodGET(uri, new HTTPRequestImpl(uri, "GET"), ctx);
            }
        } finally {
            request.freeParts();
        }
    }

    private HTMLNode createContent(PageMaker pageMaker, ToadletContext ctx) {
        InfoboxNode infobox = pageMaker.getInfobox(l10n("filterFile"), "filter-file", true);
        HTMLNode filterBox = infobox.outer;
        HTMLNode filterContent = infobox.content;

        HTMLNode filterForm = ctx.addFormChild(filterContent, PATH, "filterForm");

        // apply read filter, write filter, or both
        //TODO: radio buttons to select, once ContentFilter supports write filtering
        filterForm.addChild("input",
                new String[] { "type", "name", "value" },
                new String[] { "hidden", "filter-operation", FilterOperation.BOTH.toString() });

        // display in browser or save to disk
        filterForm.addChild("input",
                new String[] { "type", "name", "value", "id" },
                new String[] { "radio", "result-handling", ResultHandling.DISPLAY.toString(), "result-handling-display" });
        filterForm.addChild("label",
                new String[] { "for" },
                new String[] { "result-handling-display" },
                l10n("displayResultLabel"));
        filterForm.addChild("br");
        filterForm.addChild("input",
                new String[] { "type", "name", "value", "id" },
                new String[] { "radio", "result-handling", ResultHandling.SAVE.toString(), "result-handling-save" });
        filterForm.addChild("label",
                new String[] { "for" },
                new String[] { "result-handling-save" },
                l10n("saveResultLabel"));
        filterForm.addChild("br");
        filterForm.addChild("br");

        // mime type
        filterForm.addChild("#", l10n("mimeTypeLabel") + ": ");
        filterForm.addChild("input",
                new String[] { "type", "name", "value" },
                new String[] { "text", "mime-type", "" });
        filterForm.addChild("br");
        filterForm.addChild("#", l10n("mimeTypeText"));
        filterForm.addChild("br");
        filterForm.addChild("br");

        // file selection
        if (ctx.isAllowedFullAccess()) {
            filterForm.addChild("#", l10n("filterFileBrowseLabel") + ": ");
            filterForm.addChild("input",
                    new String[] { "type", "name", "value" },
                    new String[] { "submit", "filter-local", l10n("filterFileBrowseButton") + "..." });
            filterForm.addChild("br");
        }
        filterForm.addChild("#", l10n("filterFileUploadLabel") + ": ");
        filterForm.addChild("input",
                new String[] { "type", "name", "value" },
                new String[] { "file", "filename", "" });
        filterForm.addChild("#", " \u00a0 ");
        filterForm.addChild("input",
                new String[] { "type", "name", "value" },
                new String[] { "submit", "filter-upload", l10n("filterFileFilterLabel") });
        filterForm.addChild("#", " \u00a0 ");

        return filterBox;
    }

    private void writeBadRequestError(String header, String message, ToadletContext context, boolean returnToFilterPage)
            throws ToadletContextClosedException, IOException {
        PageMaker pageMaker = context.getPageMaker();
        PageNode page = pageMaker.getPageNode(header, context);
        HTMLNode pageNode = page.outer;
        HTMLNode contentNode = page.content;
        if (context.isAllowedFullAccess()) {
            contentNode.addChild(context.getAlertManager().createSummary());
        }
        HTMLNode infoboxContent = pageMaker.getInfobox("infobox-error", header, contentNode, "filter-error", false);
        infoboxContent.addChild("#", message);
        if (returnToFilterPage) {
            NodeL10n.getBase().addL10nSubstitution(infoboxContent.addChild("div"),
                    "ContentFilterToadlet.tryAgainFilterFilePage", new String[] { "link" },
                    new HTMLNode[] { HTMLNode.link(ContentFilterToadlet.PATH) });
        }
        writeHTMLReply(context, 400, "Bad request", pageNode.generate());
    }

    /**
     * Handle a request to filter a file.
     */
    private void handleFilterRequest(HTTPRequest request, ToadletContext ctx, NodeClientCore core, boolean localFile)
            throws ToadletContextClosedException, IOException {
        try {
            FilterOperation filterOperation = getFilterOperation(request);
            ResultHandling resultHandling = getResultHandling(request);
            String mimeType = request.getPartAsStringFailsafe("mime-type", 100);
            String filename;
            Bucket bucket;
            if (localFile) {
            	filename = request.getPartAsStringFailsafe("filename", QueueToadlet.MAX_FILENAME_LENGTH);
	            if (mimeType.isEmpty()) {
	                mimeType = DefaultMIMETypes.guessMIMEType(filename, false);
	            }
	            File file = new File(filename);
	            bucket = new FileBucket(file, true, false, false, false);
            } else {
                HTTPUploadedFile file = request.getUploadedFile("filename");
                if (file == null) {
                    throw new BadRequestException("filename");
                }
                filename = file.getFilename();
                if (mimeType.isEmpty()) {
                    mimeType = file.getContentType();
                }
                bucket = file.getData();
            }
            if (filename.isEmpty()) {
                throw new BadRequestException("filename");
            }
            String resultFilename = makeResultFilename(filename, mimeType);
            try {
                handleFilter(bucket, mimeType, filterOperation, resultHandling, resultFilename, ctx, core);
            } catch (FileNotFoundException e) {
                writeBadRequestError(l10n("errorNoFileOrCannotReadTitle"), l10n("errorNoFileOrCannotRead", "file", filename), ctx, true);
            }
        } catch (BadRequestException e) {
            String invalidPart = e.getInvalidRequestPart();
            if ("filter-operation".equals(invalidPart)) {
                writeBadRequestError(l10n("errorMustSpecifyFilterOperationTitle"), l10n("errorMustSpecifyFilterOperation"), ctx, true);
            } else if ("result-handling".equals(invalidPart)) {
                writeBadRequestError(l10n("errorMustSpecifyResultHandlingTitle"), l10n("errorMustSpecifyResultHandling"), ctx, true);
            } else if ("filename".equals(invalidPart)) {
                writeBadRequestError(l10n("errorNoFileSelectedTitle"), l10n("errorNoFileSelected"), ctx, true);
            } else {
                writeBadRequestError(l10n("errorBadRequestTitle"), l10n("errorBadRequest"), ctx, true);
            }
        }
    }

    private FilterOperation getFilterOperation(HTTPRequest request)
            throws BadRequestException {
        String s = request.getPartAsStringFailsafe("filter-operation", 100);
        try {
            return FilterOperation.valueOf(s);
        } catch (IllegalArgumentException e) {
            throw new BadRequestException("filter-operation", e);
        }
    }

    private ResultHandling getResultHandling(HTTPRequest request)
            throws BadRequestException {
        String s = request.getPartAsStringFailsafe("result-handling", 100);
        try {
            return ResultHandling.valueOf(s);
        } catch (IllegalArgumentException e) {
            throw new BadRequestException("result-handling", e);
        }
    }

    private String makeResultFilename(String originalFilename, String mimeType) {
        String filteredFilename;
        int p = originalFilename.indexOf('.', 1);
        if (p > 0) {
            filteredFilename = originalFilename.substring(0, p) + ".filtered" + originalFilename.substring(p);
        } else {
            filteredFilename = originalFilename + ".filtered";
        }
        filteredFilename = FileUtil.sanitize(filteredFilename, mimeType);
        return filteredFilename;
    }

    private void handleFilter(Bucket data, String mimeType, FilterOperation operation, ResultHandling resultHandling, String resultFilename, ToadletContext ctx, NodeClientCore core)
            throws ToadletContextClosedException, IOException, BadRequestException {
        Bucket resultBucket = ctx.getBucketFactory().makeBucket(-1);
        String resultMimeType = null;
        boolean unsafe = false;
        try {
            FilterStatus status = applyFilter(data, resultBucket, mimeType, operation, core);
            resultMimeType = status.mimeType;
        } catch (UnsafeContentTypeException e) {
            unsafe = true;
        } catch (IOException e) {
            Logger.error(this, "IO error running content filter", e);
            throw e;
        }

        if (unsafe) {
        	sendErrorPage(ctx, 200, l10n("errorUnsafeContentTitle"), l10n("errorUnsafeContent"));
        } else {
            if (resultHandling == ResultHandling.DISPLAY) {
                ctx.sendReplyHeaders(200, "OK", null, resultMimeType, resultBucket.size());
                ctx.writeData(resultBucket);
            } else if (resultHandling == ResultHandling.SAVE) {
                MultiValueTable<String, String> headers = new MultiValueTable<>();
                headers.put("Content-Disposition", "attachment; filename=\"" + resultFilename + '"');
                headers.put("Cache-Control", "private");
                headers.put("Content-Transfer-Encoding", "binary");
                ctx.sendReplyHeaders(200, "OK", headers, "application/force-download", resultBucket.size());
                ctx.writeData(resultBucket);
            } else {
                throw new BadRequestException("result-handling");
            }
        }
    }

    private FilterStatus applyFilter(Bucket input, Bucket output, String mimeType, FilterOperation operation, NodeClientCore core)
            throws UnsafeContentTypeException, IOException {
        InputStream inputStream = null;
        OutputStream outputStream = null;
        try {
            inputStream = input.getInputStream();
            outputStream = output.getOutputStream();
            return applyFilter(inputStream, outputStream, mimeType, operation, core);
        } finally {
            Closer.close(inputStream);
            Closer.close(outputStream);
        }
    }

    private FilterStatus applyFilter(InputStream input, OutputStream output, String mimeType, FilterOperation operation, NodeClientCore core)
            throws UnsafeContentTypeException, IOException {
        URI fakeUri;
        try {
            fakeUri = new URI("http://127.0.0.1:8888/");
        } catch (URISyntaxException e) {
            Logger.error(ContentFilterToadlet.class, "Inexplicable URI error", e);
            return null;
        }
        //TODO: check operation, once ContentFilter supports write filtering
        return ContentFilter.filter(input, output, mimeType, fakeUri, null, null, null, null,
                core.getLinkFilterExceptionProvider());
    }

    static String l10n(String key) {
        return NodeL10n.getBase().getString("ContentFilterToadlet." + key);
    }

    static String l10n(String key, String pattern, String value) {
        return NodeL10n.getBase().getString("ContentFilterToadlet." + key, pattern, value);
    }
}
