using System;
using System.Collections.Generic;

namespace Sonex.Client.Data;

public class DataTable
{
    private readonly List<DataColumnBase> _columns = new();
    private readonly Dictionary<string, int> _columnIndexes = new(StringComparer.OrdinalIgnoreCase);

    public IReadOnlyList<DataColumnBase> Columns => _columns;
    public int ColumnCount => _columns.Count;
    public int RowCount { get; private set; }

    public T AddColumn<T>(T column) where T : DataColumnBase
    {
        ArgumentNullException.ThrowIfNull(column);

        if (_columnIndexes.ContainsKey(column.Key))
            throw new InvalidOperationException($"Column '{column.Key}' already exists.");

        if (column.RowCount > RowCount)
            throw new InvalidOperationException(
                $"Column '{column.Key}' has {column.RowCount} rows, but table has {RowCount} rows.");

        while (column.RowCount < RowCount)
            column.AddEmpty();

        _columnIndexes.Add(column.Key, _columns.Count);
        _columns.Add(column);

        return column;
    }

    public DataColumnBase AddColumn(string key, string header, Type dataType)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(key);
        ArgumentException.ThrowIfNullOrWhiteSpace(header);
        ArgumentNullException.ThrowIfNull(dataType);

        Type type = Nullable.GetUnderlyingType(dataType) ?? dataType;

        DataColumnBase column = CreateColumn(key, header, type);
        AddColumn(column);

        return column;
    }

    public int AddRow()
    {
        foreach (var column in _columns)
            column.AddEmpty();

        RowCount++;
        return RowCount - 1;
    }

    public void AddRows(int count)
    {
        if (count < 0)
            throw new ArgumentOutOfRangeException(nameof(count));

        for (int i = 0; i < count; i++)
            AddRow();
    }

    public string? GetValue(int rowIndex, int columnIndex)
    {
        ValidateRowIndex(rowIndex);
        ValidateColumnIndex(columnIndex);

        return _columns[columnIndex].GetValue(rowIndex);
    }

    public string? GetValue(int rowIndex, string columnKey)
    {
        return GetValue(rowIndex, GetColumnIndex(columnKey));
    }

    public void SetValue(int rowIndex, int columnIndex, string? value)
    {
        ValidateRowIndex(rowIndex);
        ValidateColumnIndex(columnIndex);

        _columns[columnIndex].SetValue(rowIndex, value);
    }

    public void SetValue(int rowIndex, string columnKey, string? value)
    {
        SetValue(rowIndex, GetColumnIndex(columnKey), value);
    }

    public object? GetRawValue(int rowIndex, int columnIndex)
    {
        ValidateRowIndex(rowIndex);
        ValidateColumnIndex(columnIndex);

        return _columns[columnIndex].GetRawValue(rowIndex);
    }

    public object? GetRawValue(int rowIndex, string columnKey)
    {
        return GetRawValue(rowIndex, GetColumnIndex(columnKey));
    }

    public void SetRawValue(int rowIndex, int columnIndex, object? value)
    {
        ValidateRowIndex(rowIndex);
        ValidateColumnIndex(columnIndex);

        _columns[columnIndex].SetRawValue(rowIndex, value);
    }

    public void SetRawValue(int rowIndex, string columnKey, object? value)
    {
        SetRawValue(rowIndex, GetColumnIndex(columnKey), value);
    }

    public DataColumnBase GetColumn(int columnIndex)
    {
        ValidateColumnIndex(columnIndex);
        return _columns[columnIndex];
    }

    public DataColumnBase GetColumn(string key)
    {
        return _columns[GetColumnIndex(key)];
    }

    public int GetColumnIndex(string key)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(key);

        if (_columnIndexes.TryGetValue(key, out int index))
            return index;

        throw new KeyNotFoundException($"Column '{key}' was not found.");
    }

    private static DataColumnBase CreateColumn(string key, string header, Type type)
    {
        if (type == typeof(string)) return new DataColumnString(key, header);
        if (type == typeof(int)) return new DataColumnInt(key, header);
        if (type == typeof(uint)) return new DataColumnUInt(key, header);
        if (type == typeof(long)) return new DataColumnLong(key, header);
        if (type == typeof(ulong)) return new DataColumnULong(key, header);
        if (type == typeof(short)) return new DataColumnShort(key, header);
        if (type == typeof(float)) return new DataColumnFloat(key, header);
        if (type == typeof(double)) return new DataColumnDouble(key, header);
        if (type == typeof(decimal)) return new DataColumnDecimal(key, header);
        if (type == typeof(bool)) return new DataColumnBool(key, header);
        if (type == typeof(DateTime)) return new DataColumnDateTime(key, header);
        if (type == typeof(DateOnly)) return new DataColumnDateOnly(key, header);
        if (type == typeof(TimeOnly)) return new DataColumnTimeOnly(key, header);
        if (type == typeof(TimeSpan)) return new DataColumnTimeSpan(key, header);
        if (type == typeof(Guid)) return new DataColumnGuid(key, header);
        if (type == typeof(byte[])) return new DataColumnByteArray(key, header);

        throw new NotSupportedException($"Type '{type.Name}' is not supported.");
    }

    private void ValidateRowIndex(int rowIndex)
    {
        if (rowIndex < 0 || rowIndex >= RowCount)
            throw new ArgumentOutOfRangeException(nameof(rowIndex));
    }

    private void ValidateColumnIndex(int columnIndex)
    {
        if (columnIndex < 0 || columnIndex >= _columns.Count)
            throw new ArgumentOutOfRangeException(nameof(columnIndex));
    }
}