using System;
using System.Collections.Generic;
using System.Linq;

namespace Radzen.Documents.Spreadsheet;

#nullable enable

/// <summary>
/// Represents errors that can occur during formula evaluation in a spreadsheet.
/// </summary>
public enum CellError
{
    /// <summary>
    /// Indicates a value error, such as type mismatch or invalid argument.
    /// </summary>
    Value,  // #VALUE! - Type mismatch or invalid argument
    /// <summary>
    /// Indicates a division by zero error.
    /// </summary>
    Div0,   // #DIV/0! - Division by zero
    /// <summary>
    /// Indicates an invalid cell reference error, such as referencing a cell that does not exist.
    /// </summary>
    Ref,    // #REF! - Invalid cell reference
    /// <summary>
    /// Indicates an invalid name error, such as using an undefined function or variable name.
    /// </summary>
    Name,   // #NAME? - Invalid function name
    /// <summary>
    /// Indicates an invalid number error, such as using a number that is too large or too small for the context.
    /// </summary>
    Num,    // #NUM! - Invalid number
    /// <summary>
    /// Indicates that a value is not available, such as when a formula cannot return a result.
    /// </summary>
    NA,      // #N/A - Value not available
    /// <summary>
    /// Indicates a circular reference error, which occurs when a formula refers back to its own cell either directly or indirectly.
    /// </summary>
    Circular // #CIRCULAR - Circular reference
}

class FormulaEvaluator(Worksheet sheet, Cell currentCell) : IFormulaSyntaxNodeVisitor
{
    private object? value;
    private readonly HashSet<Cell> evaluationStack = [];

    public void VisitNumberLiteral(NumberLiteralSyntaxNode numberLiteralSyntaxNode)
    {
        var token = numberLiteralSyntaxNode.Token;

        value = token.ValueKind switch
        {
            ValueKind.Int => CellData.FromNumber(token.IntValue),
            ValueKind.UInt => CellData.FromNumber(token.UintValue),
            ValueKind.Long => CellData.FromNumber(token.LongValue),
            ValueKind.ULong => CellData.FromNumber(token.UlongValue),
            ValueKind.Float => CellData.FromNumber(token.FloatValue),
            ValueKind.Double => CellData.FromNumber(token.DoubleValue),
            ValueKind.Decimal => CellData.FromNumber((double)token.DecimalValue),
            _ => throw new InvalidOperationException($"Unsupported value kind: {token.ValueKind}")
        };
    }

    public void VisitStringLiteral(StringLiteralSyntaxNode stringLiteralSyntaxNode)
    {
        if (stringLiteralSyntaxNode.Token.Value is not null)
        {
            value = CellData.FromString(stringLiteralSyntaxNode.Token.Value);
        }
    }

    public void VisitBooleanLiteral(BooleanLiteralSyntaxNode booleanLiteralSyntaxNode)
    {
        value = CellData.FromBoolean(booleanLiteralSyntaxNode.Token.ValueKind == ValueKind.True);
    }

    public void VisitErrorLiteral(ErrorLiteralSyntaxNode errorLiteralSyntaxNode)
    {
        value = CellData.FromError(errorLiteralSyntaxNode.Token.ErrorValue);
    }

    public void VisitBinaryExpression(BinaryExpressionSyntaxNode binaryExpressionSyntaxNode)
    {
        binaryExpressionSyntaxNode.Left.Accept(this);
        var left = (CellData)value!;
        binaryExpressionSyntaxNode.Right.Accept(this);
        var right = (CellData)value!;

        if (left.IsError)
        {
            value = left;
            return;
        }

        if (right.IsError)
        {
            value = right;
            return;
        }

        // Treat empty values as zero for arithmetic
        if (left.IsEmpty)
        {
            left = CellData.FromNumber(0d);
        }

        if (right.IsEmpty)
        {
            right = CellData.FromNumber(0d);
        }

        // For comparison operators, we don't need both sides to be numeric
        var isComparisonOperator = binaryExpressionSyntaxNode.Operator is
            BinaryOperator.Equals or BinaryOperator.NotEquals or
            BinaryOperator.LessThan or BinaryOperator.LessThanOrEqual or
            BinaryOperator.GreaterThan or BinaryOperator.GreaterThanOrEqual;

        // Coerce Date and Boolean to Number for arithmetic operations (Excel semantics)
        if (!isComparisonOperator)
        {
            if (left.Type == CellDataType.Date)
            {
                var ln = left.GetValueOrDefault<DateTime>().ToNumber();
                left = CellData.FromNumber(ln);
            }
            if (right.Type == CellDataType.Date)
            {
                var rn = right.GetValueOrDefault<DateTime>().ToNumber();
                right = CellData.FromNumber(rn);
            }
            if (left.Type == CellDataType.Boolean)
            {
                left = CellData.FromNumber(left.GetValueOrDefault<bool>() ? 1d : 0d);
            }
            if (right.Type == CellDataType.Boolean)
            {
                right = CellData.FromNumber(right.GetValueOrDefault<bool>() ? 1d : 0d);
            }
        }

        if (!isComparisonOperator && (left.Type != CellDataType.Number || right.Type != CellDataType.Number))
        {
            value = CellData.FromError(CellError.Value);
            return;
        }

        if (binaryExpressionSyntaxNode.Operator == BinaryOperator.Divide)
        {
            var rnum = right.GetValueOrDefault<double>();

            if (Math.Abs(rnum) == 0d)
            {
                value = CellData.FromError(CellError.Div0);
                return;
            }
        }

        if (isComparisonOperator)
        {
            switch (binaryExpressionSyntaxNode.Operator)
            {
                case BinaryOperator.Equals:
                    value = CellData.FromBoolean(left.IsEqualTo(right));
                    return;
                case BinaryOperator.NotEquals:
                    value = CellData.FromBoolean(!left.IsEqualTo(right));
                    return;
                case BinaryOperator.LessThan:
                    value = CellData.FromBoolean(left.IsLessThan(right));
                    return;
                case BinaryOperator.LessThanOrEqual:
                    value = CellData.FromBoolean(left.IsLessThanOrEqualTo(right));
                    return;
                case BinaryOperator.GreaterThan:
                    value = CellData.FromBoolean(left.IsGreaterThan(right));
                    return;
                case BinaryOperator.GreaterThanOrEqual:
                    value = CellData.FromBoolean(left.IsGreaterThanOrEqualTo(right));
                    return;
            }
        }
        else
        {
            var l = left.GetValueOrDefault<double>();
            var r = right.GetValueOrDefault<double>();
            double res = 0d;
            switch (binaryExpressionSyntaxNode.Operator)
            {
                case BinaryOperator.Plus:
                    res = l + r;
                    break;
                case BinaryOperator.Minus:
                    res = l - r;
                    break;
                case BinaryOperator.Multiply:
                    res = l * r;
                    break;
                case BinaryOperator.Divide:
                    res = l / r;
                    break;
            }
            value = CellData.FromNumber(res);
        }
    }

    public void VisitUnaryExpression(UnaryExpressionSyntaxNode unaryExpressionSyntaxNode)
    {
        unaryExpressionSyntaxNode.Operand.Accept(this);
        var operand = (CellData)value!;

        if (operand.IsError)
        {
            value = operand;
            return;
        }

        if (operand.IsEmpty)
        {
            operand = CellData.FromNumber(0d);
        }

        if (unaryExpressionSyntaxNode.Operator == UnaryOperator.Plus)
        {
            // Excel: unary plus is a no-op for booleans (=+TRUE returns TRUE)
            if (operand.Type == CellDataType.Boolean)
            {
                return;
            }

            if (operand.Type != CellDataType.Number)
            {
                value = CellData.FromError(CellError.Value);
                return;
            }
            return;
        }

        if (unaryExpressionSyntaxNode.Operator == UnaryOperator.Negate)
        {
            if (operand.Type == CellDataType.Boolean)
            {
                operand = CellData.FromNumber(operand.GetValueOrDefault<bool>() ? 1d : 0d);
            }

            if (operand.Type != CellDataType.Number)
            {
                value = CellData.FromError(CellError.Value);
                return;
            }
            value = CellData.FromNumber(-operand.GetValueOrDefault<double>());
            return;
        }
    }

    private CellData EvaluateCell(Cell cell)
    {
        if (!evaluationStack.Add(cell))
        {
            return CellData.FromError(CellError.Circular);
        }

        CellData result;
        if (cell.FormulaSyntaxTree is not null)
        {
            cell.FormulaSyntaxTree.Root.Accept(this);
            result = (CellData)value!;
        }
        else
        {
            result = cell.Data;
        }
        evaluationStack.Remove(cell);
        return result;
    }

    public void VisitCell(CellSyntaxNode cellSyntaxNode)
    {
        var address = cellSyntaxNode.Token.Address;
        var targetSheet = sheet;
        if (!string.IsNullOrEmpty(address.Worksheet))
        {
            var wb = sheet.Workbook;
            targetSheet = wb.GetSheet(address.Worksheet) ?? targetSheet;
            if (!string.Equals(targetSheet.Name, address.Worksheet, StringComparison.OrdinalIgnoreCase))
            {
                value = CellData.FromError(CellError.Ref);
                return;
            }
        }

        // If the row/column was deleted, set whole formula to =#REF!
        if (targetSheet.IsDeletedRow(address.Row) || targetSheet.IsDeletedColumn(address.Column))
        {
            value = CellData.FromError(CellError.Ref);
            return;
        }

        if ((address.Row < 0 || address.Row >= targetSheet.RowCount) || (address.Column < 0 || address.Column >= targetSheet.ColumnCount))
        {
            value = CellData.FromError(CellError.Ref);
            return;
        }

        if (!targetSheet.Cells.TryGet(address.Row, address.Column, out var cell))
        {
            // Cell is in bounds but not populated - treat as empty
            value = new CellData(null);
            return;
        }

        value = EvaluateCell(cell);
    }

    public CellData Evaluate(FormulaSyntaxNode node)
    {
        node.Accept(this);

        return (CellData)value!;
    }


    public void VisitFunction(FunctionSyntaxNode functionSyntaxNode)
    {
        var function = sheet.FunctionRegistry.Get(functionSyntaxNode.Name);

        var functionArguments = ProcessArguments(function, functionSyntaxNode.Arguments);
        
        if (functionArguments is null)
        {
            return; // Error already set in ProcessArguments
        }

        value = function.Evaluate(functionArguments);
    }

    private FunctionArguments? ProcessArguments(FormulaFunction function, List<FormulaSyntaxNode> argumentNodes)
    {
        var parameterDefinitions = function.Parameters;
        var functionArguments = new FunctionArguments(currentCell);
        var argumentIndex = 0;

        for (int paramIndex = 0; paramIndex < parameterDefinitions.Length; paramIndex++)
        {
            var paramDef = parameterDefinitions[paramIndex];
            
            // Check if we have enough arguments for required parameters
            // But only if this is not a repeating parameter (repeating parameters can handle empty lists)
            if (paramDef.IsRequired &&
                paramDef.Type != ParameterType.Sequence &&
                paramDef.Type != ParameterType.Group &&
                argumentIndex >= argumentNodes.Count)
            {
                value = CellData.FromError(CellError.Value);
                return null;
            }

            if (paramDef.Type == ParameterType.Sequence)
            {
                var allArguments = new List<CellData>();
                while (argumentIndex < argumentNodes.Count)
                {
                    var argumentNode = argumentNodes[argumentIndex];
                    var argument = ProcessArgument(argumentNode, function);
                    if (argument is null)
                    {
                        return null; // Error already set
                    }

                    // Excel coerces literal boolean constants to numbers in aggregate functions
                    // (e.g. SUM(TRUE,1)→2) but skips booleans from cell references.
                    if (function.CoerceLiteralBooleans && argumentNode is BooleanLiteralSyntaxNode)
                    {
                        for (int i = 0; i < argument.Count; i++)
                        {
                            if (argument[i].Type == CellDataType.Boolean)
                            {
                                argument[i] = CellData.FromNumber(argument[i].GetValueOrDefault<bool>() ? 1d : 0d);
                            }
                        }
                    }

                    allArguments.AddRange(argument);
                    argumentIndex++;
                }
                functionArguments.Set(paramDef.Name, allArguments);
            }
            else if (paramDef.Type == ParameterType.Group)
            {
                // Collect all remaining arguments WITHOUT flattening - each stays its own group so
                // (range, criteria) pairs and array shapes survive. A single cell is a one-element list
                // (not a RangeList); consumers use the `as RangeList` / default-1xN convention.
                var groups = new List<List<CellData>>();
                while (argumentIndex < argumentNodes.Count)
                {
                    var argument = ProcessArgument(argumentNodes[argumentIndex], function);
                    if (argument is null)
                    {
                        return null; // Error already set
                    }

                    groups.Add(argument);
                    argumentIndex++;
                }
                functionArguments.Set(paramDef.Name, groups);
            }
            else
            {
                if (argumentIndex < argumentNodes.Count)
                {
                    var argument = ProcessArgument(argumentNodes[argumentIndex], function);
                    if (argument is null)
                    {
                        return null; // Error already set
                    }
                    
                    if (paramDef.Type == ParameterType.Collection)
                    {
                        if (argumentNodes[argumentIndex] is CellSyntaxNode cellNode)
                        {
                            argument = new RangeList(1, 1, cellNode.Token.Address.Row, cellNode.Token.Address.Column, sheet) { argument[0] };
                        }
                        functionArguments.Set(paramDef.Name, argument);
                    }
                    else
                    {
                        functionArguments.Set(paramDef.Name, argument[0]);
                    }
                    argumentIndex++;
                }
                else if (!paramDef.IsRequired)
                {
                    continue;
                }
            }
        }

        // Check if we have too many arguments
        // But only if this is not an ErrorFunction (ErrorFunction can handle any number of arguments)
        if (argumentIndex < argumentNodes.Count && function is not ErrorFunction)
        {
            value = CellData.FromError(CellError.Value);
            return null;
        }

        return functionArguments;
    }

    private List<CellData>? ProcessArgument(FormulaSyntaxNode argumentNode, FormulaFunction function)
    {
        argumentNode.Accept(this);

        var hasError = value is CellData cellData && cellData.IsError;

        // Only short-circuit on errors if the function cannot handle them
        if (hasError && !function.CanHandleErrors)
        {
            return null;
        }

        if (value is List<CellData> list)
        {
            return list;
        }
        else
        {
            return [(CellData)value!];
        }
    }

    public void VisitRange(RangeSyntaxNode rangeSyntaxNode)
    {
        var start = rangeSyntaxNode.Start.Token.Address;
        var end = rangeSyntaxNode.End.Token.Address;

        var startSheet = sheet;
        if (!string.IsNullOrEmpty(start.Worksheet))
        {
            var wb = sheet.Workbook;
            startSheet = wb.GetSheet(start.Worksheet) ?? startSheet;
            if (!string.Equals(startSheet.Name, start.Worksheet, StringComparison.OrdinalIgnoreCase))
            {
                value = CellData.FromError(CellError.Ref);
                return;
            }
        }

        // If end has no sheet specified, assume same as start; otherwise they must match
        var endSheet = startSheet;
        if (!string.IsNullOrEmpty(end.Worksheet))
        {
            endSheet = sheet.Workbook.GetSheet(end.Worksheet) ?? endSheet;
            if (!string.Equals(endSheet.Name, end.Worksheet, StringComparison.OrdinalIgnoreCase) || endSheet != startSheet)
            {
                value = CellData.FromError(CellError.Ref);
                return;
            }
        }

        if (start.Row > end.Row || (start.Row == end.Row && start.Column > end.Column))
        {
            value = CellData.FromError(CellError.Value);
            return;
        }

        var rows = end.Row - start.Row + 1;
        var columns = end.Column - start.Column + 1;
        var cells = new RangeList(rows, columns, start.Row, start.Column, startSheet);

        for (var row = start.Row; row <= end.Row; row++)
        {
            for (var column = start.Column; column <= end.Column; column++)
            {
                if (startSheet.IsDeletedRow(row) || startSheet.IsDeletedColumn(column))
                {
                    value = CellData.FromError(CellError.Ref);
                    return;
                }

                if (row < 0 || row >= startSheet.RowCount || column < 0 || column >= startSheet.ColumnCount)
                {
                    value = CellData.FromError(CellError.Ref);
                    return;
                }

                CellData cellValue;

                if (!startSheet.Cells.TryGet(row, column, out var cell))
                {
                    // Cell is in bounds but not populated - treat as empty
                    cellValue = new CellData(null);
                }
                else
                {
                    cellValue = EvaluateCell(cell);
                }
                // Do not short-circuit on errors here; include them in the range so
                // functions like SUBTOTAL/AGGREGATE can decide how to handle them.
                cells.Add(cellValue);
            }
        }

        value = cells;
    }
}