using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json.Serialization;
using System.Text.Json;
using Quik.FreeType;
using Quik.Media.Font;
using Quik.PAL;
using Quik.Media.Defaults.Linux;

namespace Quik.Media.Defaults.Fallback
{
    public class FallbackFontDatabase : IFontDataBase
    {
        private readonly string DbPath =
            Environment.GetEnvironmentVariable(EnvironmentVariables.FallbackFontDatabase) ??
            Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "QUIK/fontdb.json");

        private Dictionary<FontFace, FileInfo> FilesMap { get; } = new Dictionary<FontFace, FileInfo>();
        private Dictionary<string, List<FontFace>> ByFamily { get; } = new Dictionary<string, List<FontFace>>();
        private Dictionary<SystemFontFamily, FontFace> SystemFonts { get; } = new Dictionary<SystemFontFamily, FontFace>();
        private List<FontFace> All { get; } = new List<FontFace>();

        IEnumerable<FontFace> IFontDataBase.All => this.All;

        public FallbackFontDatabase(bool rebuild = false)
        {
            // Load existing database if desired.
            List<DbEntry> database;

            if(!rebuild)
            {
                database = LoadDatabase();
            }
            else
            {
                database = new List<DbEntry>();
            }

            VerifyDatabase(database);
            FlushDatabase(database);

            (FontFace, FileInfo) serif      = FontDataBaseProvider.ResolveSystemFont(EnvironmentVariables.SerifFont, LinuxFonts.DefaultSerifFamilies, this);
            SystemFonts[SystemFontFamily.Serif] = serif.Item1;
            (FontFace, FileInfo) sans       = FontDataBaseProvider.ResolveSystemFont(EnvironmentVariables.SansFont, LinuxFonts.DefaultSansFamilies, this);
            SystemFonts[SystemFontFamily.Sans] = sans.Item1;
            (FontFace, FileInfo) mono       = FontDataBaseProvider.ResolveSystemFont(EnvironmentVariables.MonospaceFont, LinuxFonts.DefaultMonospaceFamilies, this);
            SystemFonts[SystemFontFamily.Monospace] = mono.Item1;
            (FontFace, FileInfo) cursive    = FontDataBaseProvider.ResolveSystemFont(EnvironmentVariables.CursiveFont, LinuxFonts.DefaultCursiveFamilies, this);
            SystemFonts[SystemFontFamily.Cursive] = cursive.Item1;
            (FontFace, FileInfo) fantasy    = FontDataBaseProvider.ResolveSystemFont(EnvironmentVariables.FantasyFont, LinuxFonts.DefaultFantasyFamilies, this);
            SystemFonts[SystemFontFamily.Fantasy] = fantasy.Item1;

            AddFont(serif.Item1, serif.Item2);
            AddFont(sans.Item1, sans.Item2);
            AddFont(mono.Item1, mono.Item2);
            AddFont(cursive.Item1, cursive.Item2);
            AddFont(fantasy.Item1, fantasy.Item2);

            database.ForEach(x => AddFont(x.Face, new FileInfo(x.FilePath)));
        }

        public FileInfo FontFileInfo(FontFace face)
        {
            if (FilesMap.TryGetValue(face, out FileInfo info))
                return info;
            else
                return null;
        }

        public Stream Open(FontFace face)
        {
            return FontFileInfo(face)?.OpenRead() ?? throw new FileNotFoundException();
        }

        public IEnumerable<FontFace> Search(FontFace prototype, FontMatchCriteria criteria = FontMatchCriteria.All)
        {
            // A bit scuffed and LINQ heavy but it should work.
            IEnumerable<FontFace> candidates;
            
            if (criteria.HasFlag(FontMatchCriteria.Family))
            {
                List<FontFace> siblings;
                
                if (!ByFamily.TryGetValue(prototype.Family, out siblings))
                {
                    return Enumerable.Empty<FontFace>();
                }

                candidates = siblings;
            }
            else
            {
                candidates = All;
            }

            return 
                candidates
                    .Where(x =>
                        implies(criteria.HasFlag(FontMatchCriteria.Slant), prototype.Slant == x.Slant) ||
                        implies(criteria.HasFlag(FontMatchCriteria.Weight), prototype.Weight == x.Weight) ||
                        implies(criteria.HasFlag(FontMatchCriteria.Stretch), prototype.Stretch == x.Stretch)
                    )
                    .OrderByDescending(x =>

                        (prototype.Slant == x.Slant ? 1 : 0) +
                        (prototype.Weight == x.Weight ? 1 : 0) +
                        (prototype.Stretch == x.Stretch ? 1 : 0) +
                        confidence(prototype.Family, x.Family) * 3
                    );

            // a => b = a'+b
            static bool implies(bool a, bool b)
            {
                return !a || b;
            }

            static int confidence(string target, string testee)
            {
                int i;
                for (i = 0; i < target.Length && i < testee.Length && target[i] == testee[i]; i++);
                return i;
            }
        }

        public FontFace GetSystemFontFace(SystemFontFamily family)
        {
            return SystemFonts[family];
        }

        private void AddFont(FontFace face, FileInfo file)
        {
            if (!All.Contains(face))
                All.Add(face);

            FilesMap.TryAdd(face, file);

            if (!ByFamily.TryGetValue(face.Family, out List<FontFace> siblings))
            {
                siblings = new List<FontFace>();
                ByFamily.Add(face.Family, siblings);
            }

            if (!siblings.Contains(face))
                siblings.Add(face);
        }

        private List<DbEntry> LoadDatabase()
        {
            FileInfo info = new FileInfo(DbPath);

            if (!info.Exists)
                return new List<DbEntry>();

            using Stream str = info.OpenRead();
            try 
            {
                return JsonSerializer.Deserialize<List<DbEntry>>(str);
            }
            catch
            {
                return new List<DbEntry>();
            }
        }

        private void VerifyDatabase(List<DbEntry> db)
        {
            // Very slow way to do this but how many fonts could a system have on average?
            Dictionary<string, DbEntry> entires = new Dictionary<string, DbEntry>();

            foreach (DbEntry entry in db)
            {
                FileInfo info = new FileInfo(entry.FilePath);

                // Reprocess fonts that appear like this.
                if (!info.Exists) continue;
                else if (info.LastWriteTime > entry.AccessTime) continue;
            }

            string fontpath = null;
            try
            {
                fontpath = Environment.GetFolderPath(Environment.SpecialFolder.Fonts);
                if (string.IsNullOrEmpty(fontpath))
                    throw new Exception();
            }
            catch
            {
                foreach (string path in FontPaths)
                {
                    if (Directory.Exists(path))
                    {
                        fontpath = path;
                        break;
                    }
                }

                // rip
                if (string.IsNullOrEmpty(fontpath))
                    return;
            }

            SearchPathForFonts(entires, fontpath);

            db.Clear();
            db.AddRange(entires.Values);
        }

        private static void SearchPathForFonts(Dictionary<string, DbEntry> entries, string path)
        {
            DirectoryInfo dir = new DirectoryInfo(path);

            foreach (FileInfo file in dir.EnumerateFiles())
            {
                SearchFileForFonts(entries, file);
            }

            foreach (DirectoryInfo directory in dir.EnumerateDirectories())
            {
                SearchPathForFonts(entries, directory.FullName);
            }
        }

        private static void SearchFileForFonts(Dictionary<string, DbEntry> entries, FileInfo file)
        {
            if (entries.ContainsKey(file.FullName))
                return;

            if (FT.NewFace(FTProvider.Ft, file.FullName, 0, out FTFace face) != FTError.None)
                return;

            FontFace facename = FontFace.Parse(face.FamilyName, face.StyleName);

            DbEntry entry = new DbEntry(facename, file.FullName);
            entries.Add(file.FullName, entry);
            FT.DoneFace(face);
        }

        private void FlushDatabase(List<DbEntry> db)
        {
            FileInfo info = new FileInfo(DbPath);
            Directory.CreateDirectory(Path.GetDirectoryName(DbPath));
            using Stream str = info.OpenWrite();
            JsonSerializer.Serialize(str, db);
        }

        private static readonly string[] FontPaths = new string[] {
            "/usr/share/fonts",
        };

        [JsonSerializable(typeof(DbEntry))]
        private class DbEntry
        {
            [JsonIgnore] public FontFace Face => new FontFace(Family, Slant, Weight, Stretch);
            public string Family { get; set; }
            public FontSlant Slant { get; set; }
            public FontWeight Weight { get; set; }
            public FontStretch Stretch { get; set; }
            public string FilePath { get; set; }
            public DateTime AccessTime { get; set; }

            public DbEntry() {}
            public DbEntry(FontFace face, string path)
            {
                Family = face.Family;
                Slant = face.Slant;
                Weight = face.Weight;
                Stretch = face.Stretch;
                FilePath = path;
                AccessTime = DateTime.Now;
            }
        }
    }
}