namespace Radzen.Documents.Spreadsheet;

using System;
using System.Collections.Generic;

#nullable enable

/// <summary>
/// Represents a filter applied to a sheet, which includes a filter criterion and the range of rows that the filter applies to.
/// </summary>
public class SheetFilter(FilterCriterion criterion, RangeRef range)
{
    /// <summary>
    /// Gets or sets the filter criterion used to filter rows in the sheet.
    /// </summary>
    public FilterCriterion Criterion { get; } = criterion;

    /// <summary>
    /// Gets or sets the range of rows that the filter applies to.
    /// </summary>
    public RangeRef Range { get; } = range;
}

/// <summary>
/// Represents a base class for filter criteria used in filtering rows of a spreadsheet.
/// </summary>
public abstract class FilterCriterion
{
    /// <summary>
    /// Determines whether the specified row in the given sheet matches the filter criterion.
    /// </summary>
    /// <param name="sheet"></param>
    /// <param name="row"></param>
    /// <returns></returns>
    public abstract bool Matches(Worksheet sheet, int row);

    /// <summary>
    /// Called once before iteration begins, with the range the filter is being applied to.
    /// Default is no-op. Distribution-aware criteria (top/bottom, above-average) override this
    /// to compute their threshold.
    /// </summary>
    public virtual void OnApply(Worksheet sheet, RangeRef range) { }

    /// <summary>
    /// Accepts a visitor that can perform operations on this filter criterion.
    /// </summary>
    public abstract void Accept(IFilterCriterionVisitor visitor);
}

/// <summary>
/// Defines a visitor interface for filter criteria, allowing different operations to be performed on various types of filter criteria.
/// </summary>
public interface IFilterCriterionVisitor
{
    /// <summary>
    /// Visits an OrCriterion.
    /// </summary>
    void Visit(OrCriterion criterion);

    /// <summary>
    /// Visits an AndCriterion.
    /// </summary>
    void Visit(AndCriterion criterion);

    /// <summary>
    /// Visits an EqualToCriterion.
    /// </summary>
    void Visit(EqualToCriterion criterion);

    /// <summary>
    /// Visits a GreaterThanCriterion.
    /// </summary>
    void Visit(GreaterThanCriterion criterion);

    /// <summary>
    /// Visits an InListCriterion.
    /// </summary>
    void Visit(InListCriterion criterion);

    /// <summary>
    /// Visits an IsNullCriterion.
    /// </summary>
    void Visit(IsNullCriterion criterion);

    /// <summary>
    /// Visits a LessThanCriterion.
    /// </summary>
    void Visit(LessThanCriterion criterion);

    /// <summary>
    /// Visits a GreaterThanOrEqualCriterion.
    /// </summary>
    void Visit(GreaterThanOrEqualCriterion criterion);

    /// <summary>
    /// Visits a LessThanOrEqualCriterion.
    /// </summary>
    void Visit(LessThanOrEqualCriterion criterion);

    /// <summary>
    /// Visits a NotEqualToCriterion.
    /// </summary>
    void Visit(NotEqualToCriterion criterion);

    /// <summary>
    /// Visits a StartsWithCriterion.
    /// </summary>
    void Visit(StartsWithCriterion criterion);

    /// <summary>
    /// Visits a DoesNotStartWithCriterion.
    /// </summary>
    void Visit(DoesNotStartWithCriterion criterion);

    /// <summary>
    /// Visits an EndsWithCriterion.
    /// </summary>
    void Visit(EndsWithCriterion criterion);

    /// <summary>
    /// Visits a DoesNotEndWithCriterion.
    /// </summary>
    void Visit(DoesNotEndWithCriterion criterion);

    /// <summary>
    /// Visits a ContainsCriterion.
    /// </summary>
    void Visit(ContainsCriterion criterion);

    /// <summary>
    /// Visits a DoesNotContainCriterion.
    /// </summary>
    void Visit(DoesNotContainCriterion criterion);

    /// <summary>
    /// Visits a TopFilterCriterion.
    /// </summary>
    void Visit(TopFilterCriterion criterion);

    /// <summary>
    /// Visits a DynamicFilterCriterion.
    /// </summary>
    void Visit(DynamicFilterCriterion criterion);

    /// <summary>
    /// Visits a CellColorFilterCriterion.
    /// </summary>
    void Visit(CellColorFilterCriterion criterion);
}

/// <summary>
/// Base implementation of <see cref="IFilterCriterionVisitor"/> with no-op defaults.
/// Composite criteria (Or, And) recurse into their children by default.
/// Override only the methods you need.
/// </summary>
public abstract class FilterCriterionVisitorBase : IFilterCriterionVisitor
{
    /// <summary>
    /// Default visit handler invoked by every Visit overload that is not
    /// otherwise overridden. Override to provide a common fallback for unhandled criteria.
    /// </summary>
    protected virtual void DefaultVisit(FilterCriterion criterion)
    {
    }

    /// <inheritdoc/>
    public virtual void Visit(OrCriterion criterion)
    {
        ArgumentNullException.ThrowIfNull(criterion);

        foreach (var c in criterion.Criteria)
        {
            c.Accept(this);
        }
    }

    /// <inheritdoc/>
    public virtual void Visit(AndCriterion criterion)
    {
        ArgumentNullException.ThrowIfNull(criterion);

        foreach (var c in criterion.Criteria)
        {
            c.Accept(this);
        }
    }

    /// <inheritdoc/>
    public virtual void Visit(EqualToCriterion criterion) => DefaultVisit(criterion);

    /// <inheritdoc/>
    public virtual void Visit(GreaterThanCriterion criterion) => DefaultVisit(criterion);

    /// <inheritdoc/>
    public virtual void Visit(InListCriterion criterion) => DefaultVisit(criterion);

    /// <inheritdoc/>
    public virtual void Visit(IsNullCriterion criterion) => DefaultVisit(criterion);

    /// <inheritdoc/>
    public virtual void Visit(LessThanCriterion criterion) => DefaultVisit(criterion);

    /// <inheritdoc/>
    public virtual void Visit(GreaterThanOrEqualCriterion criterion) => DefaultVisit(criterion);

    /// <inheritdoc/>
    public virtual void Visit(LessThanOrEqualCriterion criterion) => DefaultVisit(criterion);

    /// <inheritdoc/>
    public virtual void Visit(NotEqualToCriterion criterion) => DefaultVisit(criterion);

    /// <inheritdoc/>
    public virtual void Visit(StartsWithCriterion criterion) => DefaultVisit(criterion);

    /// <inheritdoc/>
    public virtual void Visit(DoesNotStartWithCriterion criterion) => DefaultVisit(criterion);

    /// <inheritdoc/>
    public virtual void Visit(EndsWithCriterion criterion) => DefaultVisit(criterion);

    /// <inheritdoc/>
    public virtual void Visit(DoesNotEndWithCriterion criterion) => DefaultVisit(criterion);

    /// <inheritdoc/>
    public virtual void Visit(ContainsCriterion criterion) => DefaultVisit(criterion);

    /// <inheritdoc/>
    public virtual void Visit(DoesNotContainCriterion criterion) => DefaultVisit(criterion);

    /// <inheritdoc/>
    public virtual void Visit(TopFilterCriterion criterion) => DefaultVisit(criterion);

    /// <inheritdoc/>
    public virtual void Visit(DynamicFilterCriterion criterion) => DefaultVisit(criterion);

    /// <inheritdoc/>
    public virtual void Visit(CellColorFilterCriterion criterion) => DefaultVisit(criterion);
}

/// <summary>
/// Represents a filter criterion that combines multiple criteria using a logical OR operation.
/// </summary>
public class OrCriterion : FilterCriterion
{
    /// <summary>
    /// Gets or sets the array of filter criteria that will be combined using a logical OR operation.
    /// </summary>
    public FilterCriterion[] Criteria { get; init; } = [];

    /// <inheritdoc/>
    public override bool Matches(Worksheet sheet, int row)
    {
        foreach (var criterion in Criteria)
        {
            if (criterion.Matches(sheet, row))
            {
                return true;
            }
        }

        return false;
    }

    /// <inheritdoc/>
    public override void Accept(IFilterCriterionVisitor visitor)
    {
        ArgumentNullException.ThrowIfNull(visitor);
        visitor.Visit(this);
    }
}

/// <summary>
/// Represents a filter criterion that combines multiple criteria using a logical AND operation.
/// </summary>
public class AndCriterion : FilterCriterion
{
    /// <summary>
    /// Gets or sets the array of filter criteria that will be combined using a logical AND operation.
    /// </summary>
    public FilterCriterion[] Criteria { get; init; } = [];

    /// <inheritdoc/>
    public override bool Matches(Worksheet sheet, int row)
    {
        foreach (var criterion in Criteria)
        {
            if (!criterion.Matches(sheet, row))
            {
                return false;
            }
        }

        return true;
    }

    /// <inheritdoc/>
    public override void Accept(IFilterCriterionVisitor visitor)
    {
        ArgumentNullException.ThrowIfNull(visitor);
        visitor.Visit(this);
    }
}

/// <summary>
/// Represents a base class for filter criteria that operate on a specific column in a spreadsheet.
/// </summary>
public abstract class FilterCriterionLeaf : FilterCriterion
{
    /// <summary>
    /// Gets or sets the index of the column that this filter criterion applies to.
    /// </summary>
    public int Column { get; init; }

    /// <inheritdoc/>
    public override bool Matches(Worksheet sheet, int row)
    {
        ArgumentNullException.ThrowIfNull(sheet);
        var value = sheet.Cells.TryGet(row, Column, out var cell) ? cell.Value : null;
        return Matches(value);
    }

    /// <summary>
    /// Determines whether the specified value matches the filter criterion.
    /// </summary>
    public abstract bool Matches(object? value);

    /// <summary>
    /// Attempts to coerce the given value into a double for comparison purposes.
    /// </summary>
    /// <param name="value"></param>
    /// <param name="result"></param>
    /// <returns></returns>
    protected static bool TryCoerce(object? value, out double result)
        => NumericCoercion.TryCoerceToDouble(value, out result);
}

/// <summary>
/// Represents a base class for filter criteria that operate on numeric values.
/// </summary>
public abstract class NumericFilterCriterion : FilterCriterionLeaf
{
    /// <summary>
    /// Gets or sets the value that this filter criterion checks against.
    /// </summary>
    public object? Value { get; init; }

    /// <inheritdoc/>
    public override bool Matches(object? value)
    {
        if (value is null || Value is null)
        {
            return false;
        }

        if (TryCoerce(value, out var numericValue) && TryCoerce(Value, out var numericCriterion))
        {
            return Matches(numericValue, numericCriterion);
        }

        return false;
    }

    /// <summary>
    /// Performs the specific numeric operation for this filter criterion.
    /// </summary>
    /// <param name="numericValue">The numeric value to check.</param>
    /// <param name="numericCriterion">The criterion numeric value to check against.</param>
    /// <returns>True if the operation succeeds; otherwise, false.</returns>
    protected abstract bool Matches(double numericValue, double numericCriterion);
}

/// <summary>
/// Represents a filter criterion that checks for equality with a specified value.
/// </summary>
public class EqualToCriterion : FilterCriterionLeaf
{
    /// <summary>
    /// Gets or sets the value that this filter criterion checks for equality against.
    /// </summary>
    public object? Value { get; init; }

    /// <inheritdoc/>
    public override bool Matches(object? value)
    {
        if (value is null || Value is null)
        {
            return false;
        }

        if (Equals(value, Value))
        {
            return true;
        }

        if (TryCoerce(value, out var numericValue) && TryCoerce(Value, out var numericCriterion))
        {
            return numericValue == numericCriterion;
        }

        return false;
    }

    /// <inheritdoc/>
    public override void Accept(IFilterCriterionVisitor visitor)
    {
        ArgumentNullException.ThrowIfNull(visitor);
        visitor.Visit(this);
    }
}

/// <summary>
/// Represents a filter criterion that checks for inequality with a specified value.
/// </summary>
public class GreaterThanCriterion : NumericFilterCriterion
{
    /// <inheritdoc/>
    protected override bool Matches(double numericValue, double numericCriterion) => numericValue > numericCriterion;

    /// <inheritdoc/>
    public override void Accept(IFilterCriterionVisitor visitor)
    {
        ArgumentNullException.ThrowIfNull(visitor);
        visitor.Visit(this);
    }
}

/// <summary>
/// Represents a filter criterion that checks if the value is less than a specified value.
/// </summary>
public class LessThanCriterion : NumericFilterCriterion
{
    /// <inheritdoc/>
    protected override bool Matches(double numericValue, double numericCriterion) => numericValue < numericCriterion;

    /// <inheritdoc/>
    public override void Accept(IFilterCriterionVisitor visitor)
    {
        ArgumentNullException.ThrowIfNull(visitor);
        visitor.Visit(this);
    }
}

/// <summary>
/// Represents a filter criterion that checks if the value is greater than or equal to a specified value.
/// </summary>
public class GreaterThanOrEqualCriterion : NumericFilterCriterion
{
    /// <inheritdoc/>
    protected override bool Matches(double numericValue, double numericCriterion) => numericValue >= numericCriterion;

    /// <inheritdoc/>
    public override void Accept(IFilterCriterionVisitor visitor)
    {
        ArgumentNullException.ThrowIfNull(visitor);
        visitor.Visit(this);
    }
}

/// <summary>
/// Represents a filter criterion that checks if the value is less than or equal to a specified value.
/// </summary>
public class LessThanOrEqualCriterion : NumericFilterCriterion
{
    /// <inheritdoc/>
    protected override bool Matches(double numericValue, double numericCriterion) => numericValue <= numericCriterion;

    /// <inheritdoc/>
    public override void Accept(IFilterCriterionVisitor visitor)
    {
        ArgumentNullException.ThrowIfNull(visitor);
        visitor.Visit(this);
    }
}

/// <summary>
/// Represents a filter criterion that checks for inequality with a specified value.
/// </summary>
public class NotEqualToCriterion : FilterCriterionLeaf
{
    /// <summary>
    /// Gets or sets the value that this filter criterion checks for inequality against.
    /// </summary>
    public object? Value { get; init; }

    /// <inheritdoc/>
    public override bool Matches(object? value)
    {
        if (value is null || Value is null)
        {
            return false;
        }

        if (Equals(value, Value))
        {
            return false;
        }

        if (TryCoerce(value, out var numericValue) && TryCoerce(Value, out var numericCriterion))
        {
            return numericValue != numericCriterion;
        }

        return true;
    }

    /// <inheritdoc/>
    public override void Accept(IFilterCriterionVisitor visitor)
    {
        ArgumentNullException.ThrowIfNull(visitor);
        visitor.Visit(this);
    }
}

/// <summary>
/// Represents a filter criterion that checks if the value is in a predefined list of values.
/// </summary>
public class InListCriterion : FilterCriterionLeaf
{
    /// <summary>
    /// Gets or sets the list of values that this filter criterion checks against.
    /// </summary>
    public object?[] Values { get; init; } = [];

    /// <inheritdoc/>
    public override bool Matches(object? value)
    {
        foreach (var listValue in Values)
        {
            if (Equals(value, listValue))
            {
                return true;
            }
        }

        if (value is not null && TryCoerce(value, out var numericValue))
        {
            foreach (var listValue in Values)
            {
                if (listValue is not null && TryCoerce(listValue, out var numericListValue))
                {
                    if (numericValue == numericListValue)
                    {
                        return true;
                    }
                }
            }
        }

        return false;
    }

    /// <inheritdoc/>
    public override void Accept(IFilterCriterionVisitor visitor)
    {
        ArgumentNullException.ThrowIfNull(visitor);
        visitor.Visit(this);
    }
}

/// <summary>
/// Represents a filter criterion that matches only if the value is null.
/// </summary>
public class IsNullCriterion : FilterCriterionLeaf
{
    /// <inheritdoc/>
    public override bool Matches(object? value)
    {
        return value is null;
    }

    /// <inheritdoc/>
    public override void Accept(IFilterCriterionVisitor visitor)
    {
        ArgumentNullException.ThrowIfNull(visitor);
        visitor.Visit(this);
    }
}

/// <summary>
/// Represents a base class for filter criteria that operate on string values.
/// </summary>
public abstract class StringFilterCriterion : FilterCriterionLeaf
{
    /// <summary>
    /// Gets or sets the value that this filter criterion checks against.
    /// </summary>
    public object? Value { get; init; }

    /// <inheritdoc/>
    public override bool Matches(object? value)
    {
        if (value is null || Value is null)
        {
            return false;
        }

        return Matches(value!.ToString()!, Value.ToString()!);
    }

    /// <summary>
    /// Performs the specific string operation for this filter criterion.
    /// </summary>
    /// <param name="stringValue">The string value to check.</param>
    /// <param name="stringCriterion">The criterion string to check against.</param>
    /// <returns>True if the operation succeeds; otherwise, false.</returns>
    protected abstract bool Matches(string stringValue, string stringCriterion);
}

/// <summary>
/// Represents a filter criterion that checks if a string value starts with a specified value.
/// </summary>
public class StartsWithCriterion : StringFilterCriterion
{
    /// <inheritdoc/>
    protected override bool Matches(string stringValue, string stringCriterion)
    {
        ArgumentNullException.ThrowIfNull(stringValue);
        return stringValue.StartsWith(stringCriterion, StringComparison.OrdinalIgnoreCase);
    }

    /// <inheritdoc/>
    public override void Accept(IFilterCriterionVisitor visitor)
    {
        ArgumentNullException.ThrowIfNull(visitor);
        visitor.Visit(this);
    }
}

/// <summary>
/// Represents a filter criterion that checks if a string value does not start with a specified value.
/// </summary>
public class DoesNotStartWithCriterion : StringFilterCriterion
{
    /// <inheritdoc/>
    protected override bool Matches(string stringValue, string stringCriterion)
    {
        ArgumentNullException.ThrowIfNull(stringValue);
        return !stringValue.StartsWith(stringCriterion, StringComparison.OrdinalIgnoreCase);
    }

    /// <inheritdoc/>
    public override void Accept(IFilterCriterionVisitor visitor)
    {
        ArgumentNullException.ThrowIfNull(visitor);
        visitor.Visit(this);
    }
}

/// <summary>
/// Represents a filter criterion that checks if a string value ends with a specified value.
/// </summary>
public class EndsWithCriterion : StringFilterCriterion
{
    /// <inheritdoc/>
    protected override bool Matches(string stringValue, string stringCriterion)
    {
        ArgumentNullException.ThrowIfNull(stringValue);
        return stringValue.EndsWith(stringCriterion, StringComparison.OrdinalIgnoreCase);
    }

    /// <inheritdoc/>
    public override void Accept(IFilterCriterionVisitor visitor)
    {
        ArgumentNullException.ThrowIfNull(visitor);
        visitor.Visit(this);
    }
}

/// <summary>
/// Represents a filter criterion that checks if a string value does not end with a specified value.
/// </summary>
public class DoesNotEndWithCriterion : StringFilterCriterion
{
    /// <inheritdoc/>
    protected override bool Matches(string stringValue, string stringCriterion)
    {
        ArgumentNullException.ThrowIfNull(stringValue);
        return !stringValue.EndsWith(stringCriterion, StringComparison.OrdinalIgnoreCase);
    }

    /// <inheritdoc/>
    public override void Accept(IFilterCriterionVisitor visitor)
    {
        ArgumentNullException.ThrowIfNull(visitor);
        visitor.Visit(this);
    }
}

/// <summary>
/// Represents a filter criterion that checks if a string value contains a specified value.
/// </summary>
public class ContainsCriterion : StringFilterCriterion
{
    /// <inheritdoc/>
    protected override bool Matches(string stringValue, string stringCriterion)
    {
        ArgumentNullException.ThrowIfNull(stringValue);
        return stringValue.Contains(stringCriterion, StringComparison.OrdinalIgnoreCase);
    }

    /// <inheritdoc/>
    public override void Accept(IFilterCriterionVisitor visitor)
    {
        ArgumentNullException.ThrowIfNull(visitor);
        visitor.Visit(this);
    }
}

/// <summary>
/// Represents a filter criterion that checks if a string value does not contain a specified value.
/// </summary>
public class DoesNotContainCriterion : StringFilterCriterion
{
    /// <inheritdoc/>
    protected override bool Matches(string stringValue, string stringCriterion)
    {
        ArgumentNullException.ThrowIfNull(stringValue);
        return !stringValue.Contains(stringCriterion, StringComparison.OrdinalIgnoreCase);
    }

    /// <inheritdoc/>
    public override void Accept(IFilterCriterionVisitor visitor)
    {
        ArgumentNullException.ThrowIfNull(visitor);
        visitor.Visit(this);
    }
}

public partial class Worksheet
{
    /// <summary>
    /// Gets the auto filter for this sheet. Always non-null. Set its Range property to enable or disable.
    /// </summary>
    public AutoFilter AutoFilter { get; private set; } = default!;

    private readonly List<SheetFilter> filters = [];

    /// <summary>
    /// Gets the list of filters applied to the sheet.
    /// </summary>
    public IReadOnlyList<SheetFilter> Filters => filters;

    /// <summary>
    /// Adds a filter to the sheet.
    /// </summary>
    public void AddFilter(SheetFilter filter)
    {
        ArgumentNullException.ThrowIfNull(filter);

        var existingFilter = GetFilter(filter.Range);

        if (existingFilter is not null)
        {
            RemoveFilter(existingFilter);
        }

        filters.Add(filter);

        ApplyFilters();
    }

    /// <summary>
    /// Retrieves a filter for the specified range.
    /// </summary>
    public SheetFilter? GetFilter(RangeRef range)
    {
        if (range == RangeRef.Invalid)
        {
            return null;
        }

        foreach (var filter in filters)
        {
            if (filter.Range == range)
            {
                return filter;
            }
        }

        return null;
    }

    /// <summary>
    /// Removes a specific filter from the sheet.
    /// </summary>
    /// <param name="filter">The filter to remove.</param>
    /// <returns>True if the filter was found and removed; otherwise, false.</returns>
    public bool RemoveFilter(SheetFilter filter)
    {
        ArgumentNullException.ThrowIfNull(filter);

        var removed = filters.Remove(filter);

        if (removed)
        {
            ApplyFilters();
        }

        return removed;
    }

    /// <summary>
    /// Removes a filter from the sheet at the specified index.
    /// </summary>
    /// <param name="index">The index of the filter to remove.</param>
    public void RemoveFilterAt(int index)
    {
        if (index < 0 || index >= filters.Count)
        {
            throw new ArgumentOutOfRangeException(nameof(index));
        }

        filters.RemoveAt(index);

        ApplyFilters();
    }

    /// <summary>
    /// Clears all filters and shows all rows.
    /// </summary>
    public void ClearFilters()
    {
        filters.Clear();
        Rows.ShowAll();
    }

    private void ApplyFilters()
    {
        Rows.BeginUpdate();

        Rows.ShowAll();

        foreach (var filter in filters)
        {
            Filter(filter.Range, filter.Criterion);
        }

        Rows.EndUpdate();
    }

    // Re-evaluate active filters by value after a sort (Excel parity). No-op when no filters
    // are active, so manually hidden rows are never disturbed.
    internal void ReapplyFilters()
    {
        if (filters.Count > 0)
        {
            ApplyFilters();
        }
    }

    private void Filter(RangeRef range, FilterCriterion criterion)
    {
        if (range == RangeRef.Invalid)
        {
            return;
        }

        criterion.OnApply(this, range);

        // Excel treats the first row as a header and excludes it from filtering
        var startRow = range.Start.Row + 1;
        var endRow = range.End.Row;

        for (var row = startRow; row <= endRow; row++)
        {
            if (!criterion.Matches(this, row))
            {
                Rows.Hide(row);
            }
        }
    }
}
