using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;

namespace Radzen.Documents.Spreadsheet;

#nullable enable

/// <summary>
/// Provides Excel-compatible number format code rendering.
/// </summary>
public static class NumberFormat
{

    /// <summary>
    /// Applies a format code to a value and returns the formatted string.
    /// Returns null when the format is General/null (caller should fall back to default rendering).
    /// </summary>
    public static string? Apply(string? formatCode, object? value, CellDataType type)
        => ApplyWithColor(formatCode, value, type).Text;

    /// <summary>
    /// Applies a format code to a value and returns the formatted string and optional color.
    /// The color is determined by color codes in the format string (e.g., [Red], [Green]).
    /// </summary>
    public static (string? Text, string? Color) ApplyWithColor(string? formatCode, object? value, CellDataType type)
    {
        if (value is null || string.IsNullOrEmpty(formatCode) ||
            string.Equals(formatCode, "General", StringComparison.OrdinalIgnoreCase))
        {
            return (null, null);
        }

        if (formatCode == "@")
        {
            return (value.ToString(), null);
        }

        if (type == CellDataType.String && value is string s)
        {
            return (s, null);
        }

        var parsed = NumberFormatParser.Parse(formatCode);

        if (!TryGetNumber(value, type, out var number))
        {
            return (value.ToString(), null);
        }

        var section = SelectSection(parsed, number);
        string? text;

        if (section.IsDate)
        {
            text = FormatDate(section, value, type, number);
        }
        else
        {
            text = FormatNumber(section, number);
        }

        return (text, section.Color);
    }

    /// <summary>
    /// Adjusts the number of decimal places in a format code.
    /// </summary>
    public static string AdjustDecimals(string? formatCode, int delta)
    {
        if (string.IsNullOrEmpty(formatCode) ||
            string.Equals(formatCode, "General", StringComparison.OrdinalIgnoreCase))
        {
            formatCode = "0";
        }

        var hasPercent = formatCode.Contains('%', StringComparison.Ordinal);
        var core = hasPercent ? formatCode.Replace("%", "", StringComparison.Ordinal) : formatCode;

        // Split off scientific exponent suffix (E+00, E-00, etc.)
        var exponentSuffix = "";
        for (var idx = 0; idx < core.Length; idx++)
        {
            if ((core[idx] is 'E' or 'e') && idx + 1 < core.Length && core[idx + 1] is '+' or '-')
            {
                exponentSuffix = core[idx..];
                core = core[..idx];
                break;
            }
        }

        var dotIndex = core.IndexOf('.', StringComparison.Ordinal);
        int currentDecimals;
        string integerPart;

        if (dotIndex >= 0)
        {
            integerPart = core[..dotIndex];
            var decimalPart = core[(dotIndex + 1)..];
            currentDecimals = decimalPart.Length;
        }
        else
        {
            integerPart = core;
            currentDecimals = 0;
        }

        var newDecimals = Math.Max(0, currentDecimals + delta);

        string result;
        if (newDecimals == 0)
        {
            result = integerPart;
        }
        else
        {
            result = integerPart + "." + new string('0', newDecimals);
        }

        result += exponentSuffix;

        if (hasPercent)
        {
            result += "%";
        }

        return result;
    }

    internal static bool IsDateFormat(string formatCode)
    {
        if (string.IsNullOrEmpty(formatCode))
        {
            return false;
        }

        var inLiteral = false;
        var escaped = false;
        var inBracket = false;

        for (var i = 0; i < formatCode.Length; i++)
        {
            var c = formatCode[i];

            if (escaped)
            {
                escaped = false;
                continue;
            }

            if (c == '\\')
            {
                escaped = true;
                continue;
            }

            if (c == '"')
            {
                inLiteral = !inLiteral;
                continue;
            }

            if (inLiteral)
            {
                continue;
            }

            if (c == '[')
            {
                inBracket = true;
                continue;
            }

            if (c == ']')
            {
                inBracket = false;
                continue;
            }

            if (inBracket)
            {
                continue;
            }

            switch (c)
            {
                case 'y' or 'Y':
                case 'd' or 'D':
                    return true;
                case 'h' or 'H':
                    return true;
                case 's' or 'S':
                    return true;
                case 'm' or 'M':
                    // m is date if not preceded by h (which would make it minutes)
                    // but even minutes make the format a "date/time" format
                    return true;
            }
        }

        return false;
    }

    private static bool TryGetNumber(object? value, CellDataType type, out double number)
    {
        if (value is string)
        {
            number = 0;
            if (type != CellDataType.Number && type != CellDataType.Date)
            {
                return false;
            }
        }
        else if (NumericCoercion.TryCoerceToDouble(value, out number))
        {
            return true;
        }

        return double.TryParse(value?.ToString(), NumberStyles.Any, CultureInfo.InvariantCulture, out number);
    }

    private static FormatSection SelectSection(ParsedFormat parsed, double value)
    {
        if (parsed.Sections.Count == 1)
        {
            return parsed.Sections[0];
        }

        if (parsed.Sections.Count == 2)
        {
            return value >= 0 ? parsed.Sections[0] : parsed.Sections[1];
        }

        if (parsed.Sections.Count >= 3)
        {
            if (value > 0)
            {
                return parsed.Sections[0];
            }

            if (value < 0)
            {
                return parsed.Sections[1];
            }
            return parsed.Sections[2];
        }

        return parsed.Sections[0];
    }

    private static string FormatDate(FormatSection section, object? value, CellDataType type, double number)
    {
        DateTime dt;
        if (value is DateTime dateTime)
        {
            dt = dateTime;
        }
        else
        {
            dt = number.ToDate();
            // Preserve fractional day as time
            var fractional = number - Math.Truncate(number);
            if (fractional > 0)
            {
                dt = dt.Date.AddDays(fractional);
            }
        }

        var sb = StringBuilderCache.Acquire();
        var tokens = section.Tokens;

        for (var i = 0; i < tokens.Count; i++)
        {
            var token = tokens[i];
            switch (token.Type)
            {
                case TokenType.Literal:
                    sb.Append(token.Text);
                    break;
                case TokenType.Year:
                    sb.Append(token.Text.Length switch
                    {
                        1 or 2 => dt.ToString("yy", CultureInfo.InvariantCulture),
                        _ => dt.ToString("yyyy", CultureInfo.InvariantCulture)
                    });
                    break;
                case TokenType.Month:
                    sb.Append(token.Text.Length switch
                    {
                        1 => dt.Month.ToString(CultureInfo.InvariantCulture),
                        2 => dt.ToString("MM", CultureInfo.InvariantCulture),
                        3 => dt.ToString("MMM", CultureInfo.InvariantCulture),
                        4 => dt.ToString("MMMM", CultureInfo.InvariantCulture),
                        _ => dt.ToString("MMMM", CultureInfo.InvariantCulture)
                    });
                    break;
                case TokenType.Day:
                    sb.Append(token.Text.Length switch
                    {
                        1 => dt.Day.ToString(CultureInfo.InvariantCulture),
                        2 => dt.ToString("dd", CultureInfo.InvariantCulture),
                        3 => dt.ToString("ddd", CultureInfo.InvariantCulture),
                        _ => dt.ToString("dddd", CultureInfo.InvariantCulture)
                    });
                    break;
                case TokenType.Hour:
                    if (section.Is12Hour)
                    {
                        var h = dt.Hour % 12;
                        if (h == 0)
                        {
                            h = 12;
                        }
                        sb.Append(h.ToString(CultureInfo.InvariantCulture));
                    }
                    else
                    {
                        sb.Append(token.Text.Length >= 2
                            ? dt.ToString("HH", CultureInfo.InvariantCulture)
                            : dt.Hour.ToString(CultureInfo.InvariantCulture));
                    }
                    break;
                case TokenType.Minute:
                    sb.Append(token.Text.Length >= 2
                        ? dt.ToString("mm", CultureInfo.InvariantCulture)
                        : dt.Minute.ToString(CultureInfo.InvariantCulture));
                    break;
                case TokenType.Second:
                    sb.Append(token.Text.Length >= 2
                        ? dt.ToString("ss", CultureInfo.InvariantCulture)
                        : dt.Second.ToString(CultureInfo.InvariantCulture));
                    break;
                case TokenType.AmPm:
                    sb.Append(dt.Hour >= 12 ? "PM" : "AM");
                    break;
            }
        }

        return StringBuilderCache.GetStringAndRelease(sb);
    }

    private static string FormatNumber(FormatSection section, double value)
    {
        // Section with no digit placeholders (e.g. literal "-") returns just prefix+suffix
        if (!section.HasDigitPlaceholders)
        {
            return section.Prefix + section.Suffix;
        }

        if (section.IsScientific)
        {
            return FormatScientific(section, value);
        }

        var absValue = Math.Abs(value);
        var isNegative = value < 0;

        if (section.IsPercentage)
        {
            absValue *= 100;
        }

        if (section.ThousandsScale > 0)
        {
            absValue /= Math.Pow(1000, section.ThousandsScale);
        }

        var intZeros = section.IntegerZeros;
        var intHashes = section.IntegerHashes;
        var decimalPlaces = section.DecimalPlaces;
        var hasThousands = section.HasThousandsSeparator;

        absValue = Math.Round(absValue, decimalPlaces, MidpointRounding.AwayFromZero);

        var sb = StringBuilderCache.Acquire();

        sb.Append(section.Prefix);

        // Add negative sign if this is a single-section format and value is negative
        if (isNegative && section.Index == 0 && !section.HasParens)
        {
            sb.Append('-');
        }

        var intPart = (long)Math.Truncate(absValue);
        var fracPart = absValue - Math.Truncate(absValue);

        var intStr = intPart.ToString(CultureInfo.InvariantCulture);

        var minIntDigits = Math.Max(intZeros, 1);
        if (intZeros == 0 && intHashes > 0 && intPart == 0)
        {
            // Hash hides leading zeros - don't show zero integer part
            intStr = "";
            minIntDigits = 0;
        }
        else if (intStr.Length < intZeros)
        {
            intStr = intStr.PadLeft(intZeros, '0');
        }

        if (hasThousands && intStr.Length > 0)
        {
            var formatted = new StringBuilder(intStr.Length + (intStr.Length - 1) / 3);
            for (var i = 0; i < intStr.Length; i++)
            {
                var remaining = intStr.Length - i;
                if (i > 0 && remaining % 3 == 0)
                {
                    formatted.Append(',');
                }
                formatted.Append(intStr[i]);
            }
            intStr = formatted.ToString();
        }

        sb.Append(intStr);

        if (decimalPlaces > 0)
        {
            sb.Append('.');
            var fracStr = Math.Round(fracPart, decimalPlaces, MidpointRounding.AwayFromZero)
                .ToString("F" + decimalPlaces, CultureInfo.InvariantCulture);
            // Remove "0." prefix
            if (fracStr.StartsWith("0.", StringComparison.Ordinal))
            {
                fracStr = fracStr[2..];
            }
            else if (fracStr.StartsWith("1.", StringComparison.Ordinal))
            {
                // Rounding carried over - already handled by absValue rounding
                fracStr = new string('0', decimalPlaces);
            }

            // Strip trailing zeros for hash (#) decimal placeholders
            // Keep at least DecimalZeros + DecimalSpacePlaceholders digits
            var decimalZeros = section.DecimalZeros;
            var spacePlaceholders = section.DecimalSpacePlaceholders;
            var keepLen = decimalZeros + spacePlaceholders;

            if (keepLen < decimalPlaces)
            {
                while (fracStr.Length > keepLen && fracStr[^1] == '0')
                {
                    fracStr = fracStr[..^1];
                }
            }

            // Replace trailing zeros with spaces for question mark (?) placeholders
            if (spacePlaceholders > 0)
            {
                var chars = fracStr.ToCharArray();
                for (var j = chars.Length - 1; j >= decimalZeros; j--)
                {
                    if (chars[j] == '0')
                    {
                        chars[j] = ' ';
                    }
                    else
                    {
                        break;
                    }
                }
                fracStr = new string(chars);
            }

            // If all decimal digits stripped, remove the dot too
            if (fracStr.Length == 0)
            {
                sb.Length--; // remove the '.'
            }
            else
            {
                sb.Append(fracStr);
            }
        }

        sb.Append(section.Suffix);

        return StringBuilderCache.GetStringAndRelease(sb);
    }

    private static string FormatScientific(FormatSection section, double value)
    {
        var sb = StringBuilderCache.Acquire();
        sb.Append(section.Prefix);

        var isNegative = value < 0;
        var absValue = Math.Abs(value);

        if (isNegative)
        {
            sb.Append('-');
        }

        int exponent;
        double mantissa;

        if (absValue == 0)
        {
            exponent = 0;
            mantissa = 0;
        }
        else
        {
            exponent = (int)Math.Floor(Math.Log10(absValue));
            var intDigits = Math.Max(section.IntegerZeros, 1);
            exponent -= (intDigits - 1);
            mantissa = absValue / Math.Pow(10, exponent);
        }

        mantissa = Math.Round(mantissa, section.DecimalPlaces, MidpointRounding.AwayFromZero);

        // Check if rounding carried over (e.g., 9.995 → 10.00)
        if (mantissa != 0)
        {
            var intDigits = Math.Max(section.IntegerZeros, 1);
            var mantissaIntPart = (long)Math.Truncate(mantissa);
            var mantissaIntDigits = mantissaIntPart == 0 ? 1 : (int)Math.Floor(Math.Log10(mantissaIntPart)) + 1;
            if (mantissaIntDigits > intDigits)
            {
                exponent += (mantissaIntDigits - intDigits);
                mantissa /= Math.Pow(10, mantissaIntDigits - intDigits);
                mantissa = Math.Round(mantissa, section.DecimalPlaces, MidpointRounding.AwayFromZero);
            }
        }

        if (section.DecimalPlaces > 0)
        {
            sb.Append(mantissa.ToString("F" + section.DecimalPlaces, CultureInfo.InvariantCulture));
        }
        else
        {
            sb.Append(((long)Math.Round(mantissa)).ToString(CultureInfo.InvariantCulture));
        }

        var expFormat = section.ExponentFormat;
        var expChar = expFormat[0];
        var expSign = expFormat[1];
        var expDigits = expFormat.Length - 2;

        sb.Append(expChar);

        if (exponent >= 0)
        {
            if (expSign == '+')
            {
                sb.Append('+');
            }
        }
        else
        {
            sb.Append('-');
        }

        sb.Append(Math.Abs(exponent).ToString(CultureInfo.InvariantCulture).PadLeft(expDigits, '0'));
        sb.Append(section.Suffix);

        return StringBuilderCache.GetStringAndRelease(sb);
    }
}
