// Copyright (c) Wojciech Figat. All rights reserved.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using Flax.Build.NativeCpp;

namespace Flax.Build.Bindings
{
    internal interface IBindingsCache
    {
        void Write(BinaryWriter writer);
        void Read(BinaryReader reader);
    }

    partial class BindingsGenerator
    {
        private static readonly Dictionary<string, Type> TypeCache = new Dictionary<string, Type>();
        private const int CacheVersion = 23;

        internal static void Write(BinaryWriter writer, string e)
        {
            var valid = e != null;
            writer.Write(valid);
            if (valid)
                writer.Write(e);
        }

        internal static void Write(BinaryWriter writer, string[] list)
        {
            if (list != null)
            {
                writer.Write(list.Length);
                for (int i = 0; i < list.Length; i++)
                    writer.Write(list[i]);
            }
            else
            {
                writer.Write(0);
            }
        }

        internal static void Write(BinaryWriter writer, Dictionary<string, string> map)
        {
            if (map != null)
            {
                writer.Write(map.Count);
                foreach (var e in map)
                {
                    writer.Write(e.Key);
                    writer.Write(e.Value);
                }
            }
            else
            {
                writer.Write(0);
            }
        }

        internal static void Write(BinaryWriter writer, HashSet<string> list)
        {
            if (list != null)
            {
                writer.Write(list.Count());
                foreach (var e in list)
                    writer.Write(e);
            }
            else
            {
                writer.Write(0);
            }
        }

        internal static void Write(BinaryWriter writer, List<string> list)
        {
            if (list != null)
            {
                writer.Write(list.Count());
                foreach (var e in list)
                    writer.Write(e);
            }
            else
            {
                writer.Write(0);
            }
        }

        internal static void Write(BinaryWriter writer, IEnumerable<string> list)
        {
            if (list != null)
            {
                writer.Write(list.Count());
                foreach (var e in list)
                    writer.Write(e);
            }
            else
            {
                writer.Write(0);
            }
        }

        internal static void Write<T>(BinaryWriter writer, T e) where T : IBindingsCache
        {
            if (e != null)
            {
                writer.Write(e.GetType().FullName);
                e.Write(writer);
            }
            else
            {
                writer.Write(string.Empty);
            }
        }

        internal static void Write<T>(BinaryWriter writer, List<T> list) where T : IBindingsCache
        {
            if (list != null)
            {
                var count = list.Count;
                writer.Write(count);
                for (int i = 0; i < count; i++)
                {
                    var e = list[i];
                    writer.Write(e.GetType().FullName);
                    e.Write(writer);
                }
            }
            else
            {
                writer.Write(0);
            }
        }

        internal static void Write<T>(BinaryWriter writer, T[] list) where T : IBindingsCache
        {
            if (list != null)
            {
                var count = list.Length;
                writer.Write(count);
                for (int i = 0; i < count; i++)
                {
                    var e = list[i];
                    writer.Write(e.GetType().FullName);
                    e.Write(writer);
                }
            }
            else
            {
                writer.Write(0);
            }
        }

        internal static string Read(BinaryReader reader, string e)
        {
            var valid = reader.ReadBoolean();
            if (valid)
                e = reader.ReadString();
            return e;
        }

        internal static string[] Read(BinaryReader reader, string[] list)
        {
            var count = reader.ReadInt32();
            if (count != 0)
            {
                list = new string[count];
                for (int i = 0; i < count; i++)
                    list[i] = reader.ReadString();
            }
            return list;
        }

        internal static Dictionary<string, string> Read(BinaryReader reader, Dictionary<string, string> map)
        {
            var count = reader.ReadInt32();
            if (count != 0)
            {
                map = new Dictionary<string, string>();
                for (int i = 0; i < count; i++)
                {
                    var key = reader.ReadString();
                    var value = reader.ReadString();
                    map.Add(key, value);
                }
            }
            return map;
        }

        internal static T Read<T>(BinaryReader reader, T e) where T : IBindingsCache
        {
            var typename = reader.ReadString();
            if (string.IsNullOrEmpty(typename))
                return e;
            if (!TypeCache.TryGetValue(typename, out var type))
            {
                type = Builder.BuildTypes.FirstOrDefault(x => x.FullName == typename);
                if (type == null)
                {
                    var msg = $"Missing type {typename}.";
                    Log.Error(msg);
                    throw new Exception(msg);
                }
                TypeCache.Add(typename, type);
            }
            e = (T)Activator.CreateInstance(type);
            e.Read(reader);
            return e;
        }

        internal static List<T> Read<T>(BinaryReader reader, List<T> list) where T : IBindingsCache
        {
            var count = reader.ReadInt32();
            if (count != 0)
            {
                list = new List<T>(count);
                for (int i = 0; i < count; i++)
                {
                    var typename = reader.ReadString();
                    if (!TypeCache.TryGetValue(typename, out var type))
                    {
                        type = Builder.BuildTypes.FirstOrDefault(x => x.FullName == typename);
                        if (type == null)
                        {
                            var msg = $"Missing type {typename}.";
                            Log.Error(msg);
                            throw new Exception(msg);
                        }
                        TypeCache.Add(typename, type);
                    }
                    var e = (T)Activator.CreateInstance(type);
                    e.Read(reader);
                    list.Add(e);
                }
            }
            return list;
        }

        private static string GetCachePath(Module module, BuildOptions moduleOptions)
        {
            return Path.Combine(moduleOptions.IntermediateFolder, module.Name + ".Bindings.cache");
        }

        private static void SaveCache(ModuleInfo moduleInfo, BuildOptions moduleOptions, List<string> headerFiles)
        {
            if (!Directory.Exists(moduleOptions.IntermediateFolder))
                return;
            var path = GetCachePath(moduleInfo.Module, moduleOptions);
            using (var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
            using (var writer = new BinaryWriter(stream, Encoding.UTF8))
            {
                // Version
                writer.Write(CacheVersion);
                writer.Write(FileCache.GetLastWriteTime(Assembly.GetExecutingAssembly().Location).Ticks);

                // Build options
                writer.Write(moduleOptions.IntermediateFolder);
                writer.Write((int)moduleOptions.Platform.Target);
                writer.Write((int)moduleOptions.Architecture);
                writer.Write((int)moduleOptions.Configuration);
                Write(writer, moduleOptions.PublicDefinitions);
                Write(writer, moduleOptions.PrivateDefinitions);
                Write(writer, moduleOptions.CompileEnv.PreprocessorDefinitions);

                // Header files
                writer.Write(headerFiles.Count);
                for (int i = 0; i < headerFiles.Count; i++)
                {
                    var headerFile = headerFiles[i];
                    writer.Write(headerFile);
                    writer.Write(FileCache.GetLastWriteTime(headerFile).Ticks);
                }

                // Info
                moduleInfo.Write(writer);
            }
        }

        private static bool LoadCache(ref ModuleInfo moduleInfo, BuildOptions moduleOptions, List<string> headerFiles)
        {
            var path = GetCachePath(moduleInfo.Module, moduleOptions);
            if (!FileCache.Exists(path))
                return false;
            try
            {
                using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
                using (var reader = new BinaryReader(stream, Encoding.UTF8))
                {
                    // Version
                    var version = reader.ReadInt32();
                    if (version != CacheVersion)
                        return false;
                    if (FileCache.GetLastWriteTime(Assembly.GetExecutingAssembly().Location).Ticks != reader.ReadInt64())
                        return false;

                    // Build options
                    if (reader.ReadString() != moduleOptions.IntermediateFolder ||
                        reader.ReadInt32() != (int)moduleOptions.Platform.Target ||
                        reader.ReadInt32() != (int)moduleOptions.Architecture ||
                        reader.ReadInt32() != (int)moduleOptions.Configuration)
                        return false;
                    var publicDefinitions = Read(reader, Utilities.GetEmptyArray<string>());
                    if (publicDefinitions.Length != moduleOptions.PublicDefinitions.Count || publicDefinitions.Any(x => !moduleOptions.PublicDefinitions.Contains(x)))
                        return false;
                    var privateDefinitions = Read(reader, Utilities.GetEmptyArray<string>());
                    if (privateDefinitions.Length != moduleOptions.PrivateDefinitions.Count || privateDefinitions.Any(x => !moduleOptions.PrivateDefinitions.Contains(x)))
                        return false;
                    var preprocessorDefinitions = Read(reader, Utilities.GetEmptyArray<string>());
                    if (preprocessorDefinitions.Length != moduleOptions.CompileEnv.PreprocessorDefinitions.Count || preprocessorDefinitions.Any(x => !moduleOptions.CompileEnv.PreprocessorDefinitions.Contains(x)))
                        return false;

                    // Header files
                    var headerFilesCount = reader.ReadInt32();
                    if (headerFilesCount != headerFiles.Count)
                        return false;
                    for (int i = 0; i < headerFilesCount; i++)
                    {
                        var headerFile = headerFiles[i];
                        if (headerFile != reader.ReadString())
                            return false;
                        if (FileCache.GetLastWriteTime(headerFile).Ticks > reader.ReadInt64())
                            return false;
                    }

                    // Info
                    var newModuleInfo = new ModuleInfo
                    {
                        Module = moduleInfo.Module,
                        Name = moduleInfo.Name,
                        Namespace = moduleInfo.Namespace,
                        IsFromCache = true,
                    };
                    newModuleInfo.Read(reader);

                    // Skip parsing and use data loaded from cache
                    moduleInfo = newModuleInfo;
                    return true;
                }
            }
            catch
            {
                // Skip loading cache
                return false;
            }
        }
    }
}
