package com.srbenoit.filter;

import java.io.UnsupportedEncodingException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.net.URLEncoder;
import java.text.ParseException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.srbenoit.log.LoggedObject;
import com.srbenoit.xml.ElementBase;
import com.srbenoit.xml.EmptyElement;
import com.srbenoit.xml.Node;
import com.srbenoit.xml.NonemptyElement;
import com.srbenoit.xml.XmlParser;

/**
 * A class that can generate a serialized representation of a tree of filters (XML) and can
 * reconstruct the filter tree from a list of filters and a serialized representation.
 */
public class FilterTreeIO extends LoggedObject {

    /** the character encoding to use for URL encode/decode */
    private static final String UTF8 = "UTF-8";

    /** the line end string */
    private static final String CRLF;

    /** a zero-length class array to use when finding constructors */
    private static final Class<?>[] CLASS_0;

    /** a zero-length object array to use when instantiating objects */
    private static final Object[] OBJECT_0;

    static {
        String crlf;

        crlf = System.getProperty("line.separator");
        CRLF = (crlf == null) ? "\n" : crlf;

        CLASS_0 = new Class<?>[0];
        OBJECT_0 = new Object[0];
    }

    /**
     * Constructs a new <code>FilterTreeIO</code>.
     */
    public FilterTreeIO() {

        // No action
    }

    /**
     * Generates the serialized representation of a filter tree.
     *
     * @param   tree  the filter tree
     * @return  the serialized representation
     */
    public String serialize(final FilterTree tree) {

        int count;
        StringBuilder str;

        str = new StringBuilder(256);
        count = tree.getNumFilters();

        str.append("<filter_tree>");
        str.append(CRLF);

        for (int i = 0; i < count; i++) {
            appendFilter(tree.getFilter(i), str);
        }

        str.append("</filter_tree>");
        str.append(CRLF);

        return str.toString();
    }

    /**
     * Recursively appends a filter and all of its descendants to a <code>StringBuilder</code>. The
     * output format is a simple XML string describing the tree structure of filters.
     *
     * <p>Any settings in the filter are stored as attributes in the XML string.
     *
     * @param  filter  the filter to append
     * @param  str     the <code>StringBuilder</code> to which to append
     */
    private void appendFilter(final AbstractFilter filter, final StringBuilder str) {

        String[] keys;
        String tag;
        String key;
        String value;
        int index;

        keys = filter.getPropertyKeys();

        try {
            tag = URLEncoder.encode(filter.getTag(), UTF8);

            str.append(" <");
            str.append(tag);

            for (index = 0; index < filter.getNumInputs(); index++) {
                str.append(" in-");
                str.append(Integer.toString(index));
                str.append("='");
                str.append(filter.getInputFormat(index).getKey());
                str.append('\'');
            }

            for (index = 0; index < filter.getNumOutputs(); index++) {
                str.append(" out-");
                str.append(Integer.toString(index));
                str.append("='");
                str.append(filter.getOutputFormat(index).getKey());
                str.append('\'');
            }

            for (index = 0; index < keys.length; index++) {

                try {
                    key = URLEncoder.encode(keys[index], UTF8);
                    value = URLEncoder.encode(filter.getProperty(keys[index]), UTF8);

                    // Only after successfully encoding both do we append
                    str.append(' ');
                    str.append(key);
                    str.append('=');
                    str.append('\'');
                    str.append(value);
                    str.append('\'');
                } catch (UnsupportedEncodingException e) {

                    // Any encoding error, we append nothing and warn (keep the
                    // output valid XML in this case)
                    LOG.warning("URLEncoder indicated 'UTF-8' is not supported");
                }
            }

            str.append("/>");
            str.append(CRLF);
        } catch (UnsupportedEncodingException e) {

            // If we can't encode the tag, do not append anything, but warn
            LOG.warning("URLEncoder indicated 'UTF-8' is not supported");
        }
    }

    /**
     * Given an XML representation of a filter tree, and a list of filter classes from which to
     * construct the tree, parses an XML stream and rebuilds the filter tree if possible.
     *
     * @param   xml      the XML representation
     * @param   classes  the set of filter classes from which to draw when rebuilding the tree
     * @return  the reconstructed tree
     * @throws  NoSuchMethodException      if any of the filter classes does not have a no-argument
     *                                     constructor
     * @throws  SecurityException          if this class does not have permission to access the
     *                                     no-argument constructor on any of the filters
     * @throws  IllegalAccessException     if the no-argument constructor on any of the filters is
     *                                     not accessible
     * @throws  InstantiationException     if any of the given filter classes are abstract classes
     * @throws  InvocationTargetException  if the no-argument constructor on any filter throws an
     *                                     exception
     * @throws  ParseException             if the XML could not be parsed
     */
    public FilterTree deserialize(final String xml,
        final List<Class<? extends AbstractFilter>> classes) throws SecurityException,
        NoSuchMethodException, IllegalAccessException, InstantiationException,
        InvocationTargetException, ParseException {

        Map<String, Constructor<? extends AbstractFilter>> constructors;
        Constructor<? extends AbstractFilter> constr;
        AbstractFilter instance;
        List<Node> nodes;
        NonemptyElement toplevel;
        FilterTree tree;

        // Identify the no-argument constructors on all the filters and build
        // up a map from filter tag to its constructor
        constructors = new HashMap<String, Constructor<? extends AbstractFilter>>(10);

        for (Class<? extends AbstractFilter> clazz : classes) {
            constr = clazz.getConstructor(CLASS_0);
            instance = constr.newInstance(OBJECT_0);
            constructors.put(instance.getTag(), constr);
        }

        // Parse the XML
        nodes = new XmlParser().parse(xml, true);

        // Identify the one and only top-level node
        toplevel = null;
        tree = new FilterTree();

        if (nodes.isEmpty()) {
            throw new ParseException("Input XML contained no elements", 0);
        }

        for (Node node : nodes) {

            if (node instanceof NonemptyElement) {

                if (toplevel != null) {
                    throw new ParseException("Must have only one top-level node", 0);
                }

                toplevel = (NonemptyElement) node;

                if ("filter_tree".equals(toplevel.tagName)) {

                    for (Node child : toplevel.children) {

                        if (child instanceof ElementBase) {
                            buildFilter((ElementBase) child, constructors, tree);
                        }
                    }
                } else {
                    throw new ParseException("Must have one top-level 'filter-tree' node", 0);
                }
            } else {
                throw new ParseException("Must have one top-level non-empty filter tree node", 0);
            }
        }

        // Build the filter tree
        return tree;
    }

    /**
     * Recursively builds a filter from an element. If the element is nonempty, the filter will
     * have child filters for each element child of the supplied element.
     *
     * @param   element       the element with the filter definition
     * @param   constructors  a map from filter name to no-argument constructor
     * @param   tree          the filter tree to which to add the constructed filter
     * @return  the constructed filter
     * @throws  IllegalAccessException     if the no-argument constructor on any of the filters is
     *                                     not accessible
     * @throws  InstantiationException     if any of the given filter classes are abstract classes
     * @throws  InvocationTargetException  if the no-argument constructor on any filter throws an
     *                                     exception
     * @throws  ParseException             if the element's tag name does not match any configured
     *                                     filter names
     */
    private void buildFilter(final ElementBase element,
        final Map<String, Constructor<? extends AbstractFilter>> constructors,
        final FilterTree tree) throws IllegalAccessException, InstantiationException,
        InvocationTargetException, ParseException {

        EmptyElement empty;
        Constructor<? extends AbstractFilter> constr;
        AbstractFilter filter;
        int index;
        String attrib;
        String value;
        String[] supported;

        constr = constructors.get(element.tagName);

        if (element instanceof EmptyElement) {
            empty = (EmptyElement) element;

            if (constr == null) {
                throw new ParseException("Unrecognized filter name: '" + element.tagName + "'",
                    empty.tagSpan.nameStart);
            }

            filter = constr.newInstance(OBJECT_0);

            for (index = 0; index < filter.getNumInputs(); index++) {
                attrib = "in-" + Integer.toString(index);
                value = empty.get(attrib);
                filter.getInputFormat(index).setKey(value);
            }

            for (index = 0; index < filter.getNumOutputs(); index++) {
                attrib = "out-" + Integer.toString(index);
                value = empty.get(attrib);
                filter.getOutputFormat(index).setKey(value);
            }

            supported = filter.getSupportedPropertyKeys();

            for (index = 0; index < supported.length; index++) {
                value = empty.get(supported[index]);

                if (value != null) {
                    filter.setProperty(supported[index], value);
                }
            }

            tree.addFilter(filter);
        }
    }
}
