using System.Text.RegularExpressions;

using RolemasterDb.App.Domain;

namespace RolemasterDb.ImportTool.Parsing;

internal static class AffixEffectParser
{
    private const string FoeTarget = "foe";

    private static readonly Regex DirectHitsRegex = new(@"[+-]\s*\d+\s*H\b", RegexOptions.Compiled);
    private static readonly Regex PowerPointModifierRegex = new(@"\+\s*\((?<expression>[^)]+)\)\s*P\b", RegexOptions.Compiled);
    private static readonly Regex ModifierRegex = new(@"\((?<noise>[^0-9+\-)]*)(?<sign>[+-])\s*(?<value>\d+)\)", RegexOptions.Compiled);

    internal static IReadOnlyList<ParsedCriticalEffect> Parse(string? rawAffixText, AffixLegend affixLegend)
    {
        if (string.IsNullOrWhiteSpace(rawAffixText))
        {
            return [];
        }

        var effects = new List<ParsedCriticalEffect>();

        foreach (var rawLine in rawAffixText.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
        {
            ParseLine(CriticalTableParserSupport.CollapseWhitespace(rawLine), affixLegend, effects);
        }

        return effects;
    }

    private static void ParseLine(string line, AffixLegend affixLegend, List<ParsedCriticalEffect> effects)
    {
        if (string.IsNullOrWhiteSpace(line) || line is "-" or "–" or "—")
        {
            return;
        }

        var consumedRanges = new List<(int Start, int End)>();
        var matchedEffects = new List<(int Index, ParsedCriticalEffect Effect)>();

        AddMatches(
            DirectHitsRegex.Matches(line),
            matchedEffects,
            consumedRanges,
            match =>
            {
                var hits = ParseSignedInteger(match.Value);
                return new ParsedCriticalEffect(
                    CriticalEffectCodes.DirectHits,
                    FoeTarget,
                    hits,
                    null,
                    null,
                    null,
                    null,
                    null,
                    false,
                    "symbol",
                    NormalizeToken(match.Value));
            });

        if (affixLegend.SupportsPowerPointModifier)
        {
            AddMatches(
                PowerPointModifierRegex.Matches(line),
                matchedEffects,
                consumedRanges,
                match => new ParsedCriticalEffect(
                    CriticalEffectCodes.PowerPointModifier,
                    FoeTarget,
                    null,
                    CriticalTableParserSupport.CollapseWhitespace(match.Groups["expression"].Value),
                    null,
                    null,
                    null,
                    null,
                    false,
                    "symbol",
                    NormalizeToken(match.Value)));
        }

        AddMatches(
            ModifierRegex.Matches(line),
            matchedEffects,
            consumedRanges,
            match =>
            {
                var modifier = BuildModifier(match);
                if (modifier is null)
                {
                    return null;
                }

                if (modifier.Value < 0 && affixLegend.SupportsFoePenalty)
                {
                    return new ParsedCriticalEffect(
                        CriticalEffectCodes.FoePenalty,
                        FoeTarget,
                        null,
                        null,
                        null,
                        null,
                        modifier.Value,
                        null,
                        false,
                        "symbol",
                        NormalizeToken(match.Value));
                }

                if (modifier.Value > 0 && affixLegend.SupportsAttackerBonus)
                {
                    return new ParsedCriticalEffect(
                        CriticalEffectCodes.AttackerBonusNextRound,
                        null,
                        null,
                        null,
                        null,
                        null,
                        modifier.Value,
                        null,
                        false,
                        "symbol",
                        NormalizeToken(match.Value));
                }

                return null;
            });

        var symbolClusterRegex = CreateSymbolClusterRegex(affixLegend.EffectSymbols);
        if (symbolClusterRegex is null)
        {
            return;
        }

        foreach (Match match in symbolClusterRegex.Matches(line))
        {
            if (!match.Success || OverlapsConsumedRange(match, consumedRanges))
            {
                continue;
            }

            var magnitude = match.Groups["count"].Success
                ? int.Parse(match.Groups["count"].Value)
                : 1;
            var matchedText = NormalizeToken(match.Value);

            foreach (var symbol in match.Groups["symbols"].Value.Select(character => character.ToString()))
            {
                var effectCode = affixLegend.ResolveEffectCode(symbol);
                if (effectCode is null)
                {
                    continue;
                }

                matchedEffects.Add((match.Index, CreateSymbolEffect(effectCode, magnitude, matchedText)));
            }
        }

        if (matchedEffects.Count > 0)
        {
            effects.AddRange(matchedEffects
                .OrderBy(item => item.Index)
                .Select(item => item.Effect));
        }
    }

    private static ParsedCriticalEffect CreateSymbolEffect(string effectCode, int magnitude, string sourceText) =>
        effectCode switch
        {
            CriticalEffectCodes.MustParryRounds => new ParsedCriticalEffect(
                effectCode,
                FoeTarget,
                null,
                null,
                magnitude,
                null,
                null,
                null,
                false,
                "symbol",
                sourceText),
            CriticalEffectCodes.NoParryRounds => new ParsedCriticalEffect(
                effectCode,
                FoeTarget,
                null,
                null,
                magnitude,
                null,
                null,
                null,
                false,
                "symbol",
                sourceText),
            CriticalEffectCodes.StunnedRounds => new ParsedCriticalEffect(
                effectCode,
                FoeTarget,
                null,
                null,
                magnitude,
                null,
                null,
                null,
                false,
                "symbol",
                sourceText),
            CriticalEffectCodes.BleedPerRound => new ParsedCriticalEffect(
                effectCode,
                FoeTarget,
                null,
                null,
                null,
                magnitude,
                null,
                null,
                false,
                "symbol",
                sourceText),
            _ => throw new InvalidOperationException($"Unsupported symbol effect code '{effectCode}'.")
        };

    private static Regex? CreateSymbolClusterRegex(IReadOnlySet<string> symbols)
    {
        if (symbols.Count == 0)
        {
            return null;
        }

        var escapedSymbols = string.Concat(symbols.Select(Regex.Escape));
        return new Regex(
            $@"(?<![A-Za-z0-9])(?:(?<count>\d+)\s*)?(?<symbols>[{escapedSymbols}]+)",
            RegexOptions.Compiled);
    }

    private static void AddMatches(
        MatchCollection matches,
        List<(int Index, ParsedCriticalEffect Effect)> matchedEffects,
        List<(int Start, int End)> consumedRanges,
        Func<Match, ParsedCriticalEffect?> createEffect)
    {
        foreach (Match match in matches)
        {
            if (!match.Success || OverlapsConsumedRange(match, consumedRanges))
            {
                continue;
            }

            consumedRanges.Add((match.Index, match.Index + match.Length));

            var effect = createEffect(match);
            if (effect is not null)
            {
                matchedEffects.Add((match.Index, effect));
            }
        }
    }

    private static bool OverlapsConsumedRange(Match match, IReadOnlyList<(int Start, int End)> consumedRanges) =>
        consumedRanges.Any(range => match.Index < range.End && range.Start < match.Index + match.Length);

    private static int ParseSignedInteger(string value) =>
        int.Parse(value.Replace(" ", string.Empty, StringComparison.Ordinal).Replace("H", string.Empty, StringComparison.OrdinalIgnoreCase));

    private static int? BuildModifier(Match match)
    {
        if (!int.TryParse(match.Groups["value"].Value, out var absoluteValue))
        {
            return null;
        }

        return string.Equals(match.Groups["sign"].Value, "-", StringComparison.Ordinal)
            ? -absoluteValue
            : absoluteValue;
    }

    private static string NormalizeToken(string value) =>
        CriticalTableParserSupport.CollapseWhitespace(value)
            .Replace(" +", "+", StringComparison.Ordinal)
            .Replace("( ", "(", StringComparison.Ordinal)
            .Replace(" )", ")", StringComparison.Ordinal);
}
