#nullable enable

using Barotrauma.IO;
using Barotrauma.Extensions;
using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
using System.Globalization;
using System.Text.Unicode;

namespace Barotrauma
{
    public enum FormatCapitals
    {
        Yes, No
    }

    public static class TextManager
    {
        public static bool DebugDraw;

        public readonly static LanguageIdentifier DefaultLanguage = "English".ToLanguageIdentifier();
        public readonly static ConcurrentDictionary<LanguageIdentifier, ImmutableList<TextPack>> TextPacks = new ConcurrentDictionary<LanguageIdentifier, ImmutableList<TextPack>>();
        public static IEnumerable<LanguageIdentifier> AvailableLanguages => TextPacks.Keys;

        private readonly static Dictionary<Identifier, WeakReference<TagLString>> cachedStrings =
            new Dictionary<Identifier, WeakReference<TagLString>>();
        private static ImmutableHashSet<Identifier> nonCacheableTags =
            ImmutableHashSet<Identifier>.Empty;

        public static int LanguageVersion { get; private set; } = 0;

        private readonly static object mutex = new object();

        private static ImmutableArray<Range<int>> UnicodeToIntRanges(params UnicodeRange[] ranges)
            => ranges
                .Select(r => new Range<int>(r.FirstCodePoint, r.FirstCodePoint + r.Length - 1))
                .OrderBy(r => r.Start)
                .ToImmutableArray();

        [Flags]
        public enum SpeciallyHandledCharCategory
        {
            None = 0x0,
            
            CJK = 0x1,
            Cyrillic = 0x2,
            Japanese = 0x4,

            All = 0x7
        }

        public static readonly ImmutableArray<SpeciallyHandledCharCategory> SpeciallyHandledCharCategories
            = Enum.GetValues<SpeciallyHandledCharCategory>()
                .Where(c => c is not (SpeciallyHandledCharCategory.None or SpeciallyHandledCharCategory.All))
                .ToImmutableArray();
        
        private static readonly ImmutableDictionary<SpeciallyHandledCharCategory, ImmutableArray<Range<int>>> SpeciallyHandledCharacterRanges
            = new[]
            {
                (SpeciallyHandledCharCategory.CJK, UnicodeToIntRanges(
                    UnicodeRanges.HalfwidthandFullwidthForms,
                    UnicodeRanges.HangulJamo,
                    UnicodeRanges.HangulCompatibilityJamo,
                    UnicodeRanges.CjkRadicalsSupplement,
                    UnicodeRanges.CjkSymbolsandPunctuation,
                    UnicodeRanges.EnclosedCjkLettersandMonths,
                    UnicodeRanges.CjkCompatibility,
                    UnicodeRanges.CjkUnifiedIdeographsExtensionA,
                    UnicodeRanges.CjkUnifiedIdeographs,
                    UnicodeRanges.HangulSyllables,
                    UnicodeRanges.CjkCompatibilityForms,
                    //not really CJK symbols, but these seem to be present in the CJK fonts but not in the default ones, so we can use them as a fallback
                    UnicodeRanges.BlockElements
                )),
                (SpeciallyHandledCharCategory.Japanese, UnicodeToIntRanges(
                    UnicodeRanges.Hiragana,
                    UnicodeRanges.Katakana
                )),
                (SpeciallyHandledCharCategory.Cyrillic, UnicodeToIntRanges(
                    UnicodeRanges.Cyrillic,
                    UnicodeRanges.CyrillicSupplement,
                    UnicodeRanges.CyrillicExtendedA,
                    UnicodeRanges.CyrillicExtendedB,
                    UnicodeRanges.CyrillicExtendedC
                ))
            }.ToImmutableDictionary();

        private readonly struct CachedCategory
        {
            public readonly SpeciallyHandledCharCategory Category;
            public readonly double LastAccessed;

            public CachedCategory(SpeciallyHandledCharCategory category)
            {
                Category = category;
                LastAccessed = Timing.TotalTime;
            }
        }

        const int SpeciallyHandledCategoriesCacheSize = 5000;
        private static readonly Dictionary<string, CachedCategory> SpeciallyHandledCategoriesCache = new Dictionary<string, CachedCategory>();

        public static SpeciallyHandledCharCategory GetSpeciallyHandledCategories(LocalizedString text)
            => GetSpeciallyHandledCategories(text.Value);
                
        public static SpeciallyHandledCharCategory GetSpeciallyHandledCategories(string text)
        {
            if (string.IsNullOrEmpty(text)) { return SpeciallyHandledCharCategory.None; }

            lock (mutex)
            {
                if (SpeciallyHandledCategoriesCache.TryGetValue(text, out var cachedCategory))
                {
                    SpeciallyHandledCategoriesCache[text] = new CachedCategory(cachedCategory.Category);
                    return cachedCategory.Category;
                }
            }

            var retVal = SpeciallyHandledCharCategory.None;
            for (int i = 0; i < text.Length; i++)
            {
                char chr = text[i];

                foreach (var category in SpeciallyHandledCharCategories)
                {
                    if (retVal.AlreadyHasCategoryFlag(category)) { continue; }
                    
                    for (int j = 0; j < SpeciallyHandledCharacterRanges[category].Length; j++)
                    {
                        var range = SpeciallyHandledCharacterRanges[category][j];

                        // If chr < range.Start, we know that it can't
                        // be in any of the following ranges, so let's
                        // not even bother checking them
                        if (chr < range.Start) { break; }

                        // This character is in a range, set the flag
                        if (range.Contains(chr))
                        {
                            retVal |= category;
                            break;
                        }
                    }
                }

                if (retVal == SpeciallyHandledCharCategory.All)
                {
                    // Input contains characters from all
                    // specially handled categories, there's
                    // no need to inspect the string further
                    break;
                }
            }

            lock (mutex)
            {
                SpeciallyHandledCategoriesCache[text] = new CachedCategory(retVal);
                TrimSpeciallyHandledCategoriesCache();
            }

            return retVal;
        }

        private static void TrimSpeciallyHandledCategoriesCache()
        {
            if (SpeciallyHandledCategoriesCache.Count > SpeciallyHandledCategoriesCacheSize)
            {
                //drop half of the cache, starting from the strings that haven't been used in the longest time

                //this is relatively expensive (instantiates a big new list),
                //but the cache should get cleared so infrequently (when 5000 unique texts have been visible, which may not even happen in normal gameplay)
                //it should not have much effect in practice
                foreach (var cachedVal in SpeciallyHandledCategoriesCache.OrderBy(static c => c.Value.LastAccessed).Take(SpeciallyHandledCategoriesCacheSize / 2).ToList())
                {
                    SpeciallyHandledCategoriesCache.Remove(cachedVal.Key);
                }
            }
        }

        public static bool IsCJK(LocalizedString text)
            => IsCJK(text.Value);

        public static bool IsCJK(string text)
            => GetSpeciallyHandledCategories(text).AlreadyHasCategoryFlag(SpeciallyHandledCharCategory.CJK);
        
        // This is a local optimized version of HasFlag/HasAnyFlag, which makes sense here because the loop using this is big enough
        // to have made 8GB worth of memory allocations with HasFlag and still several dozen MB with the generic HasAnyFlag. 
        private static bool AlreadyHasCategoryFlag(this SpeciallyHandledCharCategory existingFlags, SpeciallyHandledCharCategory categoryFlag)
        {
            return (existingFlags & categoryFlag) != 0;
        }
        
        /// <summary>
        /// Check if the currently selected language is available, and switch to English if not
        /// </summary>
        public static void VerifyLanguageAvailable()
        {
            if (!TextPacks.ContainsKey(GameSettings.CurrentConfig.Language))
            {
                DebugConsole.ThrowError($"Could not find the language \"{GameSettings.CurrentConfig.Language}\". Trying to switch to English...");
                var config = GameSettings.CurrentConfig;
                config.Language = "English".ToLanguageIdentifier();;
                GameSettings.SetCurrentConfig(config);
            }
        }

        public static bool ContainsTag(string tag)
        {
            return ContainsTag(tag.ToIdentifier());
        }

        public static bool ContainsTag(Identifier tag)
        {
            return TextPacks[GameSettings.CurrentConfig.Language].Any(p => p.Texts.ContainsKey(tag));
        }

        public static bool ContainsTag(Identifier tag, LanguageIdentifier language)
        {
            return TextPacks[language].Any(p => p.Texts.ContainsKey(tag));
        }
        public static IEnumerable<string> GetAll(string tag)
            => GetAll(tag.ToIdentifier());

        public static IEnumerable<string> GetAll(Identifier tag)
        {
            var allTexts = TextPacks[GameSettings.CurrentConfig.Language]
                .SelectMany(p => p.Texts.TryGetValue(tag, out var value)
                    ? (IEnumerable<TextPack.Text>)value
                    : Array.Empty<TextPack.Text>()).ToList();

            var firstOverride = allTexts.FirstOrDefault(t => t.IsOverride);
            if (firstOverride != default)
            {
                return allTexts.Where(t => t.IsOverride && t.TextPack == firstOverride.TextPack).Select(t => t.String);
            }
            else
            {
                return allTexts.Select(t => t.String);
            }
        }

        public static IEnumerable<KeyValuePair<Identifier, string>> GetAllTagTextPairs()
        {
            var allTexts = TextPacks[GameSettings.CurrentConfig.Language]
                .SelectMany(p => p.Texts);

            foreach (var textList in allTexts)
            {
                var firstOverride = textList.Value.FirstOrDefault(t => t.IsOverride);
                if (firstOverride != default)
                {
                    //if there's any overrides for this tag, only return the overrides
                    foreach (var text in textList.Value)
                    {
                        if (!text.IsOverride) { continue; }
                        yield return new KeyValuePair<Identifier, string>(textList.Key, text.String);
                    }
                }
                else
                {
                    foreach (var text in textList.Value)
                    {
                        yield return new KeyValuePair<Identifier, string>(textList.Key, text.String);
                    }
                }
            }
        }

        public static IEnumerable<string> GetTextFiles()
        {
            return GetTextFilesRecursive(Path.Combine("Content", "Texts"));
        }

        private static IEnumerable<string> GetTextFilesRecursive(string directory)
        {
            foreach (string file in Directory.GetFiles(directory))
            {
                yield return file.CleanUpPath();
            }
            foreach (string subDir in Directory.GetDirectories(directory))
            {
                foreach (string file in GetTextFilesRecursive(subDir))
                {
                    yield return file;
                }
            }
        }

        public static string GetTranslatedLanguageName(LanguageIdentifier languageIdentifier)
        {
            return TextPacks[languageIdentifier].First().TranslatedName;
        }

        public static void LanguageChanged()
        {
            foreach ((LanguageIdentifier language, ImmutableList<TextPack> textPacks) in TextPacks)
            {
                foreach (TextPack textPack in textPacks)
                {
                    if (GameSettings.CurrentConfig.Language == language)
                    {
                        textPack.VerifyLoaded();
                    }
                    else
                    {
                        textPack.Unload();
                    }
                }
            }
            ClearCache();
        }

        public static void ClearCache()
        {
            lock (cachedStrings)
            {
                cachedStrings.Clear();
                nonCacheableTags.Clear();
            }
        }

        public static LocalizedString Get(params Identifier[] tags)
        {
            if (tags.Length == 1)
            {
                return Get(tags[0]);
            }
            return new TagLString(tags);
        }

        public static LocalizedString Get(Identifier tag)
        {
            TagLString? str = null;
            lock (cachedStrings)
            {
                if (!nonCacheableTags.Contains(tag))
                {
                    if (cachedStrings.TryGetValue(tag, out var strRef))
                    {
                        if (!strRef.TryGetTarget(out str))
                        {
                            cachedStrings.Remove(tag);
                        }
                    }

                    if (str is null && TextPacks.ContainsKey(GameSettings.CurrentConfig.Language))
                    {
                        int count = 0;
                        foreach (var pack in TextPacks[GameSettings.CurrentConfig.Language])
                        {
                            if (pack.Texts.TryGetValue(tag, out var texts))
                            {
                                count += texts.Length;
                                if (count > 1) { break; }
                            }
                        }

                        if (count > 1)
                        {
                            nonCacheableTags = nonCacheableTags.Add(tag);
                        }
                        else
                        {
                            str = new TagLString(tag);
                            cachedStrings.Add(tag, new WeakReference<TagLString>(str));
                        }
                    }
                }
            }
            return str ?? new TagLString(tag);
        }

        public static LocalizedString Get(string tag)            
            => Get(tag.ToIdentifier());

        public static LocalizedString Get(params string[] tags)
            => Get(tags.ToIdentifiers());

        public static LocalizedString AddPunctuation(char punctuationSymbol, params LocalizedString[] texts)
        {
            return new AddedPunctuationLString(punctuationSymbol, texts);
        }

        public static LocalizedString GetFormatted(Identifier tag, params object[] args)
        {
            return GetFormatted(new TagLString(tag), args);
        }

        public static LocalizedString GetFormatted(LocalizedString str, params object[] args)
        {
            LocalizedString[] argStrs = new LocalizedString[args.Length];
            for (int i = 0; i < args.Length; i++)
            {
                if (args[i] is LocalizedString ls) { argStrs[i] = ls; }
                else { argStrs[i] = new RawLString(args[i].ToString() ?? ""); }
            }
            return new FormattedLString(str, argStrs);
        }

        public static string FormatServerMessage(string str) => $"{str}~";
        
        public static string FormatServerMessage(string message, params (string Key, string Value)[] keysWithValues)
        {
            if (keysWithValues.Length == 0)
            {
                return FormatServerMessage(message);
            }
            var startIndex = message.LastIndexOf('/') + 1;
            var endIndex = message.IndexOf('~', startIndex);
            if (endIndex == -1)
            {
                endIndex = message.Length - 1;
            }
            var textId = message.Substring(startIndex, endIndex - startIndex + 1);
            var prefixEntries = keysWithValues.Select((kv, index) =>
            {
                if (kv.Value.IndexOfAny(new char[] { '~', '/' }) != -1)
                {
                    var kvStartIndex = kv.Value.LastIndexOf('/') + 1;
                    return kv.Value.Substring(0, kvStartIndex) + $"[{textId}.{index}]={kv.Value.Substring(kvStartIndex)}";
                }
                else
                {
                    return null;
                }
            }).Where(e => e != null).ToArray();
            return string.Join("",
                (prefixEntries.Length > 0 ? string.Join("/", prefixEntries) + "/" : ""),
                message,
                string.Join("", keysWithValues.Select((kv, index) => kv.Value.IndexOfAny(new char[] { '~', '/' }) != -1 ? $"~{kv.Key}=[{textId}.{index}]" : $"~{kv.Key}={kv.Value}"))
            );
        }
        
        internal static string FormatServerMessageWithPronouns(CharacterInfo charInfo, string message, params (string Key, string Value)[] keysWithValues)
        {
            var pronounCategory = charInfo.Prefab.Pronouns;
            (string Key, string Value)[] pronounKwv = new[]
            {
                ("[PronounLowercase]", charInfo.ReplaceVars($"Pronoun[{pronounCategory}]Lowercase")),
                ("[PronounUppercase]", charInfo.ReplaceVars($"Pronoun[{pronounCategory}]")),
                ("[PronounPossessiveLowercase]", charInfo.ReplaceVars($"PronounPossessive[{pronounCategory}]Lowercase")),
                ("[PronounPossessiveUppercase]", charInfo.ReplaceVars($"PronounPossessive[{pronounCategory}]")),
                ("[PronounReflexiveLowercase]", charInfo.ReplaceVars($"PronounReflexive[{pronounCategory}]Lowercase")),
                ("[PronounReflexiveUppercase]", charInfo.ReplaceVars($"PronounReflexive[{pronounCategory}]"))
            };
            return FormatServerMessage(message, keysWithValues.Concat(pronounKwv).ToArray());
        }

        // Same as string.Join(separator, parts) but performs the operation taking into account server message string replacements.
        public static string JoinServerMessages(string separator, string[] parts, string namePrefix = "part.")
        {

            return string.Join("/",
                string.Join("/", parts.Select((part, index) =>
                {
                    var partStart = part.LastIndexOf('/') + 1;
                    return partStart > 0 ? $"{part.Substring(0, partStart)}/[{namePrefix}{index}]={part.Substring(partStart)}" : $"[{namePrefix}{index}]={part.Substring(partStart)}";
                })),
                string.Join(separator, parts.Select((part, index) => $"[{namePrefix}{index}]")));
        }
        
        public static LocalizedString ParseInputTypes(LocalizedString str, bool useColorHighlight = false)
        {
            return new InputTypeLString(str, useColorHighlight);
        }

        public static LocalizedString GetWithVariable(string tag, string varName, LocalizedString value, FormatCapitals formatCapitals = FormatCapitals.No)
        {
            return GetWithVariable(tag.ToIdentifier(), varName.ToIdentifier(), value, formatCapitals);
        }

        public static LocalizedString GetWithVariable(Identifier tag, Identifier varName, LocalizedString value, FormatCapitals formatCapitals = FormatCapitals.No)
        {
            return GetWithVariables(tag, (varName, value));
        }

        public static LocalizedString GetWithVariables(string tag, params (string Key, string Value)[] replacements)
        {
            return GetWithVariables(
                tag.ToIdentifier(),
                replacements.Select(kv =>
                    (kv.Key.ToIdentifier(),
                    (LocalizedString)new RawLString(kv.Value),
                    FormatCapitals.No)));
        }

        public static LocalizedString GetWithVariables(string tag, params (string Key, LocalizedString Value)[] replacements)
        {
            return GetWithVariables(
                tag.ToIdentifier(),
                replacements.Select(kv =>
                    (kv.Key.ToIdentifier(),
                    kv.Value,
                    FormatCapitals.No)));
        }

        public static LocalizedString GetWithVariables(string tag, params (string Key, LocalizedString Value, FormatCapitals FormatCapitals)[] replacements)
        {
            return GetWithVariables(
                tag.ToIdentifier(),
                replacements.Select(kv =>
                    (kv.Key.ToIdentifier(),
                     kv.Value,
                     kv.FormatCapitals)));
        }

        public static LocalizedString GetWithVariables(string tag, params (string Key, string Value, FormatCapitals FormatCapitals)[] replacements)
        {
            return GetWithVariables(
                tag.ToIdentifier(),
                replacements.Select(kv =>
                    (kv.Key.ToIdentifier(),
                    (LocalizedString)new RawLString(kv.Value),
                    kv.FormatCapitals)));
        }

        public static LocalizedString GetWithVariables(Identifier tag, params (Identifier Key, LocalizedString Value)[] replacements)
        {
            return GetWithVariables(tag, replacements.Select(kv => (kv.Key, kv.Value, FormatCapitals.No)));
        }

        public static LocalizedString GetWithVariables(Identifier tag, IEnumerable<(Identifier, LocalizedString, FormatCapitals)> replacements)
        {
            return new ReplaceLString(new TagLString(tag), StringComparison.OrdinalIgnoreCase, replacements);
        }
        
        public static void ConstructDescription(ref LocalizedString description, XElement descriptionElement, Func<string, string>? customTagReplacer = null)
        {
            /*
            <Description tag="talentdescription.simultaneousskillgain">
                <Replace tag="[skillname1]" value="skillname.helm"/>
                <Replace tag="[skillname2]" value="skillname.weapons"/>
                <Replace tag="[somevalue]" value="45.3"/>
            </Description>
            */
            Identifier tag = descriptionElement.GetAttributeIdentifier("tag", Identifier.Empty);
            LocalizedString extraDescriptionLine = Get(tag).Fallback(tag.Value);
            foreach (XElement replaceElement in descriptionElement.Elements())
            {
                if (replaceElement.NameAsIdentifier() != "replace") { continue; }

                Identifier variableTag = replaceElement.GetAttributeIdentifier("tag", Identifier.Empty);
                LocalizedString replacementValue = string.Empty;
                if (customTagReplacer != null)
                {
                    replacementValue = customTagReplacer(replaceElement.GetAttributeString("value", string.Empty));
                }
                if (replacementValue.IsNullOrWhiteSpace())
                {
                    string[] replacementValues = replaceElement.GetAttributeStringArray("value", Array.Empty<string>());
                    for (int i = 0; i < replacementValues.Length; i++)
                    {
                        replacementValue += Get(replacementValues[i]).Fallback(replacementValues[i]);
                        if (i < replacementValues.Length - 1)
                        {
                            replacementValue += ", ";
                        }
                    }
                }
                if (replaceElement.Attribute("color") != null)
                {
                    string colorStr = replaceElement.GetAttributeString("color", "255,255,255,255");
                    replacementValue = $"‖color:{colorStr}‖" + replacementValue + "‖color:end‖";
                }
                extraDescriptionLine = extraDescriptionLine.Replace(variableTag, replacementValue);
            }
            if (!(description is RawLString { Value: "" })) { description += "\n"; } //TODO: this is cursed
            description += extraDescriptionLine;
        }

        public static LocalizedString FormatCurrency(int amount, bool includeCurrencySymbol = true)
        {
            string valueString = string.Format(CultureInfo.InvariantCulture, "{0:N0}", amount);
             return includeCurrencySymbol ? 
                GetWithVariable("currencyformat", "[credits]", valueString) :
                valueString;
        }

        public static LocalizedString GetServerMessage(string serverMessage)
        {
            return new ServerMsgLString(serverMessage);
        }

        public static LocalizedString Capitalize(this LocalizedString str)
        {
            return new CapitalizeLString(str);
        }

        public static void IncrementLanguageVersion()
        {
            LanguageVersion++;
            ClearCache();
        }
        
#if DEBUG
        public static void CheckForDuplicates(LanguageIdentifier lang)
        {
            if (!TextPacks.ContainsKey(lang))
            {
                DebugConsole.ThrowError("No text packs available for the selected language (" + lang + ")!");
                return;
            }

            int packIndex = 0;
            foreach (TextPack textPack in TextPacks[lang])
            {
                textPack.CheckForDuplicates(packIndex);
                packIndex++;
            }
        }

        public static void WriteToCSV()
        {
            LanguageIdentifier lang = DefaultLanguage;

            if (!TextPacks.ContainsKey(lang))
            {
                DebugConsole.ThrowError("No text packs available for the selected language (" + lang + ")!");
                return;
            }

            int packIndex = 0;
            foreach (TextPack textPack in TextPacks[lang])
            {
                textPack.WriteToCSV(packIndex);
                packIndex++;
            }
        }
#endif
    }
}