// 
// Copyright (c) 2013-2016 Carsten Sonne Larsen <cs@innolan.dk>
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Ntp.Analyzer.Config.Node;
using Ntp.Analyzer.Config.Syntax.Setting;
using Ntp.Analyzer.Config.Table;

namespace Ntp.Analyzer.Config.Syntax
{
    public abstract class SyntaxNode<T> : ISyntaxNode
        where T : ConfigurationNode
    {
        protected SyntaxNode(Symbol symbol, string name, int line, bool requirePath = false)
        {
            Symbol = symbol;
            Name = name;
            Line = line;
            RequirePath = requirePath;
            compiledNode = null;
            Nodes = new List<ISyntaxNode>();
            errors = new List<string>();
        }

        private readonly List<string> errors;
        private T compiledNode;

        protected List<ISyntaxNode> Nodes { get; }

        protected string Name { get; }

        public Symbol Symbol { get; }

        public bool RequirePath { get; }

        public int Line { get; }

        public bool HasErrors => errors.Count != 0;

        public IEnumerable<string> Errors => errors;

        public ConfigurationNode CompiledNode => compiledNode;

        public void Add(ISyntaxNode node)
        {
            Nodes.Add(node);
        }

        public IEnumerator<ISyntaxNode> GetEnumerator()
        {
            return Nodes.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        public void CompileNode()
        {
            Compile();
        }

        public void Resolve(SymbolTable table)
        {
            InternalResolve(table);
        }

        public void Validate(SymbolTable table)
        {
            ValidateTypes();
            ValidateMandatories();
            ValidateReferences(table);
        }

        public void Assemble(ISyntaxNode node)
        {
            if (compiledNode == null)
                return;

            compiledNode.Parent = node.CompiledNode;
            node.CompiledNode.Assemble();
        }

        public T Compile()
        {
            return compiledNode ?? (compiledNode = InternalCompile());
        }

        protected abstract T InternalCompile();

        #region Validation methods

        /// <summary>
        /// Override to validates the types in this syntax node.
        /// </summary>
        /// <remarks>When overriding this method do not call the base method.</remarks>
        protected virtual void ValidateTypes()
        {
        }

        /// <summary>
        /// Override to validates the mandatory types in this syntax node.
        /// </summary>
        /// <remarks>When overriding this method do not call the base method.</remarks>
        protected virtual void ValidateMandatories()
        {
        }

        /// <summary>
        /// Override to validates the references in this syntax node.
        /// </summary>
        /// <remarks>When overriding this method do not call the base method.</remarks>
        /// <param name="table">Table.</param>
        protected virtual void ValidateReferences(SymbolTable table)
        {
        }

        /// <summary>
        /// OVerride to resolve references to other syntax nodes from this syntax node.
        /// </summary>
        /// <remarks>When overriding this method do not call the base method.</remarks>
        /// <param name="table">Table.</param>
        protected virtual void InternalResolve(SymbolTable table)
        {
        }

        #endregion

        #region Validation helpers

        protected void CheckTypeIs<TU>(Symbol symbol)
            where TU : SettingNode, new()
        {
            foreach (var node in Nodes.Where(n => n.Symbol == symbol))
            {
                if (node is TU)
                    continue;

                // Build error message
                var b = new StringBuilder();
                if (Line != 0)
                {
                    b.Append("Error in line ");
                    b.Append(Line);
                    b.Append(": ");
                }
                b.Append("Value of setting ");
                b.Append(Keyword.Find(symbol).Name);
                b.Append(" must be ");
                b.Append(new TU().SettingValue);
                errors.Add(b.ToString());
            }
        }

        protected void CheckTypeIs<TU, TV>(Symbol symbol)
            where TU : SettingNode, new()
            where TV : SettingNode, new()
        {
            foreach (var node in Nodes.Where(n => n.Symbol == symbol))
            {
                if (node is TU || node is TV)
                    continue;

                // Build error message
                var b = new StringBuilder();
                if (Line != 0)
                {
                    b.Append("Error in line ");
                    b.Append(Line);
                    b.Append(": ");
                }
                b.Append("Value of setting ");
                b.Append(Keyword.Find(symbol).Name);
                b.Append(" must be ");
                b.Append(new TU().SettingValue);
                b.Append(" or ");
                b.Append(new TV().SettingValue);
                errors.Add(b.ToString());
            }
        }

        protected void CheckAllIsPresent(IEnumerable<Symbol> list)
        {
            var missing = list.
                Where(item => Nodes.Count(n => n.Symbol == item) == 0).
                ToList();

            if (missing.Count == 0)
                return;

            // Build error message
            int i = 0;
            int count = missing.Count;
            var b = new StringBuilder();

            if (Line != 0)
            {
                b.Append("Error in line ");
                b.Append(Line);
                b.Append(": ");
            }
            b.Append(Keyword.Find(Symbol).Name);
            b.Append(" section is missing the setting");
            b.Append(missing.Count > 1 ? "s: " : ": ");

            foreach (var item in missing)
            {
                i++;
                b.Append(Keyword.Find(item).Name);
                if (i == count - 1 && count > 1)
                {
                    b.Append(" and ");
                }
                else if (i != count)
                {
                    b.Append(", ");
                }
                else
                {
                    b.Append(".");
                }
            }

            errors.Add(b.ToString());
        }

        protected void CheckOneIsPresent(IEnumerable<Symbol> list)
        {
            var symbols = list as IList<Symbol> ?? list.ToList();
            var set = new HashSet<Symbol>(symbols);

            if (Nodes.Any(node => set.Contains(node.Symbol)))
                return;

            // Build error message
            int i = 0;
            int count = symbols.Count;
            var b = new StringBuilder();

            if (Line != 0)
            {
                b.Append("Error in line ");
                b.Append(Line);
                b.Append(": ");
            }
            b.Append(Keyword.Find(Symbol).Name);
            b.Append(" section must contain at least one of the setting");
            b.Append(count > 1 ? "s: " : ": ");

            foreach (var item in symbols)
            {
                i++;
                b.Append(Keyword.Find(item).Name);
                if (i == count - 1)
                {
                    b.Append(" or ");
                }
                else if (i != count)
                {
                    b.Append(", ");
                }
                else
                {
                    b.Append(".");
                }
            }

            errors.Add(b.ToString());
        }

        protected void CheckOnlyOneIsPresent(IEnumerable<Symbol> list)
        {
            var symbols = list.ToList();
            var set = new HashSet<Symbol>(symbols);

            int a = Nodes.Count(node => set.Contains(node.Symbol));

            if (a < 2)
                return;

            // Build error message
            int i = 0;
            int count = symbols.Count;
            var b = new StringBuilder();

            if (Line != 0)
            {
                b.Append("Error in line ");
                b.Append(Line);
                b.Append(": ");
            }
            b.Append(Keyword.Find(Symbol).Name);
            b.Append(" section can only contain one of the setting");
            b.Append(count > 1 ? "s: " : ": ");

            foreach (var item in symbols)
            {
                i++;
                b.Append(Keyword.Find(item).Name);
                if (i == count - 1)
                {
                    b.Append(" or ");
                }
                else if (i != count)
                {
                    b.Append(", ");
                }
                else
                {
                    b.Append(".");
                }
            }

            errors.Add(b.ToString());
        }

        protected void CheckIsUnique(IEnumerable<Symbol> list)
        {
            var nonUniques = (
                from symbol in list
                let c = Nodes.Count(n => n.Symbol == symbol)
                where c > 1
                select symbol
                ).ToList();

            if (nonUniques.Count == 0)
                return;

            // Build error message
            int i = 0;
            int count = nonUniques.Count;
            var b = new StringBuilder();

            if (Line != 0)
            {
                b.Append("Error in line ");
                b.Append(Line);
                b.Append(": ");
            }
            b.Append(Keyword.Find(Symbol).Name);
            b.Append(" section has multiple occurrences of the setting");
            b.Append(count > 1 ? "s: " : ": ");

            foreach (var item in nonUniques)
            {
                i++;
                b.Append(Keyword.Find(item).Name);
                if (i == count - 1)
                {
                    b.Append(" and ");
                }
                else if (i != count)
                {
                    b.Append(", ");
                }
                else
                {
                    b.Append(".");
                }
            }

            errors.Add(b.ToString());
        }

        protected Uri CheckLink(string link, string keyword)
        {
            if (link == null)
                return null;

            Uri uri;
            string text = link.TrimStart();

            if ((text.StartsWith("http://") ||
                 text.StartsWith("https://") ||
                 text.StartsWith("ftp://") ||
                 text.StartsWith("maito:") ||
                 text.StartsWith("javascript:")) &&
                Uri.TryCreate(link, UriKind.Absolute, out uri))
                return uri;

            if (Uri.TryCreate("/" + link.TrimStart('/'), UriKind.Relative, out uri))
                return uri;

            if (Uri.TryCreate(link, UriKind.Absolute, out uri))
                return uri;

            var name = Keyword.Find(Symbol).Name;
            var b = new StringBuilder();
            if (Line != 0)
            {
                b.Append("Error in line ");
                b.Append(Line);
                b.Append(": ");
            }
            b.Append($"{keyword} {name} is not a wellformed link.");
            AddError(b.ToString());

            return null;
        }

        #endregion

        #region Error message helpers

        protected void AddReferenceNameError(ISyntaxNode node, string keyword, string name)
        {
            var b = new StringBuilder();
            if (Line != 0)
            {
                b.Append("Error in line ");
                b.Append(node.Line);
                b.Append(": ");
            }
            b.Append($"{keyword} {name} refers to a nonexistent section.");
            AddError(b.ToString());
        }

        protected void AddRefrenceTypeError(ISyntaxNode node, string keyword, string section, string name)
        {
            var b = new StringBuilder();
            if (node.Line != 0)
            {
                b.Append("Error in line ");
                b.Append(node.Line);
                b.Append(": ");
            }
            b.Append($"{keyword} {name} does not refer to a {section} section.");
            AddError(b.ToString());
        }

        protected void AddError(string message)
        {
            errors.Add(message);
        }

        #endregion
    }
}