using System.Collections.Generic;
using System.Reflection;
using System.IO;
using System;
using System.Linq;
using Newtonsoft.Json;
using JsonSubTypes;
using System.Collections;
using UnityEngine;
using Newtonsoft.Json.Linq;
using static CommunicationEvents;

// I would go for static virtual methods, but C#9 does not allow me...
// static methods cannot be overwritten -> virtual
public interface IJSONsavable<T> where T : IJSONsavable<T>, new()
{
    // stand-in for non static methods
    public static readonly IJSONsavable<T> Instance = new T();
    public static readonly FieldInfo[] JsonSeperateFields =
         typeof(T)
        .GetFields(
            BindingFlags.Instance |
            BindingFlags.Public |
            BindingFlags.NonPublic |
            BindingFlags.Static )
        .Where((field)
            => field.GetCustomAttributes().Any((attribute) 
                => attribute.GetType() == typeof(JSONManager.JsonSeparateAttribute))
            && field.FieldType.GetInterfaces().Any((inter) 
                => inter.IsGenericType && inter.GetGenericTypeDefinition() == typeof(IJSONsavable<>)))
        .ToArray();


    // TODO: this?
    public string name { get; set; }
    public string path { get; set; }
    protected static List<Directories>
        hierarchie = new List<Directories> { Directories.misc };

    #region OverridableMethods

    public virtual string _IJGetName(string name) => name;
    public virtual List<Directories> _IJGetHierarchie(List<Directories> hierarchie_base)
    {
        hierarchie_base ??= new List<Directories>();
        return hierarchie_base.Concat(hierarchie).ToList();
    }
    public virtual bool _IJGetRawObject(out T payload, string path) => JSONManager.ReadFromJsonFile<T>(out payload, path);
    public virtual T _IJPreProcess(T payload) => payload;
    public virtual T _IJPostProcess(T payload) => payload;

    #endregion OverridableMethods


    #region MethodTemplates

    public bool store(List<Directories> hierarchie, string name, bool use_install_folder = false, bool overwrite = true, bool deep_store = true)
        => store(hierarchie, name, (T) this, use_install_folder, overwrite, deep_store);

    public static bool store(List<Directories> hierarchie, string name, T payload, bool use_install_folder = false, bool overwrite = true, bool deep_store = true)
    {
        var new_hierarchie = 
            Instance._IJGetHierarchie(hierarchie);
        var new_name = 
            Instance._IJGetName(name);

        string path = CreatePathToFile(out bool exists, new_name, "JSON", new_hierarchie, use_install_folder);
        if (exists && !overwrite)
            return false;

        // store fields decorated with JsonSeparateAttribute and implementing IJSONsavable<> separately
        if (deep_store
            && !store_children(hierarchie, name, payload, use_install_folder: false, overwrite, deep_store: true))
                return false;

        // store this
        string path_o = payload.path;
        payload.path = path;

        var new_payload =
            Instance._IJPreProcess(payload);

        payload.path = path_o;

        JSONManager.WriteToJsonFile(path, new_payload);
        return true;
    }
    
    public bool store_children(List<Directories> hierarchie, string name, bool use_install_folder = false, bool overwrite = true, bool deep_store = true)
        => store_children(hierarchie, name, (T) this, use_install_folder, overwrite, deep_store);

    public static bool store_children(List<Directories> hierarchie, string name, T payload, bool use_install_folder = false, bool overwrite = true, bool deep_store = true)
    {
        var new_hierarchie =
            Instance._IJGetHierarchie(hierarchie);
        var new_name =
            Instance._IJGetName(name);

        for ((int max_i, bool success) = (0, true); max_i < JsonSeperateFields.Count(); max_i++)
        {
            var field = JsonSeperateFields[max_i];
            dynamic save_me = field.GetValue(payload); // is S:IJSONsavable<S>

            Type interface_type = typeof(IJSONsavable<>).MakeGenericType(field.FieldType);
            Type[] store_args_type = new Type[] { typeof(List<Directories>), typeof(string), field.FieldType, typeof(bool), typeof(bool), typeof(bool) };
            object[] store_args = new object[] { new_hierarchie, new_name, save_me, use_install_folder, overwrite, deep_store };

            var method = interface_type.GetMethod("store", store_args_type);
            success &= (bool)method.Invoke(null, store_args);

            // in case of no success: delete it again
            if (!success)
            {
                delete_children(hierarchie, name, use_install_folder, JsonSeperateFields.Count() - max_i);
                return false;
            }
        }

        return true;
    }

    public static bool load_children(List<Directories> hierarchie, string name, ref T raw_payload, bool use_install_folder = false, bool deep_load = true, bool post_process = true)
    {
        var new_hierarchie =
            Instance._IJGetHierarchie(hierarchie);
        var new_name =
            Instance._IJGetName(name);

        bool success = true;
        for (int max_i = 0; max_i < JsonSeperateFields.Count(); max_i++)
        {
            var field = JsonSeperateFields[max_i];

            Type interface_type = typeof(IJSONsavable<>).MakeGenericType(field.FieldType);
            Type[] load_args_type = new Type[] { typeof(List<Directories>), typeof(string), field.FieldType.MakeByRefType(), typeof(bool), typeof(bool), typeof(bool) };
            object[] load_args = new object[] { new_hierarchie, new_name, null, use_install_folder, deep_load, post_process };

            var method = interface_type.GetMethod("load", BindingFlags.Public | BindingFlags.Static, null, load_args_type, null);
            bool success_i = (bool)method.Invoke(null, load_args);
            
            field.SetValue(raw_payload, success_i ? load_args[2] : Activator.CreateInstance(field.FieldType));
            success &= success_i;
        }

        return success;
    }

    public static bool load(List<Directories> hierarchie, string name, out T payload, bool use_install_folder = false, bool deep_load = true, bool post_process = true)
    {
        payload = default(T);
        bool success = true;

        var new_hierarchie =
            Instance._IJGetHierarchie(hierarchie);
        var new_name =
            Instance._IJGetName(name);

        string path = CreatePathToFile(out bool loadable, new_name, "JSON", new_hierarchie, use_install_folder);
        if (!loadable)
            return false;

        if (!Instance._IJGetRawObject(out T raw_payload, path))
            return false;
        raw_payload.name = new_name;

        // load fields decorated with JsonSeparateAttribute and implementing IJSONsavable<> separately
        if (deep_load
            && !load_children(hierarchie, name, ref raw_payload, false /*use_install_folder*/))
            success = false;

        payload = post_process
            ? Instance._IJPostProcess(raw_payload)
            : raw_payload;

        return success;
    }

    public static void delete_children(List<Directories> hierarchie, string name, bool use_install_folder = false, int skip_last_children = 0)
    {
        var new_hierarchie =
            Instance._IJGetHierarchie(hierarchie);
        var new_name =
            Instance._IJGetName(name);

        for(int i = 0; i < JsonSeperateFields.Count() - skip_last_children; i++) 
        {
            var field = JsonSeperateFields[i];

            Type interface_type = typeof(IJSONsavable<>).MakeGenericType(field.FieldType);
            Type[] delete_args_type = new Type[] { typeof(List<Directories>), typeof(string), typeof(bool) };
            object[] delete_args = new object[] { new_hierarchie, new_name, use_install_folder };

            var method = interface_type.GetMethod("delete", delete_args_type);
            method.Invoke(null, delete_args);
        }
    }

    public static bool delete(List<Directories> hierarchie, string name, bool use_install_folder = false)
    {
        var new_hierarchie =
            Instance._IJGetHierarchie(hierarchie);
        var new_name =
            Instance._IJGetName(name);

        string path = CreatePathToFile(out bool _, new_name, "JSON", new_hierarchie, use_install_folder);
        if (!delete(path))
            return false;

        delete_children(hierarchie, name, use_install_folder);
        return true;
    }

    // does not delete children!
    private static bool delete(string path)
    {
        if (!File.Exists(path))
            return false;

        File.Delete(path);
        return true;
    }

    // public bool delete() => delete(hierarchie, name);

    #endregion MethodTemplates

}

public class MMTURICollection
{
    public string Point = "http://mathhub.info/MitM/core/geometry?3DGeometry?point";
    public string Tuple = "http://gl.mathhub.info/MMT/LFX/Sigma?Symbols?Tuple";
    public string LineType = "http://mathhub.info/MitM/core/geometry?Geometry/Common?line_type";
    public string LineOf = "http://mathhub.info/MitM/core/geometry?Geometry/Common?lineOf";
    public string OnLine = "http://mathhub.info/MitM/core/geometry?Geometry/Common?onLine";
    public string Ded = "http://mathhub.info/MitM/Foundation?Logic?ded";
    public string Eq = "http://mathhub.info/MitM/Foundation?Logic?eq";
    public string Metric = "http://mathhub.info/MitM/core/geometry?Geometry/Common?metric";
    public string Angle = "http://mathhub.info/MitM/core/geometry?Geometry/Common?angle_between";
    public string Sketch = "http://mathhub.info/MitM/Foundation?InformalProofs?proofsketch";
    public string RealLit = "http://mathhub.info/MitM/Foundation?RealLiterals?real_lit";
}

public static class JSONManager 
{
    //could init the strings of MMTURIs with JSON or other settings file instead
    public static MMTURICollection MMTURIs = new MMTURICollection();

    public class URI
    {
        public string uri;

        public URI(string uri)
        {
            this.uri = uri;
        }
    }
    
    [JsonConverter(typeof(JsonSubtypes), "kind")]
    public class MMTTerm
    {
        string kind;
    }

    public class OMA : MMTTerm
    {
        public MMTTerm applicant;
        public List<MMTTerm> arguments;
        public string kind = "OMA";
        public OMA(MMTTerm applicant, List<MMTTerm> arguments)
        {
            this.applicant = applicant;
            this.arguments = arguments;
        }
    }

    public class OMS : MMTTerm
    {
        public string uri;
        public string kind = "OMS";

        public OMS(string uri)
        {
            this.uri = uri;
        }
    }

    public class OMSTR : MMTTerm
    {
        [JsonProperty("float")]
        public string s;
        public string kind = "OMSTR";

        public OMSTR(string s)
        {
            this.s = s;
        }
    }

    public class OMF : MMTTerm
    {
        [JsonProperty("float")]
        public float f;
        public string kind = "OMF";

        public OMF(float f)
        {
            this.f = f;
        }
    }

    public class MMTDeclaration
    {
        public string label;
        public static MMTDeclaration FromJson(string json)
        {
            MMTDeclaration mmtDecl = JsonConvert.DeserializeObject<MMTDeclaration>(json);
            if (mmtDecl.label == null)
                mmtDecl.label = string.Empty;

            return mmtDecl;
        }
        public static string ToJson(MMTDeclaration mmtDecl)
        {
            if (mmtDecl.label == null)
                mmtDecl.label = string.Empty;

            string json = JsonConvert.SerializeObject(mmtDecl);
            return json;
        }
    }

    /**
     * MMTSymbolDeclaration: Class for facts without values, e.g. Points
     */ 
    public class MMTSymbolDeclaration : MMTDeclaration
    {
        public string kind = "general";
        public MMTTerm tp;
        public MMTTerm df;

        /**
         * Constructor used for sending new declarations to mmt
         */
        public MMTSymbolDeclaration(string label, MMTTerm tp, MMTTerm df)
        {
            this.label = label;
            this.tp = tp;
            this.df = df;
        }
    }

    /**
     * MMTValueDeclaration: Class for facts with values, e.g. Distances or Angles
     */
    public class MMTValueDeclaration : MMTDeclaration
    {
        public string kind = "veq";
        public MMTTerm lhs;
        public MMTTerm valueTp;
        public MMTTerm value;

        /**
         * Constructor used for sending new declarations to mmt
         */
        public MMTValueDeclaration(string label, MMTTerm lhs, MMTTerm valueTp, MMTTerm value)
        {
            this.label = label;
            this.lhs = lhs;
            this.valueTp = valueTp;
            this.value = value;
        }
    }

    [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
    public sealed class JsonSeparateAttribute : Attribute
    {
        public JsonSeparateAttribute() { }
    }

    // TODO? /// <para>If there are public properties/variables that you do not want written to the file, decorate them with the [JsonIgnore] attribute.</para>

    /// <summary>
    /// Writes the given object instance to a Json file, recursively to set depth, including all members.
    /// <para>Object type must have a parameterless constructor.</para>
    /// <para>Only All properties and variables will be written to the file. These can be any type though, even other non-abstract classes.</para>
    /// </summary>
    /// <param name="filePath">The file path to write the object instance to.</param>
    /// <param name="objectToWrite">The object instance to write to the file.</param>
    /// <param name="max_depth">The depth recursion will occur. Default = 0.</param>
    public static bool WriteToJsonFile(string filePath, object objectToWrite, int max_depth = 0)
    {
        // This tells your serializer that multiple references are okay.
        var settings = new JsonSerializerSettings
        {
            ReferenceLoopHandling = ReferenceLoopHandling.Ignore
        };

        TextWriter writer = null;
        try
        {
            string payload = JsonConvert.SerializeObject(objectToWrite, settings);
            writer = new StreamWriter(filePath);
            writer.Write(payload);
            return true;
        }
        catch (Exception e)
        {
            Debug.LogError(e);
            return false;
        }
        finally
        {
            if (writer != null)
                writer.Close();
        }
    }

    /// <summary>
    /// Reads an object instance from an Json file.
    /// <para>Object type must have a parameterless constructor.</para>
    /// </summary>
    /// <typeparam name="T">The type of object to read from the file.</typeparam>
    /// <param name="filePath">The file path to read the object instance from.</param>
    /// <returns>Returns a new instance of the object read from the Json file.</returns>
    public static bool ReadFromJsonFile<T>(out T payload, string filePath) where T : new()
    {
        payload = default(T);
        TextReader reader = null;

        try
        {
            reader = new StreamReader(filePath);
            var fileContents = reader.ReadToEnd();
            payload = JsonConvert.DeserializeObject<T>(fileContents);
            return true;
        }
        catch (Exception e)
        {
            Debug.LogError(e);
            return false;
        }
        finally
        {
            if (reader != null)
                reader.Close();
        }
    }
}