/****************************************************************************
**
** Copyright (C) 2019 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.Diagnostics;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace QtVsTools.Test.RegExpr
{
    using static SyntaxAnalysis.RegExpr;

    [TestClass]
    public class Test_XmlIntParser
    {
        public const string IdNum = "NUM";
        public const string IdExpr = "EXPR";
        public const string IdExprLPar = "EXPR_LPAR";
        public const string IdTagValue = "TAG_VALUE";
        public const string IdTagBegin = "TAG_BEGIN";
        public const string IdTagName = "TAG_NAME";
        public const string IdTag = "TAG";

        public static Parser GetParser()
        {
            // XML chars
            var charLt = Char['<'];
            var charGt = Char['>'];
            var charSlash = Char['/'];

            // int expr chars
            var charPlus = Char['+'];
            var charMinus = Char['-'];
            var charMult = Char['*'];
            var charDiv = Char['/'];
            var charLPar = Char['('];
            var charRPar = Char[')'];
            var charDigit = Char['0', '9'];
            var charAddtOper = CharSet[charPlus + charMinus];
            var charMultOper = CharSet[charMult + charDiv];
            var charOper = CharSet[charAddtOper + charMultOper];

            // int operator priorities
            int priorityInfixAddt = 10;
            int priorityInfixMult = 20;
            int priorityPrefixAddt = 30;

            // token: number
            var exprNum = new Token(IdNum,
                charDigit.Repeat() & !LookAhead[SkipWs & (charDigit | charLPar)])
            {
                new Rule<int>
                {
                    Capture(value => int.Parse(value))
                }
            };

            // token: plus (infix or prefix)
            var exprPlus = new Token(IdExpr,
                charPlus & !LookAhead[SkipWs & (charOper | charRPar | charLt)])
            {
                new PrefixRule<int, int>(
                    priority: priorityPrefixAddt,
                    select: t => (t.IsFirst || t.LookBehind().First().Is(IdExprLPar))
                            && t.LookAhead().First().Is(IdNum, IdExprLPar))
                {
                    Create((int x) => +x)
                },

                new InfixRule<int, int, int>(priority: priorityInfixAddt)
                {
                    Create((int x, int y) => x + y)
                }
            };

            // token: minus (infix or prefix)
            var exprMinus = new Token(IdExpr,
                charMinus & !LookAhead[SkipWs & (charOper | charRPar | charLt)])
            {
                new PrefixRule<int, int>(
                    priority: priorityPrefixAddt,
                    select: t => (t.IsFirst || t.LookBehind().First().Is(IdExprLPar))
                            && t.LookAhead().First().Is(IdNum, IdExprLPar))
                {
                    Create((int x) => -x)
                },

                new InfixRule<int, int, int>(priority: priorityInfixAddt)
                {
                    Create((int x, int y) => x - y)
                }
            };

            // token: multiplication
            var exprMult = new Token(IdExpr,
                charMult & !LookAhead[SkipWs & (charOper | charRPar | charLt)])
            {
                new InfixRule<int, int, int>(priority: priorityInfixMult)
                {
                    Create((int x, int y) => x * y)
                }
            };

            // token: division
            var exprDiv = new Token(IdExpr,
                charDiv & !LookAhead[SkipWs & (charOper | charRPar | charLt)])
            {
                new InfixRule<int, int, int>(priority: priorityInfixMult)
                {
                    Create((int x, int y) => x / y)
                }
            };

            // token: left parenthesis
            var exprLPar = new Token(IdExprLPar,
                charLPar & !LookAhead[SkipWs & (charRPar | charLt)])
            {
                new LeftDelimiterRule<string>()
                {
                    Capture(value => value)
                }
            };

            // token: right parenthesis
            var exprRPar = new Token(IdExpr,
                charRPar & !LookAhead[SkipWs & (charDigit | charLPar)])
            {
                new RightDelimiterRule<string, int, int>
                {
                    Create((string lPar, int n) => n)
                }
            };

            // int expression
            var numExpr = (exprNum
                        | exprPlus
                        | exprMinus
                        | exprMult
                        | exprDiv
                        | exprLPar
                        | exprRPar).Repeat();

            // token: tag value containing int expression
            var tagValue = new Token(IdTagValue, SkipWs_Disable,
                LookAhead[SkipWs & CharSet[~(CharSpace + charLt)]] & numExpr & LookAhead[charLt])
            {
                new Rule<string>
                {
                    Create(IdNum, (int expr) => "=" + expr.ToString()),
                    Create(IdExpr, (int expr) => "=" + expr.ToString())
                }
            };

            // token: tag open (only tag name, no attribs)
            var tagBegin = new Token(IdTagBegin,
                charLt & new Token(IdTagName, CharWord.Repeat()) & charGt)
            {
                new LeftDelimiterRule<string>
                {
                    Create(IdTagName, (string tagName) => tagName)
                }
            };

            // token: tag close
            var tagEnd = new Token(IdTag,
                charLt & charSlash & new Token(IdTagName, CharWord.Repeat()) & LookAhead[charGt])
            {
                new RightDelimiterRule<string, string, string>
                {
                    Create(IdTagName, (string name) => name),
                    Error(
                        (string tag, string tagName) => tagName != tag,
                        (tag, tagName) => string.Format("Expected {0}, found {1}", tagName, tag)),
                    Create(
                        (string tag, string value) => value.StartsWith("="),
                        (tag, value) => tag + value),
                    Create(
                        (string tag, string value) => !value.StartsWith("="),
                        (tag, value) => tag + ":{" + value + "}")
                }
            };

            // token: tag sequence
            var tagConcat = new Token(IdTag, charGt & LookAhead[SkipWs & charLt & ~charSlash])
            {
                new InfixRule<string, string, string>(
                    pre: t => t.LeftOperand.Is(IdTag) && t.RightOperand.Is(IdTag))
                {
                    Create((string leftTag, string rightTag) => leftTag + "," + rightTag)
                }
            };

            // XML containing int expressions
            var xmlInt = StartOfLine
                & (tagBegin | tagValue | tagEnd & (tagConcat | charGt)).Repeat()
                & SkipWs & EndOfFile;

            // generate RegExpr parser
            return xmlInt.Render(CharSpace.Repeat());
        }

        readonly Parser Parser = GetParser();

        [TestMethod]
        public void TestConst()
        {
            string testInput = "<x>42</x>";
            string testOutput = "x=42";
            Debug.Assert(Parser.Parse(testInput).GetValues<string>(IdTag).First() == testOutput);
        }

        [TestMethod]
        [ExpectedException(typeof(ParseErrorException))]
        public void TestConstError()
        {
            string testInput = "<x>foo</x>";
            Parser.Parse(testInput);
        }

        [TestMethod]
        public void TestInfix()
        {
            string testInput = "<x>2 - 1</x>";
            string testOutput = "x=1";
            Debug.Assert(Parser.Parse(testInput).GetValues<string>(IdTag).First() == testOutput);
        }

        [TestMethod]
        [ExpectedException(typeof(ParseErrorException))]
        public void TestInfixError()
        {
            string testInput = "<x>2 - </x>";
            Parser.Parse(testInput);
        }

        [TestMethod]
        public void TestPrefix()
        {
            string testInput = "<x>-2 + 1</x>";
            string testOutput = "x=-1";
            Debug.Assert(Parser.Parse(testInput).GetValues<string>(IdTag).First() == testOutput);
        }
        [TestMethod]
        [ExpectedException(typeof(ParseErrorException))]
        public void TestPrefixError()
        {
            string testInput = "<x>- + 1</x>";
            Parser.Parse(testInput);
        }

        [TestMethod]
        public void TestPrecedence()
        {
            string testInput = "<x>2 + 3 * 4</x>";
            string testOutput = "x=14";
            Debug.Assert(Parser.Parse(testInput).GetValues<string>(IdTag).First() == testOutput);
        }

        [TestMethod]
        public void TestParentheses()
        {
            string testInput = "<x>(2 + 3) * 4</x>";
            string testOutput = "x=20";
            Debug.Assert(Parser.Parse(testInput).GetValues<string>(IdTag).First() == testOutput);
        }

        [TestMethod]
        [ExpectedException(typeof(ParseErrorException))]
        public void TestParenthesesLeftError()
        {
            string testInput = "<x>2 + 3) * 4</x>";
            Parser.Parse(testInput);
        }

        [TestMethod]
        [ExpectedException(typeof(ParseErrorException))]
        public void TestParenthesesRightError()
        {
            string testInput = "<x>(2 + 3 * 4</x>";
            Parser.Parse(testInput);
        }

        [TestMethod]
        public void TestParenthesesNested()
        {
            string testInput = "<x>(-((2 + 3) * 4) / 5) * 3</x>";
            string testOutput = "x=-12";
            Debug.Assert(Parser.Parse(testInput).GetValues<string>(IdTag).First() == testOutput);
        }

        [TestMethod]
        public void TestNestedTags()
        {
            string testInput = "<a><x>(-((2 + 3) * 4) / 5) * 3</x><y>(2 + 3) * 4</y></a>";
            string testOutput = "a:{x=-12,y=20}";
            Debug.Assert(Parser.Parse(testInput).GetValues<string>(IdTag).First() == testOutput);
        }

        [TestMethod]
        [ExpectedException(typeof(ParseErrorException))]
        public void TestNestedTagsError()
        {
            string testInput = "<a><x>1</x><y>2<z><w>";
            Parser.Parse(testInput);
        }

        [TestMethod]
        public void TestMultiLines()
        {
            string testInput =
                "<a>" + "\r\n" +
                "  <x>2 + 3 * 4</x>" + "\r\n" +
                "  <y>(2 + 3) * 4</y>" + "\r\n" +
                "</a>";
            string testOutput = "a:{x=14,y=20}";
            Debug.Assert(Parser.Parse(testInput).GetValues<string>(IdTag).First() == testOutput);
        }
    }
}