/****************************************************************************
**
** Copyright (C) 2017 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the Qt VS Tools.
**
** $QT_BEGIN_LICENSE:GPL-EXCEPT$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.IO;

namespace QtVsTools.Core.CommandLine
{
    using IVSMacroExpander = QtMsBuild.IVSMacroExpander;

    public class Parser
    {

        List<Option> commandLineOptionList = new List<Option>();
        Dictionary<string, int> nameHash = new Dictionary<string, int>();
        Dictionary<int, List<string>> optionValuesHash = new Dictionary<int, List<string>>();
        List<string> optionNames = new List<string>();
        List<string> positionalArgumentList = new List<string>();
        List<string> unknownOptionNames = new List<string>();
        bool needsParsing = true;

        public enum SingleDashWordOptionMode
        {
            ParseAsCompactedShortOptions = 0,
            ParseAsLongOptions = 1
        }
        SingleDashWordOptionMode singleDashWordOptionMode = 0;

        public enum OptionsAfterPositionalArgumentsMode
        {
            ParseAsOptions = 0,
            ParseAsPositionalArguments = 1
        }
        OptionsAfterPositionalArgumentsMode optionsAfterPositionalArgumentsMode = 0;

        public Parser()
        {
        }

        public string ApplicationDescription
        {
            get;
            set;
        }

        public string ErrorText
        {
            get
            {
                throw new NotImplementedException();
            }
        }

        public string HelpText
        {
            get
            {
                throw new NotImplementedException();
            }
        }

        public IEnumerable<string> PositionalArguments
        {
            get
            {
                CheckParsed("PositionalArguments");
                return positionalArgumentList;
            }
        }

        public IEnumerable<string> OptionNames
        {
            get
            {
                CheckParsed("OptionNames");
                return optionNames;
            }
        }

        public IEnumerable<string> UnknownOptionNames
        {
            get
            {
                CheckParsed("UnknownOptionNames");
                return unknownOptionNames;
            }
        }

        IEnumerable<string> Aliases(string optionName)
        {
            int optionIndex;
            if (!nameHash.TryGetValue(optionName, out optionIndex)) {
                return new List<string>();
            }
            return commandLineOptionList[optionIndex].Names;
        }

        public void SetSingleDashWordOptionMode(SingleDashWordOptionMode singleDashWordOptionMode)
        {
            this.singleDashWordOptionMode = singleDashWordOptionMode;
        }

        public void SetOptionsAfterPositionalArgumentsMode(
            OptionsAfterPositionalArgumentsMode parsingMode)
        {
            this.optionsAfterPositionalArgumentsMode = parsingMode;
        }

        public bool AddOption(Option option)
        {
            if (option.Names.Any()) {
                foreach (var name in option.Names) {
                    if (nameHash.ContainsKey(name))
                        return false;
                }

                commandLineOptionList.Add(option);

                int offset = commandLineOptionList.Count() - 1;
                foreach (var name in option.Names)
                    nameHash.Add(name, offset);

                return true;
            }

            return false;
        }

        public bool AddOptions(IEnumerable<Option> options)
        {
            bool result = true;
            foreach (var option in options)
                result &= AddOption(option);

            return result;
        }

        public Option AddVersionOption()
        {
            Option opt = new Option(new[] { "v", "version" });
            AddOption(opt);
            return opt;
        }

        public Option AddHelpOption()
        {
            Option opt = new Option(new[] { "?", "h", "help" });
            AddOption(opt);
            return opt;
        }

        public void AddPositionalArgument(string name, string description, string syntax)
        {
            throw new NotImplementedException();
        }

        public void ClearPositionalArguments()
        {
            throw new NotImplementedException();
        }

        bool RegisterFoundOption(string optionName)
        {
            if (nameHash.ContainsKey(optionName)) {
                optionNames.Add(optionName);
                return true;
            } else {
                unknownOptionNames.Add(optionName);
                return false;
            }
        }

        bool ParseOptionValue(string optionName, string argument,
             IEnumerator<string> argumentEnumerator, ref bool atEnd)
        {
            const char assignChar = '=';
            int optionOffset;
            if (nameHash.TryGetValue(optionName, out optionOffset)) {
                int assignPos = argument.IndexOf(assignChar);
                bool withValue = !string.IsNullOrEmpty(
                    commandLineOptionList[optionOffset].ValueName);
                if (withValue) {
                    if (assignPos == -1) {
                        if (atEnd = (!argumentEnumerator.MoveNext())) {
                            return false;
                        }
                        if (!optionValuesHash.ContainsKey(optionOffset))
                            optionValuesHash.Add(optionOffset, new List<string>());
                        optionValuesHash[optionOffset].Add(argumentEnumerator.Current);
                    } else {
                        if (!optionValuesHash.ContainsKey(optionOffset))
                            optionValuesHash.Add(optionOffset, new List<string>());
                        optionValuesHash[optionOffset].Add(argument.Substring(assignPos + 1));
                    }
                } else {
                    if (assignPos != -1) {
                        return false;
                    }
                }
            }
            return true;
        }

        void CheckParsed(string method)
        {
            if (needsParsing)
                Trace.TraceWarning("CommandLineParser: Parse() before {0}", method);
        }

        List<string> TokenizeArgs(string commandLine, IVSMacroExpander macros, string execName = "")
        {
            List<string> arguments = new List<string>();
            StringBuilder arg = new StringBuilder();
            bool foundExec = string.IsNullOrEmpty(execName);
            foreach (Match token in Lexer.Tokenize(commandLine + " ")) {
                // Additional " " ensures loop will always end with whitespace processing

                if (!foundExec) {
                    if (!token.TokenText()
                        .EndsWith(execName,
                        StringComparison.InvariantCultureIgnoreCase)) {
                        continue;
                    }

                    foundExec = true;
                }

                var tokenType = token.TokenType();
                if (tokenType == Token.Whitespace || tokenType == Token.Newline) {
                    // This will always run at the end of the loop

                    if (arg.Length > 0) {
                        var argData = arg.ToString();
                        arg.Clear();
                        if (argData.StartsWith("@")) {
                            var workingDir = macros.ExpandString("$(MSBuildProjectDirectory)");
                            var optFilePath = macros.ExpandString(argData.Substring(1));
                            string[] additionalArgs = File.ReadAllLines(
                                Path.Combine(workingDir, optFilePath));
                            if (additionalArgs != null) {
                                var additionalArgsString = string.Join(" ", additionalArgs
                                    .Select(x => "\"" + x.Replace("\"", "\\\"") + "\""));
                                arguments.AddRange(TokenizeArgs(additionalArgsString, macros));
                            }
                        } else {
                            arguments.Add(argData);
                        }
                    }
                    if (tokenType == Token.Newline)
                        break;

                } else {
                    arg.Append(token.TokenText());
                }
            }
            return arguments;
        }

        public bool Parse(string commandLine, IVSMacroExpander macros, string execName)
        {
            List<string> args = null;
            try {
                args = TokenizeArgs(commandLine, macros, execName);
            } catch {
                return false;
            }
            return Parse(args);
        }

        public bool Parse(IEnumerable<string> args)
        {
            needsParsing = false;

            bool error = false;

            const string doubleDashString = "--";
            const char dashChar = '-';
            const char assignChar = '=';

            bool forcePositional = false;
            positionalArgumentList.Clear();
            optionNames.Clear();
            unknownOptionNames.Clear();
            optionValuesHash.Clear();

            if (args == null || !args.Any()) {
                return false;
            }

            var argumentIterator = args.GetEnumerator();
            bool atEnd = false;

            while (!atEnd && argumentIterator.MoveNext()) {
                var argument = argumentIterator.Current;
                if (forcePositional) {
                    positionalArgumentList.Add(argument);
                } else if (argument.StartsWith(doubleDashString)) {
                    if (argument.Length > 2) {
                        var optionName = argument.Substring(2).Split(new char[] { assignChar })[0];
                        if (RegisterFoundOption(optionName)) {
                            if (!ParseOptionValue(
                                optionName,
                                argument,
                                argumentIterator,
                                ref atEnd)) {
                                error = true;
                            }

                        } else {
                            error = true;
                        }
                    } else {
                        forcePositional = true;
                    }
                } else if (argument.StartsWith(dashChar.ToString())) {
                    if (argument.Length == 1) { // single dash ("stdin")
                        positionalArgumentList.Add(argument);
                        continue;
                    }
                    string optionName = "";
                    switch (singleDashWordOptionMode) {
                    case SingleDashWordOptionMode.ParseAsCompactedShortOptions:
                        bool valueFound = false;
                        for (int pos = 1; pos < argument.Length; ++pos) {
                            optionName = argument.Substring(pos, 1);
                            if (!RegisterFoundOption(optionName)) {
                                error = true;
                            } else {
                                int optionOffset;
                                Trace.Assert(nameHash.TryGetValue(
                                    optionName,
                                    out optionOffset));
                                bool withValue = !string.IsNullOrEmpty(
                                    commandLineOptionList[optionOffset].ValueName);
                                if (withValue) {
                                    if (pos + 1 < argument.Length) {
                                        if (argument[pos + 1] == assignChar)
                                            ++pos;
                                        if (!optionValuesHash.ContainsKey(optionOffset)) {
                                            optionValuesHash.Add(
                                                optionOffset,
                                                new List<string>());
                                        }
                                        optionValuesHash[optionOffset].Add(
                                            argument.Substring(pos + 1));
                                        valueFound = true;
                                    }
                                    break;
                                }
                                if (pos + 1 < argument.Length
                                    && argument[pos + 1] == assignChar) {
                                    break;
                                }
                            }
                        }
                        if (!valueFound
                            && !ParseOptionValue(
                                optionName,
                                argument,
                                argumentIterator,
                                ref atEnd)) {
                            error = true;
                        }

                        break;
                    case SingleDashWordOptionMode.ParseAsLongOptions:
                        if (argument.Length > 2) {
                            string possibleShortOptionStyleName = argument.Substring(1, 1);

                            int shortOptionIdx;
                            if (nameHash.TryGetValue(
                                possibleShortOptionStyleName,
                                out shortOptionIdx)) {
                                var arg = commandLineOptionList[shortOptionIdx];
                                if ((arg.Flags & Option.Flag.ShortOptionStyle) != 0) {
                                    RegisterFoundOption(possibleShortOptionStyleName);
                                    if (!optionValuesHash.ContainsKey(shortOptionIdx)) {
                                        optionValuesHash.Add(
                                            shortOptionIdx,
                                            new List<string>());
                                    }
                                    optionValuesHash[shortOptionIdx].Add(
                                        argument.Substring(2));
                                    break;
                                }
                            }
                        }
                        optionName = argument.Substring(1).Split(new char[] { assignChar })[0];
                        if (RegisterFoundOption(optionName)) {
                            if (!ParseOptionValue(
                                optionName,
                                argument,
                                argumentIterator,
                                ref atEnd)) {
                                error = true;
                            }

                        } else {
                            error = true;
                        }
                        break;
                    }
                } else {
                    positionalArgumentList.Add(argument);
                    if (optionsAfterPositionalArgumentsMode
                        == OptionsAfterPositionalArgumentsMode.ParseAsPositionalArguments) {
                        forcePositional = true;
                    }
                }
            }
            return !error;
        }

        public bool IsSet(string name)
        {
            CheckParsed("IsSet");
            if (optionNames.Contains(name))
                return true;
            var aliases = Aliases(name);
            foreach (var optionName in optionNames) {
                if (aliases.Contains(optionName))
                    return true;
            }
            return false;
        }

        public string Value(string optionName)
        {
            CheckParsed("Value");
            var valueList = Values(optionName);
            if (valueList.Any())
                return valueList.Last();
            return "";
        }

        public IEnumerable<string> Values(string optionName)
        {
            CheckParsed("Values");
            int optionOffset;
            if (nameHash.TryGetValue(optionName, out optionOffset)) {
                var values = optionValuesHash[optionOffset];
                return values;
            }

            Trace.TraceWarning("QCommandLineParser: option not defined: \"{0}\"", optionName);
            return new List<string>();
        }

        public bool IsSet(Option option)
        {
            return option.Names.Any() && IsSet(option.Names.First());
        }

        public string Value(Option option)
        {
            return Value(option.Names.FirstOrDefault());
        }

        public IEnumerable<string> Values(Option option)
        {
            return Values(option.Names.FirstOrDefault());
        }
    }

    public class Option
    {
        [Flags]
        public enum Flag
        {
            HiddenFromHelp = 0x1,
            ShortOptionStyle = 0x2
        }

        public Option(string name, string valueName = "")
        {
            Names = new[] { name };
            ValueName = valueName;
            Flags = 0;
        }

        public Option(IEnumerable<string> names, string valueName = "")
        {
            Names = names;
            ValueName = valueName;
            Flags = 0;
        }

        public Option(Option other)
        {
            Names = other.Names;
            ValueName = other.ValueName;
            Flags = other.Flags;
        }

        public IEnumerable<string> Names
        {
            get;
            private set;
        }

        public string ValueName
        {
            get;
            set;
        }

        public Flag Flags
        {
            get;
            set;
        }

        public override string ToString()
        {
            return Names.Last();
        }

    }

    enum Token
    {
        Unknown = 0,
        Newline = 1,
        Unquoted = 2,
        Quoted = 3,
        Whitespace = 4
    };

    static class Lexer
    {
        static Regex lexer = new Regex(
            /* Newline    */ @"(\n)" +
            /* Unquoted   */ @"|((?:(?:[^\s\""])|(?:(?<=\\)\""))+)" +
            /* Quoted     */ @"|(?:\""((?:(?:[^\""])|(?:(?<=\\)\""))+)\"")" +
            /* Whitespace */ @"|(\s+)");

        public static Token TokenType(this Match token)
        {
            for (int i = 1; i < token.Groups.Count; i++) {
                if (!string.IsNullOrEmpty(token.Groups[i].Value))
                    return (Token)i;
            }
            return Token.Unknown;
        }

        public static string TokenText(this Match token)
        {
            Token t = TokenType(token);
            if (t != Token.Unknown)
                return token.Groups[(int)t].Value.Replace("\\\"", "\"");
            return "";
        }

        public static MatchCollection Tokenize(string commandLine)
        {
            return lexer.Matches(commandLine);
        }
    }
}