using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using YamlDotNet.RepresentationModel;
using YamlDotNet.Serialization;
namespace Amazon.Lambda.Annotations.SourceGenerator.Writers
{
    /// 
    /// This contains the functionality to manipulate a YAML blob
    /// 
    public class YamlWriter : ITemplateWriter
    {
        private YamlMappingNode _rootNode;
        private readonly Serializer _serializer  = new Serializer();
        private readonly Deserializer _deserializer = new Deserializer();
        private readonly SerializerBuilder _serializerBuilder = new SerializerBuilder();
        public YamlWriter()
        {
            _rootNode = new YamlMappingNode();
        }
        /// 
        /// Checks if the dot(.) seperated yamlPath exists in the YAML blob stored at the _rootNode
        /// 
        /// dot(.) seperated path. Example "Person.Name.FirstName"
        /// true if the path exist, else false
        /// Thrown if the yamlPath is invalid
        public bool Exists(string yamlPath)
        {
            if (!IsValidPath(yamlPath))
            {
                throw new InvalidDataException($"'{yamlPath}' is not a valid {nameof(yamlPath)}");
            }
            YamlNode currentNode = _rootNode;
            foreach (var property in yamlPath.Split('.'))
            {
                if (currentNode == null)
                {
                    return false;
                }
                try
                {
                    currentNode = currentNode[property];
                }
                catch (KeyNotFoundException)
                {
                    return false;
                }
            }
            return true;
        }
        /// 
        /// Gets the object stored at the dot(.) seperated yamlPath. If the path does not exist then return the defaultToken.
        /// The defaultToken is only returned if it holds a non-null value.
        /// 
        /// dot(.) seperated path. Example "Person.Name.FirstName"
        /// The object that is returned if yamlPath does not exist.
        /// Thrown if the yamlPath does not exist and the defaultToken is null
        public object GetToken(string yamlPath, object defaultToken = null)
        {
            if (!Exists(yamlPath))
            {
                if (defaultToken != null)
                {
                    return defaultToken;
                }
                throw new InvalidOperationException($"'{yamlPath}' does not exist in the JSON model");
            }
            YamlNode currentNode = _rootNode;
            foreach (var property in yamlPath.Split('.'))
            {
                currentNode = currentNode[property];
            }
            return currentNode;
        }
        /// 
        /// Gets the object stored at the dot(.) seperated yamlPath. If the path does not exist then return the defaultToken.
        /// The defaultToken is only returned if it holds a non-null value.
        /// The object is deserialized into type T before being returned.
        /// 
        /// dot(.) seperated path. Example "Person.Name.FirstName"
        /// The object that is returned if yamlPath does not exist in the YAML blob. It will be convert to type T before being returned.
        /// Thrown if the yamlPath does not exist and the defaultToken is null
        public T GetToken(string yamlPath, object defaultToken = null)
        {
            var token = GetToken(yamlPath, defaultToken);
            if (token == null)
            {
                throw new InvalidOperationException($"'{yamlPath}' points to a null token");
            }
            return GetDeserializedToken(token);
        }
        /// 
        /// This method converts the supplied token it into a concrete  type and sets it at the dot(.) seperated yamlPath.
        /// Any non-existing nodes in the yamlPath are created on the fly.
        /// All non-terminal nodes in the yamlPath need to be of type .
        /// 
        /// dot(.) seperated path. Example "Person.Name.FirstName"
        /// The object to set at the specified yamlPath
        /// 
        /// Thrown if the yamlPath is invalid
        /// Thrown if the terminal property in the yamlPath is null/empty or if any non-terminal nodes in the yamlPath cannot be converted to 
        public void SetToken(string yamlPath, object token, TokenType tokenType = TokenType.Other)
        {
            if (!IsValidPath(yamlPath))
            {
                throw new InvalidDataException($"'{yamlPath}' is not a valid '{nameof(yamlPath)}'");
            }
            if (token == null)
            {
                return;
            }
            var pathList = yamlPath.Split('.');
            var lastProperty = pathList.LastOrDefault();
            if (string.IsNullOrEmpty(lastProperty))
            {
                throw new InvalidOperationException($"Cannot set a token at '{yamlPath}' because the terminal property is null or empty");
            }
            YamlNode terminalToken;
            if (token is YamlNode yamlNode)
            {
                terminalToken = yamlNode;
            }
            else
            {
                switch (tokenType)
                {
                    case TokenType.List:
                        terminalToken = GetDeserializedToken(token);
                        break;
                    case TokenType.KeyVal:
                    case TokenType.Object:
                        terminalToken = GetDeserializedToken(token);
                        break;
                    case TokenType.Other:
                        terminalToken = GetDeserializedToken(token);
                        break;
                    default:
                        throw new InvalidOperationException($"Failed to deserialize token because {nameof(tokenType)} is invalid");
                }
            }
            var currentNode = _rootNode;
            for (var i = 0; i < pathList.Length - 1; i++)
            {
                if (currentNode == null)
                {
                    throw new InvalidOperationException($"Cannot set a token at '{yamlPath}' because one of the nodes in the path is null");
                }
                var property = pathList[i];
                try
                {
                    currentNode = (YamlMappingNode)currentNode[property];
                }
                catch (KeyNotFoundException)
                {
                    currentNode.Children[property] = new YamlMappingNode();
                    currentNode = (YamlMappingNode)currentNode[property];
                }
                catch (InvalidCastException)
                {
                    throw new InvalidOperationException($"Cannot set a token at '{yamlPath}' because one of the nodes in the path cannot be converted to {nameof(YamlMappingNode)}");
                }
            }
            currentNode.Children[lastProperty] = terminalToken;
        }
        /// 
        /// Deletes the token found at the dot(.) separated yamlPath. It does not do anything if the yamlPath does not exist.
        /// 
        /// dot(.) seperated path. Example "Person.Name.FirstName"
        /// 
        public void RemoveToken(string yamlPath)
        {
            if (!Exists(yamlPath))
            {
                return;
            }
            var pathList = yamlPath.Split('.');
            var lastProperty = pathList.LastOrDefault();
            if (string.IsNullOrEmpty(lastProperty))
            {
                throw new InvalidOperationException(
                    $"Cannot remove the token at '{yamlPath}' because the terminal property is null or empty");
            }
            YamlNode currentNode = _rootNode;
            for (var i = 0; i < pathList.Length - 1; i++)
            {
                var property = pathList[i];
                currentNode = currentNode[property];
            }
            var terminalNode = (YamlMappingNode)currentNode;
            terminalNode.Children.Remove(lastProperty);
        }
        /// 
        /// Parses the YAML string as a 
        /// 
        /// 
        public void Parse(string content)
        {
            _rootNode = string.IsNullOrEmpty(content)
                ? new YamlMappingNode()
                : _deserializer.Deserialize(content);
        }
        /// 
        /// Converts the  to a YAML string
        /// 
        public string GetContent()
        {
            return _serializerBuilder
                .WithIndentedSequences()
                .Build()
                .Serialize(_rootNode);
        }
        /// 
        /// If the string does not start with '@', return it as is.
        /// If a string value starts with '@' then a reference node is created and returned.
        /// 
        public object GetValueOrRef(string value)
        {
            if (!value.StartsWith("@"))
                return value;
            var yamlNode = new YamlMappingNode();
            yamlNode.Children["Ref"] = value.Substring(1);
            return yamlNode;
        }
        /// 
        /// Validates that the yamlPath is not null or comprises only of white spaces. Also ensures that it does not have consecutive dots(.)
        /// 
        /// 
        /// true if the path is valid, else fail
        private bool IsValidPath(string yamlPath)
        {
            if (string.IsNullOrWhiteSpace(yamlPath))
                return false;
            return !yamlPath.Split('.').Any(string.IsNullOrWhiteSpace);
        }
        private T GetDeserializedToken(object token)
        {
            if (token is T deserializedToken)
            {
                return deserializedToken;
            }
            return _deserializer.Deserialize(_serializer.Serialize(token));
        }
        public IList GetKeys(string path)
        {
            try
            {
                return GetToken>(path).Keys.ToList();
            }
            catch (Exception ex)
            {
                throw new InvalidOperationException($"Unable to retrieve keys for the specified YAML path '{path}'.", ex);
            }
        }
    }
}