/*
 * 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 android.filterfw.io;

import java.lang.Float;
import java.lang.Integer;
import java.lang.String;

import java.util.ArrayList;
import java.util.regex.Pattern;

import android.filterfw.core.Filter;
import android.filterfw.core.FilterFactory;
import android.filterfw.core.FilterGraph;
import android.filterfw.core.KeyValueMap;
import android.filterfw.core.ProtocolException;
import android.filterfw.io.GraphReader;
import android.filterfw.io.GraphIOException;
import android.filterfw.io.PatternScanner;

public class TextGraphReader extends GraphReader {

    private ArrayList<Command> mCommands = new ArrayList<Command>();
    private Filter mCurrentFilter;
    private FilterGraph mCurrentGraph;
    private KeyValueMap mBoundReferences;
    private KeyValueMap mSettings;
    private FilterFactory mFactory;

    private interface Command {
        public void execute(TextGraphReader reader) throws GraphIOException;
    }

    private class ImportPackageCommand implements Command {
        private String mPackageName;

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

        @Override
        public void execute(TextGraphReader reader) throws GraphIOException {
            try {
                reader.mFactory.addPackage(mPackageName);
            } catch (IllegalArgumentException e) {
                throw new GraphIOException(e.getMessage());
            }
        }
    }

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

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

        public void execute(TextGraphReader reader) throws GraphIOException {
            // Create the filter
            Filter filter = null;
            try {
                filter = reader.mFactory.createFilterByClassName(mClassName, mFilterName);
            } catch (IllegalArgumentException e) {
                throw new GraphIOException(e.getMessage());
            }

            // Set it as the current filter
            reader.mCurrentFilter = filter;
        }
    }

    private class InitFilterCommand implements Command {
        private KeyValueMap mParams;

        public InitFilterCommand(KeyValueMap params) {
            mParams = params;
        }

        @Override
        public void execute(TextGraphReader reader) throws GraphIOException {
            Filter filter = reader.mCurrentFilter;
            try {
                filter.initWithValueMap(mParams);
            } catch (ProtocolException e) {
                throw new GraphIOException(e.getMessage());
            }
            reader.mCurrentGraph.addFilter(mCurrentFilter);
        }
    }

    private class ConnectCommand implements Command {
        private String mSourceFilter;
        private String mSourcePort;
        private String mTargetFilter;
        private String mTargetName;

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

        @Override
        public void execute(TextGraphReader reader) {
            reader.mCurrentGraph.connect(mSourceFilter, mSourcePort, mTargetFilter, mTargetName);
        }
    }

    @Override
    public FilterGraph readGraphString(String graphString) throws GraphIOException {
        FilterGraph result = new FilterGraph();

        reset();
        mCurrentGraph = result;
        parseString(graphString);
        applySettings();
        executeCommands();
        reset();

        return result;
    }

    private void reset() {
        mCurrentGraph = null;
        mCurrentFilter = null;
        mCommands.clear();
        mBoundReferences = new KeyValueMap();
        mSettings = new KeyValueMap();
        mFactory = new FilterFactory();
    }

    private void parseString(String graphString) throws GraphIOException {
        final Pattern commandPattern = Pattern.compile("@[a-zA-Z]+");
        final Pattern curlyClosePattern = Pattern.compile("\\}");
        final Pattern curlyOpenPattern = Pattern.compile("\\{");
        final Pattern ignorePattern = Pattern.compile("(\\s+|//[^\\n]*\\n)+");
        final Pattern packageNamePattern = Pattern.compile("[a-zA-Z\\.]+");
        final Pattern portPattern = Pattern.compile("\\[[a-zA-Z0-9\\-_]+\\]");
        final Pattern rightArrowPattern = Pattern.compile("=>");
        final Pattern semicolonPattern = Pattern.compile(";");
        final Pattern wordPattern = Pattern.compile("[a-zA-Z0-9\\-_]+");

        final int STATE_COMMAND           = 0;
        final int STATE_IMPORT_PKG        = 1;
        final int STATE_FILTER_CLASS      = 2;
        final int STATE_FILTER_NAME       = 3;
        final int STATE_CURLY_OPEN        = 4;
        final int STATE_PARAMETERS        = 5;
        final int STATE_CURLY_CLOSE       = 6;
        final int STATE_SOURCE_FILTERNAME = 7;
        final int STATE_SOURCE_PORT       = 8;
        final int STATE_RIGHT_ARROW       = 9;
        final int STATE_TARGET_FILTERNAME = 10;
        final int STATE_TARGET_PORT       = 11;
        final int STATE_ASSIGNMENT        = 12;
        final int STATE_EXTERNAL          = 13;
        final int STATE_SETTING           = 14;
        final int STATE_SEMICOLON         = 15;

        int state = STATE_COMMAND;
        PatternScanner scanner = new PatternScanner(graphString, ignorePattern);

        String curClassName = null;
        String curSourceFilterName = null;
        String curSourcePortName = null;
        String curTargetFilterName = null;
        String curTargetPortName = null;

        // State machine main loop
        while (!scanner.atEnd()) {
            switch (state) {
                case STATE_COMMAND: {
                    String curCommand = scanner.eat(commandPattern, "<command>");
                    if (curCommand.equals("@import")) {
                        state = STATE_IMPORT_PKG;
                    } else if (curCommand.equals("@filter")) {
                        state = STATE_FILTER_CLASS;
                    } else if (curCommand.equals("@connect")) {
                        state = STATE_SOURCE_FILTERNAME;
                    } else if (curCommand.equals("@set")) {
                        state = STATE_ASSIGNMENT;
                    } else if (curCommand.equals("@external")) {
                        state = STATE_EXTERNAL;
                    } else if (curCommand.equals("@setting")) {
                        state = STATE_SETTING;
                    } else {
                        throw new GraphIOException("Unknown command '" + curCommand + "'!");
                    }
                    break;
                }

                case STATE_IMPORT_PKG: {
                    String packageName = scanner.eat(packageNamePattern, "<package-name>");
                    mCommands.add(new ImportPackageCommand(packageName));
                    state = STATE_SEMICOLON;
                    break;
                }

                case STATE_FILTER_CLASS:
                    curClassName = scanner.eat(wordPattern, "<class-name>");
                    state = STATE_FILTER_NAME;
                    break;

                case STATE_FILTER_NAME: {
                    String curFilterName = scanner.eat(wordPattern, "<filter-name>");
                    mCommands.add(new AllocateFilterCommand(curClassName, curFilterName));
                    state = STATE_CURLY_OPEN;
                    break;
                }

                case STATE_CURLY_OPEN:
                    scanner.eat(curlyOpenPattern, "{");
                    state = STATE_PARAMETERS;
                    break;

                case STATE_PARAMETERS: {
                    KeyValueMap params = readKeyValueAssignments(scanner, curlyClosePattern);
                    mCommands.add(new InitFilterCommand(params));
                    state = STATE_CURLY_CLOSE;
                    break;
                }

                case STATE_CURLY_CLOSE:
                    scanner.eat(curlyClosePattern, "}");
                    state = STATE_COMMAND;
                    break;

                case STATE_SOURCE_FILTERNAME:
                    curSourceFilterName = scanner.eat(wordPattern, "<source-filter-name>");
                    state = STATE_SOURCE_PORT;
                    break;

                case STATE_SOURCE_PORT: {
                    String portString = scanner.eat(portPattern, "[<source-port-name>]");
                    curSourcePortName = portString.substring(1, portString.length() - 1);
                    state = STATE_RIGHT_ARROW;
                    break;
                }

                case STATE_RIGHT_ARROW:
                    scanner.eat(rightArrowPattern, "=>");
                    state = STATE_TARGET_FILTERNAME;
                    break;

                case STATE_TARGET_FILTERNAME:
                    curTargetFilterName = scanner.eat(wordPattern, "<target-filter-name>");
                    state = STATE_TARGET_PORT;
                    break;

                case STATE_TARGET_PORT: {
                    String portString = scanner.eat(portPattern, "[<target-port-name>]");
                    curTargetPortName = portString.substring(1, portString.length() - 1);
                    mCommands.add(new ConnectCommand(curSourceFilterName,
                                                     curSourcePortName,
                                                     curTargetFilterName,
                                                     curTargetPortName));
                    state = STATE_SEMICOLON;
                    break;
                }

                case STATE_ASSIGNMENT: {
                    KeyValueMap assignment = readKeyValueAssignments(scanner, semicolonPattern);
                    mBoundReferences.putAll(assignment);
                    state = STATE_SEMICOLON;
                    break;
                }

                case STATE_EXTERNAL: {
                    String externalName = scanner.eat(wordPattern, "<external-identifier>");
                    bindExternal(externalName);
                    state = STATE_SEMICOLON;
                    break;
                }

                case STATE_SETTING: {
                    KeyValueMap setting = readKeyValueAssignments(scanner, semicolonPattern);
                    mSettings.putAll(setting);
                    state = STATE_SEMICOLON;
                    break;
                }

                case STATE_SEMICOLON:
                    scanner.eat(semicolonPattern, ";");
                    state = STATE_COMMAND;
                    break;
            }
        }

        // Make sure end of input was expected
        if (state != STATE_SEMICOLON && state != STATE_COMMAND) {
            throw new GraphIOException("Unexpected end of input!");
        }
    }

    @Override
    public KeyValueMap readKeyValueAssignments(String assignments) throws GraphIOException {
        final Pattern ignorePattern = Pattern.compile("\\s+");
        PatternScanner scanner = new PatternScanner(assignments, ignorePattern);
        return readKeyValueAssignments(scanner, null);
    }

    private KeyValueMap readKeyValueAssignments(PatternScanner scanner,
                                                Pattern endPattern) throws GraphIOException {
        // Our parser is a state-machine, and these are our states
        final int STATE_IDENTIFIER = 0;
        final int STATE_EQUALS     = 1;
        final int STATE_VALUE      = 2;
        final int STATE_POST_VALUE = 3;

        final Pattern equalsPattern = Pattern.compile("=");
        final Pattern semicolonPattern = Pattern.compile(";");
        final Pattern wordPattern = Pattern.compile("[a-zA-Z]+[a-zA-Z0-9]*");
        final Pattern stringPattern = Pattern.compile("'[^']*'|\\\"[^\\\"]*\\\"");
        final Pattern intPattern = Pattern.compile("[0-9]+");
        final Pattern floatPattern = Pattern.compile("[0-9]*\\.[0-9]+f?");
        final Pattern referencePattern = Pattern.compile("\\$[a-zA-Z]+[a-zA-Z0-9]");
        final Pattern booleanPattern = Pattern.compile("true|false");

        int state = STATE_IDENTIFIER;
        KeyValueMap newVals = new KeyValueMap();
        String curKey = null;
        String curValue = null;

        while (!scanner.atEnd() && !(endPattern != null && scanner.peek(endPattern))) {
            switch (state) {
                case STATE_IDENTIFIER:
                    curKey = scanner.eat(wordPattern, "<identifier>");
                    state = STATE_EQUALS;
                    break;

                case STATE_EQUALS:
                    scanner.eat(equalsPattern, "=");
                    state = STATE_VALUE;
                    break;

                case STATE_VALUE:
                    if ((curValue = scanner.tryEat(stringPattern)) != null) {
                        newVals.put(curKey, curValue.substring(1, curValue.length() - 1));
                    } else if ((curValue = scanner.tryEat(referencePattern)) != null) {
                        String refName = curValue.substring(1, curValue.length());
                        Object referencedObject = mBoundReferences != null
                            ? mBoundReferences.get(refName)
                            : null;
                        if (referencedObject == null) {
                            throw new GraphIOException(
                                "Unknown object reference to '" + refName + "'!");
                        }
                        newVals.put(curKey, referencedObject);
                    } else if ((curValue = scanner.tryEat(booleanPattern)) != null) {
                        newVals.put(curKey, Boolean.parseBoolean(curValue));
                    } else if ((curValue = scanner.tryEat(floatPattern)) != null) {
                        newVals.put(curKey, Float.parseFloat(curValue));
                    } else if ((curValue = scanner.tryEat(intPattern)) != null) {
                        newVals.put(curKey, Integer.parseInt(curValue));
                    } else {
                        throw new GraphIOException(scanner.unexpectedTokenMessage("<value>"));
                    }
                    state = STATE_POST_VALUE;
                    break;

                case STATE_POST_VALUE:
                    scanner.eat(semicolonPattern, ";");
                    state = STATE_IDENTIFIER;
                    break;
            }
        }

        // Make sure end is expected
        if (state != STATE_IDENTIFIER && state != STATE_POST_VALUE) {
            throw new GraphIOException(
                "Unexpected end of assignments on line " + scanner.lineNo() + "!");
        }

        return newVals;
    }

    private void bindExternal(String name) throws GraphIOException {
        if (mReferences.containsKey(name)) {
            Object value = mReferences.get(name);
            mBoundReferences.put(name, value);
        } else {
            throw new GraphIOException("Unknown external variable '" + name + "'! "
                + "You must add a reference to this external in the host program using "
                + "addReference(...)!");
        }
    }

    /**
     * Unused for now: Often you may want to declare references that are NOT in a certain graph,
     * e.g. when reading multiple graphs with the same reader. We could print a warning, but even
     * that may be too much.
     **/
    private void checkReferences() throws GraphIOException {
        for (String reference : mReferences.keySet()) {
            if (!mBoundReferences.containsKey(reference)) {
                throw new GraphIOException(
                    "Host program specifies reference to '" + reference + "', which is not "
                    + "declared @external in graph file!");
            }
        }
    }

    private void applySettings() throws GraphIOException {
        for (String setting : mSettings.keySet()) {
            Object value = mSettings.get(setting);
            if (setting.equals("autoBranch")) {
                expectSettingClass(setting, value, String.class);
                if (value.equals("synced")) {
                    mCurrentGraph.setAutoBranchMode(FilterGraph.AUTOBRANCH_SYNCED);
                } else if (value.equals("unsynced")) {
                    mCurrentGraph.setAutoBranchMode(FilterGraph.AUTOBRANCH_UNSYNCED);
                } else if (value.equals("off")) {
                    mCurrentGraph.setAutoBranchMode(FilterGraph.AUTOBRANCH_OFF);
                } else {
                    throw new GraphIOException("Unknown autobranch setting: " + value + "!");
                }
            } else if (setting.equals("discardUnconnectedOutputs")) {
                expectSettingClass(setting, value, Boolean.class);
                mCurrentGraph.setDiscardUnconnectedOutputs((Boolean)value);
            } else {
                throw new GraphIOException("Unknown @setting '" + setting + "'!");
            }
        }
    }

    private void expectSettingClass(String setting,
                                    Object value,
                                    Class expectedClass) throws GraphIOException {
        if (value.getClass() != expectedClass) {
            throw new GraphIOException("Setting '" + setting + "' must have a value of type "
                + expectedClass.getSimpleName() + ", but found a value of type "
                + value.getClass().getSimpleName() + "!");
        }
    }

    private void executeCommands() throws GraphIOException {
        for (Command command : mCommands) {
            command.execute(this);
        }
    }
}
