package ee.ioc.phon.android.speechutils.editor;

import android.content.ContentResolver;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Pair;

import androidx.annotation.NonNull;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

/**
 * utterance = "go to position 1"
 * pattern = ("go to position (\d+)", "moveAbs", "$1")
 * command = moveAbs($1)
 * <p>
 * TODO: do not use Java split
 */
public class UtteranceRewriter {

    public static final String HEADER_LABEL = "Label";
    public static final String HEADER_COMMENT = "Comment";
    public static final String HEADER_LOCALE = "Locale";
    public static final String HEADER_SERVICE = "Service";
    public static final String HEADER_APP = "App";
    public static final String HEADER_UTTERANCE = "Utterance";
    public static final String HEADER_REPLACEMENT = "Replacement";
    public static final String HEADER_COMMAND = "Command";
    public static final String HEADER_ARG1 = "Arg1";
    public static final String HEADER_ARG2 = "Arg2";

    private static final Set<String> COLUMNS;

    // Support Windows and Mac line endings and consume empty lines.
    private static final String RE_LINE_SEPARATOR = "[\\r\\n]+";

    static {
        Set<String> aSet = new HashSet<>();
        aSet.add(HEADER_LABEL);
        aSet.add(HEADER_COMMENT);
        aSet.add(HEADER_LOCALE);
        aSet.add(HEADER_SERVICE);
        aSet.add(HEADER_APP);
        aSet.add(HEADER_UTTERANCE);
        aSet.add(HEADER_REPLACEMENT);
        aSet.add(HEADER_COMMAND);
        aSet.add(HEADER_ARG1);
        aSet.add(HEADER_ARG2);
        COLUMNS = Collections.unmodifiableSet(aSet);
    }

    // All fields
    public static final SortedMap<Integer, String> DEFAULT_HEADER;

    static {
        SortedMap<Integer, String> aMap0 = new TreeMap<>();
        aMap0.put(0, HEADER_LABEL);
        aMap0.put(1, HEADER_COMMENT);
        aMap0.put(2, HEADER_LOCALE);
        aMap0.put(3, HEADER_SERVICE);
        aMap0.put(4, HEADER_APP);
        aMap0.put(5, HEADER_UTTERANCE);
        aMap0.put(6, HEADER_REPLACEMENT);
        aMap0.put(7, HEADER_COMMAND);
        aMap0.put(8, HEADER_ARG1);
        aMap0.put(9, HEADER_ARG2);

        DEFAULT_HEADER = Collections.unmodifiableSortedMap(aMap0);
    }

    // Only utterance
    private static final SortedMap<Integer, String> DEFAULT_HEADER_1;

    static {
        SortedMap<Integer, String> aMap1 = new TreeMap<>();
        aMap1.put(0, HEADER_UTTERANCE);
        DEFAULT_HEADER_1 = Collections.unmodifiableSortedMap(aMap1);
    }

    // Utterance and replacement
    private static final SortedMap<Integer, String> DEFAULT_HEADER_2;

    static {
        SortedMap<Integer, String> aMap2 = new TreeMap<>();
        aMap2.put(0, HEADER_UTTERANCE);
        aMap2.put(1, HEADER_REPLACEMENT);
        DEFAULT_HEADER_2 = Collections.unmodifiableSortedMap(aMap2);
    }

    /**
     * A class that holds a text (mStr) and its possible interpretation as a command
     * with a name (mId) and a list of arguments (mArgs).
     */
    public static class Rewrite {
        public final String mId;
        public final String mStr;
        public final String[] mArgs;
        public final Command mCommand;

        public Rewrite(String str) {
            this(null, str, null, null);
        }

        public Rewrite(String id, String str, String[] args, Command command) {
            mId = id;
            mStr = str;
            mArgs = args;
            mCommand = command;
        }

        public boolean isCommand() {
            return mId != null;
        }

        public String getStr() {
            return mStr;
        }

        public Command getCommand() {
            return mCommand;
        }

        /**
         * Returns the pretty-printed command, e.g.
         * name (arg1) (arg2)
         * Empty trailing arguments are dropped.
         * TODO: do not drop the arguments (decide at parse time how many arguments a command has)
         *
         * @return pretty-printed command
         */
        public String ppCommand() {
            if (mArgs == null) {
                return mId;
            }
            int len = mArgs.length;
            int last = len - 1;
            // Search for the last non-empty argument position
            for (; last >= 0 && mArgs[last].isEmpty(); last--) ;
            String pp = mId;
            for (int i = 0; i <= last; i++) {
                pp += " (" + mArgs[i] + ")";
            }
            return pp;
        }
    }

    public static class CommandHolder {
        // Line that starts with "#" or consists entirely of 0 or more tabs.
        // Must be used with "lookingAt()".
        private static final Pattern PATTERN_EMPTY_ROW = Pattern.compile("#|\t*$");

        private final List<Command> mCommands;
        private final SortedMap<Integer, String> mHeader;
        private final SortedMap<Integer, String> mErrors = new TreeMap<>();

        public CommandHolder() {
            this(DEFAULT_HEADER_1, new ArrayList<>());
        }

        public CommandHolder(String inputHeader) {
            this(inputHeader, new ArrayList<>());
        }

        public CommandHolder(SortedMap<Integer, String> header, List<Command> commands) {
            mCommands = commands;
            mHeader = header;
        }

        public CommandHolder(String inputHeader, List<Command> commands) {
            boolean hasColumnUtterance = false;
            SortedMap<Integer, String> header = new TreeMap<>();
            List<String> fields = new ArrayList<>();
            if (inputHeader != null && !inputHeader.isEmpty()) {
                final TextUtils.StringSplitter COLUMN_SPLITTER = new TextUtils.SimpleStringSplitter('\t');
                COLUMN_SPLITTER.setString(inputHeader);
                int fieldCounter = 0;
                for (String columnName : COLUMN_SPLITTER) {
                    fields.add(columnName);
                    if (COLUMNS.contains(columnName)) {
                        header.put(fieldCounter, columnName);
                        if (HEADER_UTTERANCE.equals(columnName)) {
                            hasColumnUtterance = true;
                        }
                    }
                    fieldCounter++;
                }
            }
            mCommands = commands;
            // If the Utterance column is missing then assume that the
            // input was without a header and interpret it as a one or two column table.
            if (!hasColumnUtterance) {
                if (fields.size() > 1) {
                    mHeader = DEFAULT_HEADER_2;
                    mCommands.add(0, new Command(fields.get(0), fields.get(1)));
                } else if (fields.size() > 0) {
                    mHeader = DEFAULT_HEADER_1;
                    mCommands.add(0, new Command(fields.get(0), ""));
                } else {
                    mHeader = DEFAULT_HEADER_1;
                }
            } else {
                mHeader = header;
            }
        }

        public SortedMap<Integer, String> getHeader() {
            return mHeader;
        }

        public String getHeaderAsStr() {
            return TextUtils.join("\t", mHeader.values());
        }

        public List<Command> getCommands() {
            return mCommands;
        }

        public SortedMap<Integer, String> getErrors() {
            return mErrors;
        }

        private boolean addCommand(Command command) {
            return mCommands.add(command);
        }

        private String addError(int lineNumber, String message) {
            return mErrors.put(lineNumber, message);
        }

        public int size() {
            return mCommands.size();
        }

        /**
         * Adds a line unless it consists entirely of 0 or more tabs, or starts with "#".
         */
        public void addLine(String line, int lineCounter, CommandMatcher commandMatcher) {
            if (PATTERN_EMPTY_ROW.matcher(line).lookingAt()) {
                return;
            }
            try {
                Command command = getCommand(getHeader(), line, commandMatcher);
                if (command != null) {
                    addCommand(command);
                }
            } catch (PatternSyntaxException e) {
                addError(lineCounter, e.getLocalizedMessage());
            } catch (IllegalArgumentException e) {
                addError(lineCounter, e.getLocalizedMessage());
            }
        }
    }

    private final CommandHolder mCommandHolder;

    public UtteranceRewriter(List<Command> commands) {
        mCommandHolder = new CommandHolder(DEFAULT_HEADER_2, commands);
    }

    public UtteranceRewriter(List<Command> commands, String header) {
        mCommandHolder = new CommandHolder(header, commands);
    }

    public UtteranceRewriter(List<Command> commands, SortedMap<Integer, String> header) {
        mCommandHolder = new CommandHolder(header, commands);
    }

    public UtteranceRewriter(String str, CommandMatcher commandMatcher) {
        mCommandHolder = loadRewrites(str, commandMatcher);
    }

    public UtteranceRewriter(String str, String header, CommandMatcher commandMatcher) {
        mCommandHolder = loadRewrites(str, header, commandMatcher);
    }

    public UtteranceRewriter(String str, String header) {
        this(str, header, null);
    }

    /**
     * Loads the rewrites table from the given string. The header is detected automatically.
     * Command maching is not performed.
     *
     * @param str
     */
    public UtteranceRewriter(String str) {
        this(str, (CommandMatcher) null);
    }

    public UtteranceRewriter(ContentResolver contentResolver, Uri uri) throws IOException {
        mCommandHolder = loadRewrites(contentResolver, uri);
    }

    /**
     * Rewrites and returns the given string,
     * and the first matching command.
     */
    public Rewrite getRewrite(String str) {
        for (Command command : mCommandHolder.getCommands()) {
            Pair<String, String[]> pair = command.parse(str);
            String commandId = command.getId();
            if (commandId == null || commandId.isEmpty()) {
                str = pair.first;
            } else if (pair.second != null) {
                // If there is a full match (pair.second != null) and there is a command (commandId != null)
                // then stop the search and return the command.
                str = pair.first;
                return new Rewrite(commandId, str, pair.second, command);
            }
        }
        return new Rewrite(str);
    }

    @NonNull
    public List<Command> getCommands() {
        return mCommandHolder.getCommands();
    }

    /**
     * Rewrites and returns the given results.
     */
    public List<String> rewrite(List<String> results) {
        List<String> rewrittenResults = new ArrayList<>();
        for (String result : results) {
            rewrittenResults.add(getRewrite(result).mStr);
        }
        return rewrittenResults;
    }

    /**
     * Serializes the rewrites as tab-separated-values.
     */
    public String toTsv() {
        StringBuilder stringBuilder = new StringBuilder();
        SortedMap<Integer, String> header = mCommandHolder.getHeader();
        stringBuilder.append(mCommandHolder.getHeaderAsStr());
        for (Command command : mCommandHolder.getCommands()) {
            stringBuilder.append('\n');
            stringBuilder.append(command.toTsv(header));
        }
        return stringBuilder.toString();
    }

    public CommandHolder getCommandHolder() {
        return mCommandHolder;
    }

    public String[] getErrorsAsStringArray() {
        SortedMap<Integer, String> errors = mCommandHolder.getErrors();
        String[] array = new String[errors.size()];
        int i = 0;
        for (SortedMap.Entry<Integer, String> entry : errors.entrySet()) {
            array[i++] = "#" + entry.getKey() + ": " + entry.getValue();
        }
        return array;
    }

    /**
     * Loads the rewrites from a string of tab-separated values,
     * guessing the header from the string itself.
     */
    private static CommandHolder loadRewrites(String str, CommandMatcher commandMatcher) {
        if (str == null) {
            return new CommandHolder();
        }
        String[] rows = str.split(RE_LINE_SEPARATOR);
        int length = rows.length;
        if (length == 0) {
            return new CommandHolder();
        }
        CommandHolder commandHolder = new CommandHolder(rows[0]);
        if (length > 1) {
            for (int i = 1; i < length; i++) {
                commandHolder.addLine(rows[i], i, commandMatcher);
            }
        }
        return commandHolder;
    }

    /**
     * Loads the rewrites from a string of tab-separated values.
     * The header is given by a separate argument, the table must not
     * contain the header.
     */
    private static CommandHolder loadRewrites(String str, String header, CommandMatcher commandMatcher) {
        CommandHolder commandHolder = new CommandHolder(header);
        String[] rows = str.split(RE_LINE_SEPARATOR);
        if (rows.length > 0) {
            for (int i = 0; i < rows.length; i++) {
                commandHolder.addLine(rows[i], i, commandMatcher);
            }
        }
        return commandHolder;
    }

    /**
     * Loads the rewrites from an URI using a ContentResolver.
     * The first line is a header.
     * Non-header lines are ignored if they start with '#'.
     */
    private static CommandHolder loadRewrites(ContentResolver contentResolver, Uri uri) throws IOException {
        CommandHolder commandHolder = null;
        InputStream inputStream = contentResolver.openInputStream(uri);
        if (inputStream != null) {
            BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
            String line = reader.readLine();
            if (line != null) {
                int lineCounter = 0;
                commandHolder = new CommandHolder(line);
                while ((line = reader.readLine()) != null) {
                    lineCounter++;
                    commandHolder.addLine(line, lineCounter, null);
                }
            }
            inputStream.close();
        }
        if (commandHolder == null) {
            return new CommandHolder();
        }
        return commandHolder;
    }

    /**
     * Creates a command based on the given fields.
     *
     * @param header         parsed header
     * @param line           single row
     * @param commandMatcher command matcher
     * @return command or null if commandMatcher rejects the command
     */
    private static Command getCommand(SortedMap<Integer, String> header, String line, CommandMatcher commandMatcher) {
        String label = null;
        String comment = null;
        Pattern locale = null;
        Pattern service = null;
        Pattern app = null;
        Pattern utterance = null;
        String command = null;
        String replacement = null;
        String arg1 = null;
        String arg2 = null;

        final TextUtils.StringSplitter columnSplitter = new TextUtils.SimpleStringSplitter('\t');
        columnSplitter.setString(line);

        int i = 0;
        for (String split : columnSplitter) {
            String colName = header.get(i++);
            if (colName == null) {
                continue;
            }
            switch (colName) {
                case HEADER_LABEL:
                    label = split.trim();
                    break;
                case HEADER_COMMENT:
                    comment = split.trim();
                    break;
                case HEADER_LOCALE:
                    locale = Pattern.compile(split.trim());
                    break;
                case HEADER_SERVICE:
                    service = Pattern.compile(split.trim());
                    break;
                case HEADER_APP:
                    app = Pattern.compile(split.trim());
                    break;
                case HEADER_UTTERANCE:
                    split = split.trim();
                    if (split.isEmpty()) {
                        throw new IllegalArgumentException("Empty Utterance");
                    }
                    utterance = Pattern.compile(split, Constants.REWRITE_PATTERN_FLAGS);
                    break;
                case HEADER_REPLACEMENT:
                    replacement = Command.unescape(split);
                    break;
                case HEADER_COMMAND:
                    command = Command.unescape(split.trim());
                    break;
                case HEADER_ARG1:
                    arg1 = Command.unescape(split);
                    break;
                case HEADER_ARG2:
                    arg2 = Command.unescape(split);
                    break;
                default:
                    // Columns with undefined names are ignored
                    break;
            }
        }

        if (commandMatcher != null && !commandMatcher.matches(locale, service, app)) {
            return null;
        }

        if (arg1 == null) {
            return new Command(label, comment, locale, service, app, utterance, replacement, command);
        }

        if (arg2 == null) {
            return new Command(label, comment, locale, service, app, utterance, replacement, command, new String[]{arg1});
        }

        return new Command(label, comment, locale, service, app, utterance, replacement, command, new String[]{arg1, arg2});
    }
}