﻿using System;
using System.Collections.Generic;
using System.Linq;

namespace UniJSON
{
    public class JsonSchema : IEquatable<JsonSchema>
    {
        public string Schema; // http://json-schema.org/draft-04/schema

        #region Annotations
        string m_title;
        public string Title
        {
            get { return m_title; }
            private set
            {
                if (value == null)
                {
                    m_title = "";
                }
                else
                {
                    m_title = value.Trim();
                }
            }
        }

        string m_desc;
        public string Description
        {
            get { return m_desc; }
            private set
            {
                if (value == null)
                {
                    m_desc = "";
                }
                else
                {
                    m_desc = value.Trim();
                }
            }
        }

        public object Default
        {
            get;
            private set;
        }
        #endregion

        public IJsonSchemaValidator Validator { get; set; }

        /// <summary>
        /// Skip validator comparison
        /// </summary>
        public bool SkipComparison { get; set; }

        public object ExplicitIgnorableValue { private get; set; }
        public int ExplicitIgnorableItemLength { private get; set; }

        public override string ToString()
        {
            return string.Format("<{0}>", Title);
        }

        public override int GetHashCode()
        {
            return 1;
        }

        public override bool Equals(object obj)
        {
            var rhs = obj as JsonSchema;
            if (rhs == null) return false;
            return Equals(rhs);
        }

        public bool Equals(JsonSchema rhs)
        {
            // skip comparison
            if (SkipComparison) return true;
            if (rhs.SkipComparison) return true;
            return Validator.Equals(rhs.Validator);
        }

        public static bool operator ==(JsonSchema obj1, JsonSchema obj2)
        {
            if (ReferenceEquals(obj1, obj2))
            {
                return true;
            }

            if (ReferenceEquals(obj1, null))
            {
                return false;
            }

            if (ReferenceEquals(obj2, null))
            {
                return false;
            }

            return obj1.Equals(obj2);
        }

        public static bool operator !=(JsonSchema obj1, JsonSchema obj2)
        {
            return !(obj1 == obj2);
        }

        #region FromType
        public static JsonSchema FromType<T>()
        {
            return FromType(typeof(T), null, null);
        }

        public static JsonSchema FromType(Type t,
            BaseJsonSchemaAttribute a = null, // field attribute
            ItemJsonSchemaAttribute ia = null
            )
        {
            // class attribute
            var aa = t.GetCustomAttributes(typeof(JsonSchemaAttribute), true)
                .FirstOrDefault() as JsonSchemaAttribute;
            if (a != null)
            {
                a.Merge(aa);
            }
            else
            {
                if (aa == null)
                {
                    a = new JsonSchemaAttribute();
                }
                else
                {
                    a = aa;
                }
            }

            if (ia == null)
            {
                ia = t.GetCustomAttributes(typeof(ItemJsonSchemaAttribute), true)
                    .FirstOrDefault() as ItemJsonSchemaAttribute;
            }

            IJsonSchemaValidator validator = null;
            bool skipComparison = a.SkipSchemaComparison;
            if (t == typeof(object))
            {
                skipComparison = true;
            }

            if (a.EnumValues != null)
            {
                try
                {
                    validator = JsonEnumValidator.Create(a.EnumValues, a.EnumSerializationType);
                }
                catch (Exception)
                {
                    throw new Exception(String.Join(", ", a.EnumValues.Select(x => x.ToString()).ToArray()));
                }
            }
            else if (t.IsEnum)
            {
                validator = JsonEnumValidator.Create(t, a.EnumSerializationType, a.EnumExcludes);
            }
            else
            {
                validator = JsonSchemaValidatorFactory.Create(t, a, ia);
            }

            var schema = new JsonSchema
            {
                Title = a.Title,
                Description = a.Description,
                Validator = validator,
                SkipComparison = skipComparison,
                ExplicitIgnorableValue = a.ExplicitIgnorableValue,
                ExplicitIgnorableItemLength = a.ExplicitIgnorableItemLength,
            };

            return schema;
        }
        #endregion

        #region FromJson
        static ValueNodeType ParseValueType(string type)
        {
            try
            {
                return (ValueNodeType)Enum.Parse(typeof(ValueNodeType), type, true);
            }
            catch (ArgumentException)
            {
                throw new ArgumentException(string.Format("unknown type: {0}", type));
            }
        }

        Stack<string> m_context = new Stack<string>();

        static Utf8String s_ref = Utf8String.From("$ref");

        public void Parse(IFileSystemAccessor fs, ListTreeNode<JsonValue> root, string Key)
        {
            m_context.Push(Key);

            var compositionType = default(CompositionType);
            var composition = new List<JsonSchema>();
            foreach (var kv in root.ObjectItems())
            {
                switch (kv.Key.GetString())
                {
                    case "$schema":
                        Schema = kv.Value.GetString();
                        break;

                    case "$ref":
                        {
                            var refFs = fs.Get(kv.Value.GetString());

                            // parse JSON
                            var json = refFs.ReadAllText();
                            var refRoot = JsonParser.Parse(json);

                            Parse(refFs, refRoot, "$ref");
                        }
                        break;

                    #region Annotation
                    case "title":
                        Title = kv.Value.GetString();
                        break;

                    case "description":
                        Description = kv.Value.GetString();
                        break;

                    case "default":
                        Default = kv.Value;
                        break;
                    #endregion

                    #region Validation
                    // http://json-schema.org/latest/json-schema-validation.html#rfc.section.6.1
                    case "type":
                        if (Validator == null)
                        {
                            Validator = JsonSchemaValidatorFactory.Create(kv.Value.GetString());
                        }
                        break;

                    case "enum":
                        Validator = JsonEnumValidator.Create(kv.Value);
                        break;

                    case "const":
                        break;
                    #endregion

                    #region Composite
                    // http://json-schema.org/latest/json-schema-validation.html#rfc.section.6.7
                    case "oneOf":
                        break;

                    case "not":
                        break;

                    case "anyOf": // composition
                    case "allOf": // composition
                        {
                            compositionType = (CompositionType)Enum.Parse(typeof(CompositionType), kv.Key.GetString(), true);
                            foreach (var item in kv.Value.ArrayItems())
                            {
                                if (item.ContainsKey(s_ref))
                                {
                                    var sub = JsonSchema.ParseFromPath(fs.Get(item[s_ref].GetString()));
                                    composition.Add(sub);
                                }
                                else
                                {
                                    var sub = new JsonSchema();
                                    sub.Parse(fs, item, compositionType.ToString());
                                    composition.Add(sub);
                                }
                            }
                            Composite(compositionType, composition);
                        }
                        break;
                    #endregion

                    // http://json-schema.org/latest/json-schema-validation.html#rfc.section.7
                    case "format":
                        break;

                    #region Gltf
                    case "gltf_detailedDescription":
                        break;

                    case "gltf_webgl":
                        break;

                    case "gltf_uriType":
                        break;
                    #endregion

                    default:
                        {
                            if (Validator != null)
                            {
                                if (Validator.FromJsonSchema(fs, kv.Key.GetString(), kv.Value))
                                {
                                    continue;
                                }
                            }
                            throw new NotImplementedException(string.Format("unknown key: {0}", kv.Key));
                        }
                }
            }
            m_context.Pop();

            if (Validator == null)
            {
                SkipComparison = true;
            }
        }

        void Composite(CompositionType compositionType, List<JsonSchema> composition)
        {
            switch (compositionType)
            {
                case CompositionType.AllOf:
                    if (composition.Count == 1)
                    {
                        // inheritance
                        if (Validator == null)
                        {
                            //Validator = JsonSchemaValidatorFactory.Create(composition[0].Validator.ValueNodeType);
                            Validator = composition[0].Validator;
                        }
                        else
                        {
                            Validator.Merge(composition[0].Validator);
                        }
                    }
                    else
                    {
                        throw new NotImplementedException();
                    }
                    break;

                case CompositionType.AnyOf:
                    if (Validator == null)
                    {
                        if (composition.Count == 1)
                        {
                            throw new NotImplementedException();
                            //Validator = composition[0].Validator;
                        }
                        else
                        {
                            // extend enum
                            // enum, enum..., type
                            Validator = JsonEnumValidator.Create(composition, EnumSerializationType.AsString);
                        }
                    }
                    //throw new NotImplementedException();
                    break;

                default:
                    throw new NotImplementedException();
            }
        }

        public static JsonSchema ParseFromPath(IFileSystemAccessor fs)
        {
            // parse JSON
            var json = fs.ReadAllText();
            var root = JsonParser.Parse(json);

            // create schema
            var schema = new JsonSchema();
            schema.Parse(fs, root, "__ParseFromPath__" + fs.ToString());
            return schema;
        }
        #endregion

        public void Serialize<T>(IFormatter f, T o, JsonSchemaValidationContext c = null)
        {
            if (c == null)
            {
                c = new JsonSchemaValidationContext(o)
                {
                    EnableDiagnosisForNotRequiredFields = true,
                };
            }

            var ex = Validator.Validate(c, o);
            if (ex != null)
            {
                throw ex;
            }

            Validator.Serialize(f, c, o);
        }

        public void ToJson(IFormatter f)
        {
            f.BeginMap(2);
            if (!string.IsNullOrEmpty(Title)) { f.Key("title"); f.Value(Title); }
            if (!string.IsNullOrEmpty(Description)) { f.Key("description"); f.Value(Description); }
            Validator.ToJsonSchema(f);
            f.EndMap();
        }

        public bool IsExplicitlyIgnorableValue<T>(T obj)
        {
            if (obj == null)
            {
                return ExplicitIgnorableValue == null;
            }

            var iter = obj as System.Collections.ICollection;
            if (ExplicitIgnorableItemLength != -1 && iter != null)
            {
                return iter.Count == ExplicitIgnorableItemLength;
            }

            return obj.Equals(ExplicitIgnorableValue);
        }
    }

    public static class JsonSchemaExtensions
    {
        public static string Serialize<T>(this JsonSchema s, T o, JsonSchemaValidationContext c = null)
        {
            var f = new JsonFormatter();
            s.Serialize(f, o, c);
            return f.ToString();
        }
    }
}
