#region License

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */

#endregion

using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Numerics;
using System.Text.Json;
using System.Text.RegularExpressions;
using Gherkin.Ast;
using Gremlin.Net.IntegrationTest.Gherkin.Attributes;
using Gremlin.Net.Process.Traversal;
using Gremlin.Net.Structure;
using Gremlin.Net.Structure.IO.GraphSON;
using Xunit;

using static Gremlin.Net.Process.Traversal.AnonymousTraversalSource;

namespace Gremlin.Net.IntegrationTest.Gherkin
{
    internal class CommonSteps : StepDefinition
    {
        private GraphTraversalSource? _g;
        private string? _graphName;
        private readonly IDictionary<string, object?> _parameters = new Dictionary<string, object?>();
        private ITraversal? _traversal;
        private object?[]? _result;
        private Exception? _error;

        private static readonly JsonSerializerOptions JsonDeserializingOptions =
            new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
        
        public static ScenarioData ScenarioData { get; set; } = new ScenarioData(new GraphSON3MessageSerializer());

        private static readonly IDictionary<Regex, Func<string, string, object?>> Parsers =
            new Dictionary<string, Func<string, string, object?>>
            {
                {@"str\[(.*)\]", (x, graphName) => x }, //returns the string value as is
                {@"vp\[(.+)\]", ToVertexProperty},
                {@"dt\[(.+)\]", ToDateTime},
                {@"d\[(.*)\]\.([bsilfdmn])", ToNumber},
                {@"D\[(.+)\]", ToDirection},
                {@"M\[(.+)\]", ToMerge},
                {@"v\[(.+)\]", ToVertex},
                {@"v\[(.+)\]\.id", (x, graphName) => ToVertex(x, graphName).Id},
                {@"v\[(.+)\]\.sid", (x, graphName) => ToVertex(x, graphName).Id!.ToString()},
                {@"e\[(.+)\]", ToEdge},
                {@"e\[(.+)\].id", (x, graphName) => ToEdge(x, graphName).Id},
                {@"e\[(.+)\].sid", (x, graphName) => ToEdge(x, graphName).Id!.ToString()},
                {@"p\[(.+)\]", ToPath},
                {@"l\[(.*)\]", ToList},
                {@"s\[(.*)\]", ToSet},
                {@"m\[(.+)\]", ToMap},
                {@"c\[(.+)\]", ToLambda},
                {@"t\[(.+)\]", ToT},
                {"null", (_, __) => null},
                {"true", (_, __) => true},
                {"false", (_, __) => false},
                {@"d\[NaN\]", (_, __) => Double.NaN},
                {@"d\[Infinity\]", (_, __) => Double.PositiveInfinity},
                {@"d\[-Infinity\]", (_, __) => Double.NegativeInfinity}
            }.ToDictionary(kv => new Regex("^" + kv.Key + "$", RegexOptions.Compiled), kv => kv.Value);

        private static readonly IDictionary<char, Func<string, object>> NumericParsers =
            new Dictionary<char, Func<string, object>>
            {
                { 'b', s => Convert.ToByte(s) },
                { 's', s => Convert.ToInt16(s) },
                { 'i', s => Convert.ToInt32(s) },
                { 'l', s => Convert.ToInt64(s) },
                { 'f', s => Convert.ToSingle(s, CultureInfo.InvariantCulture) },
                { 'd', s => Convert.ToDouble(s, CultureInfo.InvariantCulture) },
                { 'm', s => Convert.ToDecimal(s, CultureInfo.InvariantCulture) },
                { 'n', s => BigInteger.Parse(s) }
            };

        [Given("the (\\w+) graph")]
        public void ChooseModernGraph(string graphName)
        {
            if (graphName == "empty")
            {
                ScenarioData.CleanEmptyData();
            }
            var data = ScenarioData.GetByGraphName(graphName);
            _graphName = graphName;
            _g = Traversal().WithRemote(data.Connection);
        }

        [Given("using the parameter (\\w+) defined as \"(.*)\"")]
        public void UsingParameter(string name, string value)
        {
            var parsedValue = ParseValue(value.Replace("\\\"", "\""), _graphName!);
            _parameters.Add(name, parsedValue);
        }

        [Given("using the parameter (\\w+) of P.(\\w+)\\(\"(.*)\"\\)")]
        public void UsingParameterP(string name, string pval, string value)
        {
            var parsedValue = ParseValue(value.Replace("\\\"", "\""), _graphName!);
            _parameters.Add(name, new P(pval, parsedValue));
        }

        [Given("the traversal of")]
        public void TranslateTraversal(string traversalText)
        {
            if (_g == null)
            {
                throw new InvalidOperationException("g should be a traversal source");
            }

            if (ScenarioData.CurrentFeature!.Tags.Any(t => t.Name == "@GraphComputerOnly"))
            {
                _g = _g.WithComputer();
            }

            _traversal =
                Gremlin.UseTraversal(ScenarioData.CurrentScenario!.Name, _g, _parameters);
        }

        [Given("the graph initializer of")]
        public void InitTraversal(string traversalText)
        {
            var traversal =
                Gremlin.UseTraversal(ScenarioData.CurrentScenario!.Name, _g, _parameters);
            traversal.Iterate();
            
            // We may have modified the so-called `empty` graph
            if (_graphName == "empty")
            {
                ScenarioData.ReloadEmptyData();
            }
        }

        [Given("an unsupported test")]
        public void UnsupportedTest()
        {
            
        }

        [When("iterated to list")]
        public void IterateToList()
        {
            if (_traversal == null)
            {
                throw new InvalidOperationException("Traversal should be set before iterating");
            }
            ITraversal t = _traversal;
            var list = new List<object>();
            try
            {
                while (t.MoveNext())
                {
                    list.Add(t.Current);
                }
                _result = list.ToArray();
            }
            catch (Exception ex)
            {
                _error = ex;
            }
        }

        [When("iterated next")]
        public void IterateNext()
        {
            if (_traversal == null)
            {
                throw new InvalidOperationException("Traversal should be set before iterating");
            }

            try
            {
                _traversal.MoveNext();
                var result = _traversal.Current;
                switch (result)
                {
                    case null:
                        _result = null;
                        return;
                    case object[] arrayResult:
                        _result = arrayResult;
                        return;
                    case IEnumerable enumerableResult:
                        _result = enumerableResult.Cast<object>().ToArray();
                        return;
                }
            }
            catch (Exception ex)
            {
                _error = ex;
            }
        }

        [Then("the traversal will raise an error")]
        public void TraversalWillRaiseError()
        {
            Assert.NotNull(_error);

            // consume the error now that it has been asserted
            _error = null;
        }

        [Then("the traversal will raise an error with message (\\w+) text of \"(.*)\"")]
        public void TraversalWillRaiseErrorWithMessage(string comparison, string expectedMessage)
        {
            Assert.NotNull(_error);

            switch (comparison) {
                case "containing":
                    Assert.Contains(expectedMessage, _error.Message);
                    break;
                case "starting":
                    Assert.StartsWith(expectedMessage, _error.Message);
                    break;
                case "ending":
                    Assert.EndsWith(expectedMessage, _error.Message);
                    break;
                default:
                    throw new NotSupportedException(
                            "Unknown comparison of " + comparison + " - must be one of: containing, starting or ending");
            }

            // consume the error now that it has been asserted
            _error = null;
        }

        [Then("the result should be (\\w+)")]
        public void AssertResult(string characterizedAs, DataTable? table = null)
        {
            AssertThatNoErrorWasThrown();

            var ordered = characterizedAs == "ordered";
            switch (characterizedAs)
            {
                case "empty":
                    Assert.Empty(_result!);
                    return;
                case "ordered":
                case "unordered":
                case "of":
                    Assert.NotNull(table);
                    var rows = table.Rows.ToArray();
                    Assert.Equal("result", rows[0].Cells.First().Value);
                    var expected = rows.Skip(1).Select(x => ParseValue(x.Cells.First().Value, _graphName!));

                    if (ordered)
                    {
                        Assert.Equal(expected, _result!);
                    }
                    else
                    {
                        var expectedArray = expected.ToArray();
                        foreach (var resultItem in _result!)
                        {
                            if (resultItem is Dictionary<object, object> resultItemDict)
                            {
                                var expectedArrayContainsResultDictionary = false;
                                foreach (var expectedItem in expectedArray)
                                {
                                    if (expectedItem is not Dictionary<object, object> expectedItemDict) continue;
                                    if (!expectedItemDict.DeepEqual(resultItemDict)) continue;
                                    expectedArrayContainsResultDictionary = true;
                                    break;
                                }
                                Assert.True(expectedArrayContainsResultDictionary);
                            }
                            else if (resultItem is HashSet<object> resultItemSet)
                            {
                                var expectedArrayContainsResultAsSet = false;
                                foreach (var expectedItem in expectedArray)
                                {
                                    if (expectedItem is not HashSet<object> expectedItemSet) continue;
                                    if (!expectedItemSet.SetEquals(resultItemSet)) continue;
                                    expectedArrayContainsResultAsSet = true;
                                    break;
                                }
                                Assert.True(expectedArrayContainsResultAsSet);
                            }
                            else if (resultItem is double resultItemDouble &&
                                     expectedArray.Select(e => e!.GetType()).Any(t => t == typeof(decimal)))
                            {
                                // Java seems to use BigDecimal by default sometimes where .NET uses double, but we only
                                // care for the value not its type here. So we just convert these to decimal (equivalent
                                // to Java's BigDecimal) and then check whether the item is in the expected results.
                                Assert.Contains((decimal)resultItemDouble, expectedArray);
                            }
                            else
                            {
                                Assert.Contains(resultItem, expectedArray);
                            }
                        }
                        if (characterizedAs != "of")
                        {
                            Assert.Equal(expectedArray.Length, _result.Length);   
                        }
                    }
                    break;
                default:
                    throw new NotSupportedException($"Result as '{characterizedAs}' not supported");
            }
        }

        [Then("the result should have a count of (\\d+)")]
        public void AssertCount(int count)
        {
            AssertThatNoErrorWasThrown();

            Assert.Equal(count, _result!.Length);
        }

        [Then("the graph should return (\\d+) for count of (.+)")]
        public void AssertTraversalCount(int expectedCount, string traversalText)
        {
            AssertThatNoErrorWasThrown();

            if (traversalText.StartsWith("\""))
            {
                traversalText = traversalText.Substring(1, traversalText.Length - 2);
            }
            
            var traversal =
                Gremlin.UseTraversal(ScenarioData.CurrentScenario!.Name, _g, _parameters);
            
            var count = 0;
            while (traversal.MoveNext())
            {
                count++;
            }
            Assert.Equal(expectedCount, count);
        }

        [Then("nothing should happen because")]
        public void AssertNothing(string reason)
        {
            
        }

        private void AssertThatNoErrorWasThrown()
        {
            if (_error != null) throw _error;
        }

        private static object ToMap(string stringMap, string graphName)
        {
            var jsonMap = JsonSerializer.Deserialize<JsonElement>(stringMap, JsonDeserializingOptions);
            return ParseMapValue(jsonMap, graphName)!;
        }

        private static object ToLambda(string stringLambda, string graphName)
        {
            return Lambda.Groovy(stringLambda);
        }

        private static object ToT(string enumName, string graphName)
        {
            return T.GetByValue(enumName);
        }

        private static object ToDirection(string enumName, string graphName)
        {
            return Direction.GetByValue(enumName);
        }

        private static object ToMerge(string enumName, string graphName)
        {
            return Merge.GetByValue(enumName);
        }

        private static object ToNumber(string stringNumber, string graphName)
        {
            return NumericParsers[stringNumber[stringNumber.Length - 1]](
                stringNumber.Substring(0, stringNumber.Length - 1));
        }

        private static object? ParseMapValue(JsonElement value, string graphName)
        {
            switch (value.ValueKind)
            {
                case JsonValueKind.Object:
                {
                    return value.EnumerateObject().ToDictionary(property => ParseValue(property.Name, graphName)!,
                        property => ParseMapValue(property.Value, graphName));
                }
                case JsonValueKind.Array:
                    return value.EnumerateArray().Select(v => ParseMapValue(v, graphName)).ToArray();
                case JsonValueKind.Number:
                {
                    // This can maybe be simplified when this issue is resolved:
                    // https://github.com/dotnet/runtime/issues/31274
                    if (value.TryGetInt32(out var integer))
                    {
                        return integer;
                    }

                    if (value.TryGetDouble(out var floating))
                    {
                        return floating;
                    }

                    throw new ArgumentOutOfRangeException(nameof(value), value, "Not a supported number type");
                }
                case JsonValueKind.String:
                    return ParseValue(value.GetString()!, graphName);
                case JsonValueKind.True:
                    return true;
                case JsonValueKind.False:
                    return false;
                case JsonValueKind.Null:
                    return null;
                default:
                    throw new ArgumentOutOfRangeException(nameof(value.ValueKind), value.ValueKind,
                        "JSON type not supported");
            }
        }

        private static ISet<object?> ToSet(string stringSet, string graphName)
        {
            return new HashSet<object?>(ToList(stringSet, graphName));
        }

        private static IList<object?> ToList(string stringList, string graphName)
        {
            if (stringList == "")
            {
                return new List<object?>(0);
            }
            return stringList.Split(',').Select(x => ParseValue(x, graphName)).ToList();
        }

        private static object ToDateTime(string date, string graphName)
        {
            return DateTimeOffset.Parse(date);
        }

        private static Vertex ToVertex(string name, string graphName)
        {
            if (ScenarioData.GetByGraphName(graphName).Vertices.ContainsKey(name))
            {
                return ScenarioData.GetByGraphName(graphName).Vertices[name];
            }
            else
            {
                return new Vertex(name);
            }
        }

        private static Edge ToEdge(string name, string graphName)
        {
            return ScenarioData.GetByGraphName(graphName).Edges[name];
        }

        private static VertexProperty ToVertexProperty(string triplet, string graphName)
        {
            return ScenarioData.GetByGraphName(graphName).VertexProperties[triplet];
        }

        private static Path ToPath(string value, string graphName)
        {
            return new Path(new List<ISet<string>>(0),
                value.Split(',').Select(x => ParseValue(x, graphName)).ToList());
        }

        private static object? ParseValue(string stringValue, string graphName)
        {
            Func<string, string, object?>? parser = null;
            string? extractedValue = null;
            foreach (var kv in Parsers)
            {
                var match = kv.Key.Match(stringValue);
                if (match.Success)
                {
                    parser = kv.Value;
                    extractedValue = match.Groups[1].Value;
                    if (match.Groups.Count > 2)
                    {
                        extractedValue += match.Groups[2].Value;
                    }
                    break;
                }
            }
            return parser != null ? parser(extractedValue!, graphName) : stringValue;
        }
    }
}