/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.media.filterfw;

import android.text.TextUtils;

import java.io.InputStream;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;

/**
 * A GraphReader allows obtaining filter graphs from XML graph files or strings.
 */
public class GraphReader {

    private static interface Command {
        public void execute(CommandStack stack);
    }

    private static class CommandStack {
        private ArrayList<Command> mCommands = new ArrayList<Command>();
        private FilterGraph.Builder mBuilder;
        private FilterFactory mFactory;
        private MffContext mContext;

        public CommandStack(MffContext context) {
            mContext = context;
            mBuilder = new FilterGraph.Builder(mContext);
            mFactory = new FilterFactory();
        }

        public void execute() {
            for (Command command : mCommands) {
                command.execute(this);
            }
        }

        public void append(Command command) {
            mCommands.add(command);
        }

        public FilterFactory getFactory() {
            return mFactory;
        }

        public MffContext getContext() {
            return mContext;
        }

        protected FilterGraph.Builder getBuilder() {
            return mBuilder;
        }
    }

    private static class ImportPackageCommand implements Command {
        private String mPackageName;

        public ImportPackageCommand(String packageName) {
            mPackageName = packageName;
        }

        @Override
        public void execute(CommandStack stack) {
            try {
                stack.getFactory().addPackage(mPackageName);
            } catch (IllegalArgumentException e) {
                throw new RuntimeException(e.getMessage());
            }
        }
    }

    private static class AddLibraryCommand implements Command {
        private String mLibraryName;

        public AddLibraryCommand(String libraryName) {
            mLibraryName = libraryName;
        }

        @Override
        public void execute(CommandStack stack) {
            FilterFactory.addFilterLibrary(mLibraryName);
        }
    }

    private static class AllocateFilterCommand implements Command {
        private String mClassName;
        private String mFilterName;

        public AllocateFilterCommand(String className, String filterName) {
            mClassName = className;
            mFilterName = filterName;
        }

        @Override
        public void execute(CommandStack stack) {
            Filter filter = null;
            try {
                filter = stack.getFactory().createFilterByClassName(mClassName,
                                                                    mFilterName,
                                                                    stack.getContext());
            } catch (IllegalArgumentException e) {
                throw new RuntimeException("Error creating filter " + mFilterName + "!", e);
            }
            stack.getBuilder().addFilter(filter);
        }
    }

    private static class AddSourceSlotCommand implements Command {
        private String mName;
        private String mSlotName;

        public AddSourceSlotCommand(String name, String slotName) {
            mName = name;
            mSlotName = slotName;
        }

        @Override
        public void execute(CommandStack stack) {
            stack.getBuilder().addFrameSlotSource(mName, mSlotName);
        }
    }

    private static class AddTargetSlotCommand implements Command {
        private String mName;
        private String mSlotName;

        public AddTargetSlotCommand(String name, String slotName) {
            mName = name;
            mSlotName = slotName;
        }

        @Override
        public void execute(CommandStack stack) {
            stack.getBuilder().addFrameSlotTarget(mName, mSlotName);
        }
    }

    private static class AddVariableCommand implements Command {
        private String mName;
        private Object mValue;

        public AddVariableCommand(String name, Object value) {
            mName = name;
            mValue = value;
        }

        @Override
        public void execute(CommandStack stack) {
            stack.getBuilder().addVariable(mName, mValue);
        }
    }

    private static class SetFilterInputCommand implements Command {
        private String mFilterName;
        private String mFilterInput;
        private Object mValue;

        public SetFilterInputCommand(String filterName, String input, Object value) {
            mFilterName = filterName;
            mFilterInput = input;
            mValue = value;
        }

        @Override
        public void execute(CommandStack stack) {
            if (mValue instanceof Variable) {
                String varName = ((Variable)mValue).name;
                stack.getBuilder().assignVariableToFilterInput(varName, mFilterName, mFilterInput);
            } else {
                stack.getBuilder().assignValueToFilterInput(mValue, mFilterName, mFilterInput);
            }
        }
    }

    private static class ConnectCommand implements Command {
        private String mSourceFilter;
        private String mSourcePort;
        private String mTargetFilter;
        private String mTargetPort;

        public ConnectCommand(String sourceFilter,
                              String sourcePort,
                              String targetFilter,
                              String targetPort) {
            mSourceFilter = sourceFilter;
            mSourcePort = sourcePort;
            mTargetFilter = targetFilter;
            mTargetPort = targetPort;
        }

        @Override
        public void execute(CommandStack stack) {
            stack.getBuilder().connect(mSourceFilter, mSourcePort, mTargetFilter, mTargetPort);
        }
    }

    private static class Variable {
        public String name;

        public Variable(String name) {
            this.name = name;
        }
    }

    private static class XmlGraphReader {

        private SAXParserFactory mParserFactory;

        private static class GraphDataHandler extends DefaultHandler {

            private CommandStack mCommandStack;
            private boolean mInGraph = false;
            private String mCurFilterName = null;

            public GraphDataHandler(CommandStack commandStack) {
                mCommandStack = commandStack;
            }

            @Override
            public void startElement(String uri, String localName, String qName, Attributes attr)
                    throws SAXException {
                if (localName.equals("graph")) {
                    beginGraph();
                } else {
                    assertInGraph(localName);
                    if (localName.equals("import")) {
                        addImportCommand(attr);
                    } else if (localName.equals("library")) {
                        addLibraryCommand(attr);
                    } else if (localName.equals("connect")) {
                        addConnectCommand(attr);
                    } else if (localName.equals("var")) {
                        addVarCommand(attr);
                    } else if (localName.equals("filter")) {
                        beginFilter(attr);
                    } else if (localName.equals("input")) {
                        addFilterInput(attr);
                    } else {
                        throw new SAXException("Unknown XML element '" + localName + "'!");
                    }
                }
            }

            @Override
            public void endElement (String uri, String localName, String qName) {
                if (localName.equals("graph")) {
                    endGraph();
                } else if (localName.equals("filter")) {
                    endFilter();
                }
            }

            private void addImportCommand(Attributes attributes) throws SAXException {
                String packageName = getRequiredAttribute(attributes, "package");
                mCommandStack.append(new ImportPackageCommand(packageName));
            }

            private void addLibraryCommand(Attributes attributes) throws SAXException {
                String libraryName = getRequiredAttribute(attributes, "name");
                mCommandStack.append(new AddLibraryCommand(libraryName));
            }

            private void addConnectCommand(Attributes attributes) {
                String sourcePortName   = null;
                String sourceFilterName = null;
                String targetPortName   = null;
                String targetFilterName = null;

                // check for shorthand: <connect source="filter:port" target="filter:port"/>
                String sourceTag = attributes.getValue("source");
                if (sourceTag != null) {
                    String[] sourceParts = sourceTag.split(":");
                    if (sourceParts.length == 2) {
                        sourceFilterName = sourceParts[0];
                        sourcePortName   = sourceParts[1];
                    } else {
                        throw new RuntimeException(
                            "'source' tag needs to have format \"filter:port\"! " +
                            "Alternatively, you may use the form " +
                            "'sourceFilter=\"filter\" sourcePort=\"port\"'.");
                    }
                } else {
                    sourceFilterName = attributes.getValue("sourceFilter");
                    sourcePortName   = attributes.getValue("sourcePort");
                }

                String targetTag = attributes.getValue("target");
                if (targetTag != null) {
                    String[] targetParts = targetTag.split(":");
                    if (targetParts.length == 2) {
                        targetFilterName = targetParts[0];
                        targetPortName   = targetParts[1];
                    } else {
                        throw new RuntimeException(
                            "'target' tag needs to have format \"filter:port\"! " +
                            "Alternatively, you may use the form " +
                            "'targetFilter=\"filter\" targetPort=\"port\"'.");
                    }
                } else {
                    targetFilterName = attributes.getValue("targetFilter");
                    targetPortName   = attributes.getValue("targetPort");
                }

                String sourceSlotName = attributes.getValue("sourceSlot");
                String targetSlotName = attributes.getValue("targetSlot");
                if (sourceSlotName != null) {
                    sourceFilterName = "sourceSlot_" + sourceSlotName;
                    mCommandStack.append(new AddSourceSlotCommand(sourceFilterName,
                                                                  sourceSlotName));
                    sourcePortName = "frame";
                }
                if (targetSlotName != null) {
                    targetFilterName = "targetSlot_" + targetSlotName;
                    mCommandStack.append(new AddTargetSlotCommand(targetFilterName,
                                                                  targetSlotName));
                    targetPortName = "frame";
                }
                assertValueNotNull("sourceFilter", sourceFilterName);
                assertValueNotNull("sourcePort", sourcePortName);
                assertValueNotNull("targetFilter", targetFilterName);
                assertValueNotNull("targetPort", targetPortName);
                // TODO: Should slot connections auto-branch?
                mCommandStack.append(new ConnectCommand(sourceFilterName,
                                                        sourcePortName,
                                                        targetFilterName,
                                                        targetPortName));
            }

            private void addVarCommand(Attributes attributes) throws SAXException {
                String varName = getRequiredAttribute(attributes, "name");
                Object varValue = getAssignmentValue(attributes);
                mCommandStack.append(new AddVariableCommand(varName, varValue));
            }

            private void beginGraph() throws SAXException {
                if (mInGraph) {
                    throw new SAXException("Found more than one graph element in XML!");
                }
                mInGraph = true;
            }

            private void endGraph() {
                mInGraph = false;
            }

            private void beginFilter(Attributes attributes) throws SAXException {
                String className = getRequiredAttribute(attributes, "class");
                mCurFilterName = getRequiredAttribute(attributes, "name");
                mCommandStack.append(new AllocateFilterCommand(className, mCurFilterName));
            }

            private void endFilter() {
                mCurFilterName = null;
            }

            private void addFilterInput(Attributes attributes) throws SAXException {
                // Make sure we are in a filter element
                if (mCurFilterName == null) {
                    throw new SAXException("Found 'input' element outside of 'filter' "
                        + "element!");
                }

                // Get input name and value
                String inputName = getRequiredAttribute(attributes, "name");
                Object inputValue = getAssignmentValue(attributes);
                if (inputValue == null) {
                    throw new SAXException("No value specified for input '" + inputName + "' "
                        + "of filter '" + mCurFilterName + "'!");
                }

                // Push commmand
                mCommandStack.append(new SetFilterInputCommand(mCurFilterName,
                                                               inputName,
                                                               inputValue));
            }

            private void assertInGraph(String localName) throws SAXException {
                if (!mInGraph) {
                    throw new SAXException("Encountered '" + localName + "' element outside of "
                        + "'graph' element!");
                }
            }

            private static Object getAssignmentValue(Attributes attributes) {
                String strValue = null;
                if ((strValue = attributes.getValue("stringValue")) != null) {
                    return strValue;
                } else if ((strValue = attributes.getValue("booleanValue")) != null) {
                    return Boolean.parseBoolean(strValue);
                } else if ((strValue = attributes.getValue("intValue")) != null) {
                    return Integer.parseInt(strValue);
                } else if ((strValue = attributes.getValue("floatValue")) != null) {
                    return Float.parseFloat(strValue);
                } else if ((strValue = attributes.getValue("floatsValue")) != null) {
                    String[] floatStrings = TextUtils.split(strValue, ",");
                    float[] result = new float[floatStrings.length];
                    for (int i = 0; i < floatStrings.length; ++i) {
                        result[i] = Float.parseFloat(floatStrings[i]);
                    }
                    return result;
                } else if ((strValue = attributes.getValue("varValue")) != null) {
                    return new Variable(strValue);
                } else {
                    return null;
                }
            }

            private static String getRequiredAttribute(Attributes attributes, String name)
                    throws SAXException {
                String result = attributes.getValue(name);
                if (result == null) {
                    throw new SAXException("Required attribute '" + name + "' not found!");
                }
                return result;
            }

            private static void assertValueNotNull(String valueName, Object value) {
                if (value == null) {
                    throw new NullPointerException("Required value '" + value + "' not specified!");
                }
            }

        }

        public XmlGraphReader() {
            mParserFactory = SAXParserFactory.newInstance();
        }

        public void parseString(String graphString, CommandStack commandStack) throws IOException {
            try {
                XMLReader reader = getReaderForCommandStack(commandStack);
                reader.parse(new InputSource(new StringReader(graphString)));
            } catch (SAXException e) {
                throw new IOException("XML parse error during graph parsing!", e);
            }
        }

        public void parseInput(InputStream inputStream, CommandStack commandStack)
                throws IOException {
            try {
                XMLReader reader = getReaderForCommandStack(commandStack);
                reader.parse(new InputSource(inputStream));
            } catch (SAXException e) {
                throw new IOException("XML parse error during graph parsing!", e);
            }
        }

        private XMLReader getReaderForCommandStack(CommandStack commandStack) throws IOException {
            try {
                SAXParser parser = mParserFactory.newSAXParser();
                XMLReader reader = parser.getXMLReader();
                GraphDataHandler graphHandler = new GraphDataHandler(commandStack);
                reader.setContentHandler(graphHandler);
                return reader;
            } catch (ParserConfigurationException e) {
                throw new IOException("Error creating SAXParser for graph parsing!", e);
            } catch (SAXException e) {
                throw new IOException("Error creating XMLReader for graph parsing!", e);
            }
        }
    }

    /**
     * Read an XML graph from a String.
     *
     * This function automatically checks each filters' signatures and throws a Runtime Exception
     * if required ports are unconnected. Use the 3-parameter version to avoid this behavior.
     *
     * @param context the MffContext into which to load the graph.
     * @param xmlSource the graph specified in XML.
     * @return the FilterGraph instance for the XML source.
     * @throws IOException if there was an error parsing the source.
     */
    public static FilterGraph readXmlGraph(MffContext context, String xmlSource)
            throws IOException {
        FilterGraph.Builder builder = getBuilderForXmlString(context, xmlSource);
        return builder.build();
    }

    /**
     * Read an XML sub-graph from a String.
     *
     * @param context the MffContext into which to load the graph.
     * @param xmlSource the graph specified in XML.
     * @param parentGraph the parent graph.
     * @return the FilterGraph instance for the XML source.
     * @throws IOException if there was an error parsing the source.
     */
    public static FilterGraph readXmlSubGraph(
            MffContext context, String xmlSource, FilterGraph parentGraph)
            throws IOException {
        FilterGraph.Builder builder = getBuilderForXmlString(context, xmlSource);
        return builder.buildSubGraph(parentGraph);
    }

    /**
     * Read an XML graph from a resource.
     *
     * This function automatically checks each filters' signatures and throws a Runtime Exception
     * if required ports are unconnected. Use the 3-parameter version to avoid this behavior.
     *
     * @param context the MffContext into which to load the graph.
     * @param resourceId the XML resource ID.
     * @return the FilterGraph instance for the XML source.
     * @throws IOException if there was an error reading or parsing the resource.
     */
    public static FilterGraph readXmlGraphResource(MffContext context, int resourceId)
            throws IOException {
        FilterGraph.Builder builder = getBuilderForXmlResource(context, resourceId);
        return builder.build();
    }

    /**
     * Read an XML graph from a resource.
     *
     * This function automatically checks each filters' signatures and throws a Runtime Exception
     * if required ports are unconnected. Use the 3-parameter version to avoid this behavior.
     *
     * @param context the MffContext into which to load the graph.
     * @param resourceId the XML resource ID.
     * @return the FilterGraph instance for the XML source.
     * @throws IOException if there was an error reading or parsing the resource.
     */
    public static FilterGraph readXmlSubGraphResource(
            MffContext context, int resourceId, FilterGraph parentGraph)
            throws IOException {
        FilterGraph.Builder builder = getBuilderForXmlResource(context, resourceId);
        return builder.buildSubGraph(parentGraph);
    }

    private static FilterGraph.Builder getBuilderForXmlString(MffContext context, String source)
            throws IOException {
        XmlGraphReader reader = new XmlGraphReader();
        CommandStack commands = new CommandStack(context);
        reader.parseString(source, commands);
        commands.execute();
        return commands.getBuilder();
    }

    private static FilterGraph.Builder getBuilderForXmlResource(MffContext context, int resourceId)
            throws IOException {
        InputStream inputStream = context.getApplicationContext().getResources()
                .openRawResource(resourceId);
        XmlGraphReader reader = new XmlGraphReader();
        CommandStack commands = new CommandStack(context);
        reader.parseInput(inputStream, commands);
        commands.execute();
        return commands.getBuilder();
    }
}

