@using System.Diagnostics
@using System.Drawing
@using System.Dynamic
@using System.Reflection
@using System.Text.RegularExpressions
@using ChartJs.Blazor
@using ChartJs.Blazor.LineChart
@using log4net
@using Rms.Risk.Mango.Pivot.Core
@using Rms.Risk.Mango.Pivot.Core.Models

@inject IJSRuntime           Js
@inject NavigationManager    NavigationManager
@inject IPivotSharingService PivotSharingService

@*
 *                                dbMango
 *
 * Copyright 2025 Deutsche Bank AG
 * SPDX-License-Identifier: Apache-2.0
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *@

<style>
    .chart-canvas {
        width: 1024px;
        height: 470px;
        margin-bottom: 50px;
    }

    .glow-pos {
        background: #279320;
    }

    .glow-neg {
        background: #723e36;
    }

    .glow-last {
        border-bottom-color: yellow;
        border-bottom-style: double;
    }
</style>

<div class="@ChartClass">
    <div class="chart-canvas ml-3">
        <Chart Config="@ChartConfig"/>
    </div>
</div>

@if (_pivotRefreshing)
{
    <div class="d-flex">
        <ProgressSpinner>Loading...</ProgressSpinner>
    </div>
}
else if (_pivotRows is { Length: > 0 } 
 && (ShowIfEmpty || (!_pivotData?.Get(0, 0)?.ToString()?.Equals("No results") ?? false)))
{
    @if (!string.IsNullOrEmpty(Title))
    {
        <h2>@Title</h2>
    }
    @if (_pivotData?.Count == PivotMaxReturnedRows )
    {
        <div class="alert alert-danger w-75" role="alert">
            Pivot results are limited to returning a maximum of @PivotMaxReturnedRows rows
        </div>
    }
    <TableControl Class="table table-hover table-striped table-forge fit-content" 
                  Items="@_pivotRows" 
                  PageSize="@Rows" 
                  @bind-FilteredItems="FilteredRows"
                  CellStyle="GetCellStyle"
                  GetCellClassCallback="GetCellClassCallback"
                  Totals="@VisibleTotals"
                  @bind-SortMode="SortMode"
                  @bind-CurrentSortColumn="SortColumn"
                  >
        @foreach (var field in _pivotRows[0].GetDynamicMemberNames())
        {
            
            var headerStyle = HeaderStyles.GetValueOrDefault(field, "");
            
            <TableColumnControl Name="@field" Field="@field" Format="@_pivotRows[0].GetFormat(field)" ShowTotals="@_pivotRows[0].ShouldShowTotals(field)" Class="@ColumnClass" HeaderStyle="@headerStyle">
                <Template>
                    <span @onclick="@(() => OnCellClick((DynamicObject)context.Row, field))">
                        @TableControl.ConvertToString(TableControl.GetLambdaForValue(field)(context.Row), _pivotRows[0].GetFormat(field))
                    </span>
                </Template>
            </TableColumnControl>
        }
    </TableControl>
}

@code{
    private static readonly ILog _log = LogManager.GetLogger(MethodBase.GetCurrentMethod()!.DeclaringType!);

    [CascadingParameter] public IModalService Modal { get; set; } = null!;

    public static int PivotMaxReturnedRows = 500_000;

    [Parameter]
    public IPivotedData? PivotData
    {
        get => _pivotData;
        set
        {
            if (_pivotData == value)
                return;

            _pivotData = value;
            _totals.Clear();

            if (_pivotData == null)
            {
                _pivotRows = [];
                InvokeAsync(StateHasChanged);
                return;
            }

            _shadowHeaders = _pivotData.GetColumnPositions();

            _descriptorsCache.Clear();
            _fieldDescriptorsCache.Clear();

            _pivotRows = Enumerable
                .Range(0, _pivotData.Count )
                .Select(x => new PivotRow(_pivotData, x, _shadowHeaders, GetColumnDescriptorInternal, GetFieldDescriptorInternal))
                .ToArray()
                ;

            _totals.Update(_pivotData!);

            try
            {
                ChartHelper.UpdateLineChart(CurrentPivot!, _pivotData, x => _pivotRows[0].GetFormat(x));
            }
            catch (Exception)
            {
                // ignore
            }

            PivotDataChanged.InvokeAsync(_pivotData);
            InvokeAsync(StateHasChanged);
        }
    }

    [Parameter] public EventCallback<IPivotedData> PivotDataChanged      { get; set; }

    /// <summary>
    /// Unlike Pivot which contains pivot that would be executed CurrentPivot holds definition
    /// that is already shown and corresponding to PivotData.
    /// Always set PivotData and CurrentPivot at the same time.
    /// </summary>
    [Parameter]
    public PivotDefinition? CurrentPivot
    {
        get;
        set
        {
            if (value == null || field == value)
                return;
            field = value;
            CurrentPivotChanged.InvokeAsync(field);
        }
    }

    [Parameter] public EventCallback<PivotDefinition>                      CurrentPivotChanged      { get; set; }


    [Parameter] public GroupedCollection? SelectedCollectionNode          { get; set; }

    [Parameter]
    public GroupedPivot? SelectedPivotNode
    {
        get => field;
        set 
        {
            if (field == value)
                return;
            field  = value;
            Filter = string.IsNullOrWhiteSpace(field?.Pivot.Filter) 
                ? new() 
                : FilterExpressionTree.ParseJson(field.Pivot.Filter)
                ;
        }
    }

    [Parameter] public int Rows                                           { get; set; } = 35;
    [Parameter] public FilterExpressionTree.ExpressionGroup? ExtraFilter  { get; set; }
    [Parameter] public bool IsExportEnabled
    {
        get => !(CurrentPivot == null || _pivotRows is not { Length: > 0 });
        // ReSharper disable once ValueParameterNotUsed
        set
        {
            // ignore
        }
    }
    [Parameter] public EventCallback<bool> IsExportEnabledChanged       { get; set; }

    [Parameter, EditorRequired] public IPivotTableDataSource      PivotService { get; set; } = null!;
    [Parameter] public Navigation<NavigationUnit>?                Navigation   { get; set; }
    [Parameter, EditorRequired] public List<GroupedCollection>    Collections  { get; set; } = null!;


    [Parameter]
    public DateTime LastRefresh
    {
        get;
        set
        {
            if (field == value)
                return;
            field = value;
            LastRefreshChanged.InvokeAsync(field);
        }
    }

    [Parameter]
    public TimeSpan LastRefreshElapsed
    {
        get;
        set
        {
            if (field == value)
                return;
            field = value;
            LastRefreshElapsedChanged.InvokeAsync(field);
        }
    }

    [Parameter] public EventCallback<DateTime> LastRefreshChanged        { get; set; }
    [Parameter] public EventCallback<TimeSpan> LastRefreshElapsedChanged { get; set; }

    [Parameter] public Func<string, string, PivotDefinition?>? GetPivotDefinition                     { get; set; } //by default DefaultGetPivotDefinition;
    [Parameter] public Func<string, string, Task<Tuple<string,PivotDefinition>?>> GetCustomDrilldown  { get; set; }= (_,_) => Task.FromResult<Tuple<string,PivotDefinition>?>(null);
    [Parameter] public Func<string /*columnName*/, PivotColumnDescriptor?> GetColumnDescriptor        { get; set; } = _ => null;
    [Parameter] public Func<string /*columnName*/, PivotFieldDescriptor?> GetFieldDescriptor          { get; set; } = _ => null;
    [Parameter] public Func<DynamicObject, string, Task<bool>> HandleCellClick                        { get; set; } =(_,_) => Task.FromResult(false);
    [Parameter] public Func<
        GroupedPivot /*pivot*/,
        FilterExpressionTree.ExpressionGroup? /*userFilter*/,
        List<GroupedPivot> /*pivots*/, 
        NavigationUnit
    > CreateNavigationUnit { get; set; } = DefaultCreateNavigationUnit;
    [Parameter] public bool IsReadOnly { get; set; }

    [Parameter] public bool UseCache        
    { 
        get;
        set
        {
            if (field == value)
                return;
            field = value;
            UseCacheChanged.InvokeAsync(field);
        }
    } = true;

    [Parameter] public EventCallback<bool> UseCacheChanged { get; set; }

    [Parameter] public string Title { get; set; } = "";
    [Parameter] public bool ShowIfEmpty { get; set; } = true;

    [Parameter] public Dictionary<string, string> HeaderStyles { get; set; } = [];

    private FilterExpressionTree.ExpressionGroup Filter { get; set; } = new();


    public Task CopyCsv()                                                  => CopyCsvInternal();
    public Task ExportCsv(string destFileName)                             => ExportCsvInternal(destFileName);
    public Task RunPivot(bool transpose = false)                           => IsReadOnly ? Task.CompletedTask : RunPivotInternal(transpose);

    public void Navigate(PivotDefinition def, NavigationUnit data)
    {
        if (IsReadOnly)
            return;
        NavigateInternal(def, data);
    }

    public Task ShowDefaultDocument(KeyValuePair<string, object>[] fields) => IsReadOnly ? Task.CompletedTask : ShowDefaultDocumentInternal(fields);


    private          PivotRow[]                               ? _pivotRows;
    private          IPivotedData                             ? _pivotData;
    private readonly MinMaxCache                                _totals = new();
    private          string                                   ? _prevCollection;
    private          GroupedCollection                        ? _currentCollection;
    private          DrilldownSupport                         ? _drilldownSupport;
    private          bool                                       _pivotRefreshing;
    private          PivotColumnDescriptor[]                  ? _descriptors;
    private          Dictionary<string, PivotFieldDescriptor> ? _fieldTypes;
    private          List<dynamic>                              _filteredRows  = [];
    private readonly DelayedExecution                           _delayedUpdate = new(TimeSpan.FromMilliseconds(1500));
    private          Dictionary<string, int>                    _shadowHeaders = [];

    private bool                                  ShowTotals        => SelectedPivotNode?.Pivot.ShowTotals ?? false;
    private IMinMaxCache?                         VisibleTotals     => ShowTotals ? _totals : null;
    private List<GroupedPivot>                    Pivots            => SelectedCollectionNode?.Pivots ?? [];
    private HashSet<string>                       AllDataFields     => SelectedCollectionNode?.DataFields ?? [];
    private HashSet<string>                       AllKeyFields      => SelectedCollectionNode?.KeyFields ?? [];
    private LineConfig                            ChartConfig       => ChartHelper.ChartConfig;
    private ChartHelperForPivot                   ChartHelper       { get; }      = new();

    private string ChartClass => !ChartHelper.IsLineChart(CurrentPivot!, _pivotData!)
        ? "d-none"
        : ""
        ;

    private string ColumnClass
    {
        get 
        {
            //This is a little awkward, usually the table control will auto wrap long headers when they contain spaces, but we have some pivots currently that have
            //a combination of long columns with and without spaces. Wrapping looks weird here, so we want to turn off wrapping if we can
            //detect any column which has a long name without spaces

            if (_pivotRows != null && 
                (_pivotRows[0].GetDynamicMemberNames().Count() <= 16 
                  || _pivotRows[0].GetDynamicMemberNames().Any(n => n.Length >= 10 && !n.Contains(" "))
                )
               )
                return "table-nowrap";
            else 
                return string.Empty;
        }
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await base.OnAfterRenderAsync(firstRender);

        if (!firstRender || IsReadOnly)
            return;

        try
        {
            Navigation ??= new(Navigate);

            _drilldownSupport = new(Collections)
            {
                ShowDocument        = ShowDefaultDocument,
                MessageBoxShow      = m => ModalDialogUtils.ShowInfoDialog(Modal, "Pivot", m),
                MessageBoxShowYesNo = async (m, t) =>
                {
                    var res = await ModalDialogUtils.ShowConfirmationDialog(Modal, t, m);
                    return !res.Cancelled;
                },
                ShowException      = ex => ModalDialogUtils.ShowExceptionDialog(Modal, "Pivot", ex),
                GetPivotDefinition = GetPivotDefinition ?? DefaultGetPivotDefinition,
                GetCustomDrilldown = GetCustomDrilldown
            };

            await InvokeAsync(StateHasChanged);
        }
        catch (Exception e)
        {
            await ModalDialogUtils.ShowExceptionDialog(Modal, "Error fetching collections from pivot service", e);
        }
    }

    private PivotDefinition? DefaultGetPivotDefinition(string pivotName, string _ /*collectionName*/) => 
        _currentCollection?.Pivots?.FirstOrDefault(x => x?.Pivot?.Name?.Equals(pivotName, StringComparison.OrdinalIgnoreCase) ?? false)?.Pivot;

    private async Task<bool> OnCellClick(DynamicObject row, string fieldName)
    {
        try
        {
            if (HandleCellClick != null && await HandleCellClick(row, fieldName))
                return true;
        }
        catch (Exception e)
        {
            await ModalDialogUtils.ShowExceptionDialog(Modal, $"Error drilling down into {fieldName}", e);
        }

        if ( IsReadOnly || _drilldownSupport == null)
            return true;

        if (CurrentPivot == null)
            return false;

        var displayToRealName = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
        if ( CurrentPivot.RenameColumn?.Count > 0 && PivotData is ArrayBasedPivotData { HeadersMap: { } } apd)
        {
            // check for renamed columns
            foreach ( var item in apd.HeadersMap )
                displayToRealName[item.DesplayHeader] = item.OrigHeader;
        }

        var res = await _drilldownSupport.Drilldown(
            PivotService!,
            SelectedCollectionNode!.CollectionNameWithPrefix,
            fieldName,
            displayToRealName,
            ((PivotRow)row).GetDynamicMemberNames().ToArray(),
            CurrentPivot,
            x => TableControl.GetDynamicMember(row, x),
            AllDataFields,
            AllKeyFields
            );

        if (res == null)
            return false;

        var (destCollection, destPivot) = res;

        if (destCollection != SelectedCollectionNode.CollectionNameWithPrefix)
            throw new NotImplementedException("destCollection != Collection");

        try
        {
            _pivotRefreshing = true;
            await InvokeAsync(StateHasChanged);

            await RunPivot(destPivot, Filter);
        }
        catch (Exception e)
        {
            await ModalDialogUtils.ShowExceptionDialog(Modal, "Error fetching pivot data", e);
        }
        finally
        {
            _pivotRefreshing = false;
            await InvokeAsync(StateHasChanged);
        }
        return true;
    }

    private async Task ShowDefaultDocumentInternal(KeyValuePair<string, object>[] fields)
    {
        var filter = ExtraFilter;
        var doc    = await PivotService.GetDocumentAsync(_currentCollection!.CollectionNameWithPrefix, fields, filter);

        await ModalDialogUtils.ShowTextDialog(
            Modal,
            "Pivot",
            doc,
            "Press F11 to enter fullscreen mode. Use ESC to exit it."
            );
    }

    private bool IsRefreshEnabled =>
        ! IsReadOnly
        && !string.IsNullOrWhiteSpace(SelectedCollectionNode?.CollectionNameWithPrefix)
        && SelectedPivotNode?.Pivot != null
        ;

    private async Task RunPivotInternal(bool transpose)
    {
        if (!IsRefreshEnabled)
            return;

        _log.Debug($"OnRefreshPivot Collection=\"{SelectedCollectionNode?.CollectionNameWithPrefix}\" Pivot=\"{SelectedPivotNode?.Pivot?.Name}\" ExtraFilter=\"{ExtraFilter}\"");

        try
        {
            var pivot = SelectedPivotNode?.Pivot;
            if (pivot == null)
            {
                await ModalDialogUtils.ShowInfoDialog(
                    Modal,
                    "Pivot",
                    "Pivot is not found.",
                    new()
                    {
                        {"Pivot"      , SelectedPivotNode?.Pivot?.Name},
                        {"Collection" , SelectedCollectionNode?.CollectionNameWithPrefix   },
                        {"ExtraFilter", ExtraFilter?.ToString()   }
                    });
                return;
            }

            // clear previous back/forward stack
            Navigation?.Clear();

            _pivotRefreshing = true;
            await InvokeAsync(StateHasChanged);

            await RunPivotInternal(pivot, Filter, transpose: transpose);
        }
        catch (Exception e)
        {
            await ModalDialogUtils.ShowExceptionDialog(Modal, "Error fetching pivot data", e);
        }
        finally
        {
            _pivotRefreshing = false;
            await InvokeAsync(StateHasChanged);
        }
    }

    public async Task RunPivot(PivotDefinition pivotDef, FilterExpressionTree.ExpressionGroup? userFilter, bool addToNavigation = true, bool transpose = false)
    {
        if (IsReadOnly)
            return;

        try
        {
            _pivotRefreshing = true;
            await InvokeAsync(StateHasChanged);

            await RunPivotInternal(pivotDef, userFilter, addToNavigation, transpose);
        }
        catch (Exception e)
        {
            await ModalDialogUtils.ShowExceptionDialog(Modal, "Error fetching pivot data", e);
        }
        finally
        {
            _pivotRefreshing = false;
            await InvokeAsync(StateHasChanged);
        }
    }

    /*
    ___________________________________________________________________________________________________________



                                  _____             _____ _            _
                                 |  __ \           |  __ (_)          | |
                                 | |__) |   _ _ __ | |__) |__   _____ | |_
                                 |  _  / | | | '_ \|  ___/ \ \ / / _ \| __|
                                 | | \ \ |_| | | | | |   | |\ V / (_) | |_
                                 |_|  \_\__,_|_| |_|_|   |_| \_/ \___/ \__|


     ___________________________________________________________________________________________________________
     */
    private async Task RunPivotInternal(
    PivotDefinition                       pivotDef,
    FilterExpressionTree.ExpressionGroup? userFilter,
    bool                                  addToNavigation = true,
    bool                                  transpose       = false
)
    {
        // userFilter is a combination of drilldown filters and pivot-defined filter
        // i.e. it excluding an ExtraFilter (the one that set in the UI)

        if (IsReadOnly)
            return;
        if (pivotDef == null) 
            throw new ArgumentNullException(nameof(pivotDef));
        if (!await WaitForSelectedCollection())
            throw new ApplicationException("SelectedCollectionNode is null");

        var sw        = Stopwatch.StartNew();
        var p         = Process.GetCurrentProcess();
        var memBefore = p.WorkingSet64;

        pivotDef = pivotDef.Clone();
        _descriptorsCache.Clear();

        if (_prevCollection != SelectedCollectionNode!.CollectionNameWithPrefix)
        {
            _descriptors    = SelectedCollectionNode.ColumnDescriptors;
            _fieldTypes = SelectedCollectionNode.FieldTypes;
            _prevCollection = SelectedCollectionNode.CollectionNameWithPrefix;
        }

        var fieldTypes = GetAllFields();

        pivotDef.Filter = CombineFilters( pivotDef.Filter, userFilter, fieldTypes);

        var data = await PivotService.PivotAsync(SelectedCollectionNode.CollectionNameWithPrefix, pivotDef, ExtraFilter, !UseCache);

        if (data.Count == PivotMaxReturnedRows)
        {
            await ModalDialogUtils.ShowInfoDialog(Modal, "Pivot Results Truncated", 
                $"Pivot results are limited to returning a maximum of {PivotMaxReturnedRows} rows");
        }

        CurrentPivot      = pivotDef;

        UseCache = true; // reset UseCache
        _currentCollection = SelectedCollectionNode;

        if ((pivotDef.RenameColumn?.Count ?? 0) > 0)
            RenameColumns(data, pivotDef.RenameColumn!, false);

        data = Make2DPivot(CurrentPivot, data);
        if (transpose && data.Count > 0 && data.Get(0, 0)?.ToString()?.Equals("No results") != true)
            data = Transpose(data);

        PivotData = data;
        _totals.Clear();
        _filteredRows = [];

        if (addToNavigation)
        {
            Navigation?.Add(pivotDef, CreateNavigationUnit(SelectedPivotNode!, userFilter, Pivots));
        }

        sw.Stop();

        LastRefresh        = DateTime.Now;
        LastRefreshElapsed = sw.Elapsed;

        await LastRefreshChanged      .InvokeAsync(LastRefresh);
        await IsExportEnabledChanged  .InvokeAsync(IsExportEnabled);

        p = Process.GetCurrentProcess();
        var memAfter = p.WorkingSet64;

        _log.Debug($"Received Rows={PivotData.Count} Pivot=\"{pivotDef.Name}\" Filter=\"{userFilter}\" MemBefore={NumbersUtils.ToHumanReadable(memBefore)} " +
                   $"MemAfter={NumbersUtils.ToHumanReadable(memAfter)} " +
                   $"MemDiff={NumbersUtils.ToHumanReadable(memAfter - memBefore)} User=\"{PivotService.User}\"");

    }

    private async Task<bool> WaitForSelectedCollection()
    {
        // SelectedCollectionNode can be not set yet if the parent component is setting it
        // asynchronously. Wait for a while.
        // This is actually a race condition. Parent initialing Collections loading in background while in the code
        // we already think that Collections are loaded and SelectedCollectionNode is set.
        if (SelectedCollectionNode != null)
            return true;

        var timeout = TimeSpan.FromSeconds(10);
        try
        {
            var cts = new CancellationTokenSource(timeout);
            while (SelectedCollectionNode == null && !cts.IsCancellationRequested)
                await Task.Delay(100, cts.Token);
        }
        catch (OperationCanceledException)
        {
            // ignore
        }
        return SelectedCollectionNode != null;
    }

    private string CombineFilters(string pivotFilter, FilterExpressionTree.ExpressionGroup? userFilter, Dictionary<string, Type> fieldTypes)
    {
        if (string.IsNullOrWhiteSpace(pivotFilter))
            return userFilter?.ToJson(fieldTypes) ?? "";

        if (userFilter?.IsEmpty ?? true)
            return pivotFilter;

        var baseFilter = FilterExpressionTree.ParseJson(pivotFilter);
        var resTree = new FilterExpressionTree.ExpressionGroup()
            {
                Condition = FilterExpressionTree.ExpressionGroup.ConditionType.And,
            };
        resTree.Children.Add(baseFilter);
        resTree.Children.Add(userFilter);

        return resTree.ToJson(fieldTypes);
    }

    private void RenameColumns(IPivotedData data, Dictionary<string, string> pivotDefRenameColumn, bool capitaliseColumns)
    {
        if ( data is not ArrayBasedPivotData d )
            throw new ApplicationException("ArrayBasedPivotData expected");

        var rx = pivotDefRenameColumn.Select(x => new {Rx = new Regex(x.Key), Src=x.Key, Dest = x.Value}).ToArray();

        d.UpdateHeaders(header =>
        {
            foreach (var item in rx)
            {
                var m = item.Rx.Match(header);
                if (!m.Success)
                    continue;
                var dest = Regex.Replace(header, item.Src, item.Dest);
                return dest;
            }

            if (capitaliseColumns)
            {
                if (char.IsLower(header[0]) && header.Length > 1)
                {
                    return header[0].ToString().ToUpper() + header[1..];
                }
            }

            return header;
        });
    }

    private static NavigationUnit DefaultCreateNavigationUnit(GroupedPivot pivot, FilterExpressionTree.ExpressionGroup? userFilter, List<GroupedPivot> pivots )
        => new ()
        {
            Filter            = userFilter,
            Pivots            = pivots,
            SelectedPivotNode = pivot
        };

    private class TransposedData : IPivotedData
    {
        private readonly IPivotedData _originalData;

        private readonly string[] _invertedOrigHeaders;

        public TransposedData(IPivotedData originalData)
        {
            _originalData = originalData;

            _invertedOrigHeaders = _originalData.Headers.ToArray();

            Headers = new [] {_invertedOrigHeaders[0]}
                .Concat(
                    Enumerable.Range(0, originalData.Count)
                    .Select(x => originalData.Get(0, x)?.ToString() ?? "")
                )
                .ToList();
            
            Id = $"{originalData.Id}-Transposed";
        }

        public string Id { get; set; }

        public IReadOnlyCollection<string> Headers { get; }

        public int Count => _originalData.Headers.Count-1;

        public object? Get(int col, int row)
        {
            if (col == 0)
            {
                if (row < 0 || row >= _invertedOrigHeaders.Length - 1)
                    return "???";
                return _invertedOrigHeaders[row + 1];
            }

            return _originalData.Get(row+1, col-1);
        }

        public Type GetColumnType(int col) => typeof(double);

        public IPivotedData Filter(Func<int, bool> filter) => new TransposedData(_originalData.Filter(filter));
    }

    private IPivotedData Transpose(IPivotedData data) => new TransposedData(data);

    private async void NavigateInternal(PivotDefinition def, NavigationUnit data)
    {
        try
        {
            if (IsReadOnly)
                return;
            try
            {
                _pivotRefreshing = true;

                Filter            = data.Filter ?? new FilterExpressionTree.ExpressionGroup();
                SelectedPivotNode = data.SelectedPivotNode;

                await InvokeAsync(StateHasChanged);

                await RunPivot(def, null, false);
            }
            catch (Exception e)
            {
                await ModalDialogUtils.ShowExceptionDialog(Modal, "Error fetching pivot data", e);
            }
            finally
            {
                _pivotRefreshing = false;
                await InvokeAsync(StateHasChanged);
            }
        }
        catch (Exception e)
        {
            _log.Error(e.Message, e);
        }
    }


    private readonly Dictionary<string, PivotColumnDescriptor> _descriptorsCache      = [];
    private readonly Dictionary<string, PivotFieldDescriptor?> _fieldDescriptorsCache = [];

    private readonly PivotColumnDescriptor _defaultPivotColumnDescriptor = new()
    {
        Format              = "N0",
        NameRegexString     = ".*",
        Background          = Color.FromName(Night.Background),
        AlternateBackground = Color.FromName(Night.BackgroundLight)
    };

    private TableControl.SortModeType SortMode   { get; set; }
    private string?                   SortColumn { get; set; }

    private PivotColumnDescriptor GetColumnDescriptorInternal(string columnName)
    {
        if (_descriptorsCache.TryGetValue(columnName, out var desc))
            return desc;

        _descriptors ??= SelectedCollectionNode?.ColumnDescriptors;

        desc = GetColumnDescriptor(columnName)
            ?? _descriptors?.FirstOrDefault(x => x.NameRegex.IsMatch(columnName)) 
            ?? _defaultPivotColumnDescriptor;

        _descriptorsCache[columnName] = desc;
        return desc;
    }

    private PivotFieldDescriptor? GetFieldDescriptorInternal(string columnName)
    {
        if (_fieldDescriptorsCache.TryGetValue(columnName, out var desc))
            return desc;

        if ( SelectedCollectionNode == null )
            return null;

        _fieldTypes ??= SelectedCollectionNode.FieldTypes;

        desc = GetFieldDescriptor?.Invoke(columnName);
        if (desc == null)
            _fieldTypes?.TryGetValue(columnName, out desc);

        _fieldDescriptorsCache[columnName] = desc;
        return desc;
    }


    private static IPivotedData Make2DPivot(PivotDefinition def, IPivotedData data)
    {
        if (!def.Make2DPivot)
            return data;


        var rhArray = (def.Pivot2DRows ?? [])
            .Select(x => x.Replace("\r", "").Trim())
            .ToArray()
            ;

        var rowHeaders = new HashSet<string>(rhArray);

        if ((def.Pivot2DRows?.Count ?? 0) == 0
             || rowHeaders.Contains(def.Pivot2DColumn)
             || rowHeaders.Contains(def.Pivot2DData)
             || def.Pivot2DColumn == def.Pivot2DData)
            return data;

        try
        {
            var data2d = new TransposedPivotData(data, def.Pivot2DColumn, rhArray, def.Pivot2DData, CancellationToken.None);
            return data2d;
        }
        catch (Exception)
        {
            return data;
        }
    }

    private Dictionary<string, Type> GetAllFields()
    {
        if (SelectedCollectionNode?.FieldTypes != null)
            return SelectedCollectionNode.FieldTypes
                .ToDictionary(
                    x => x.Key, 
                    x => x.Value.Type 
                );

        var res = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase);

        if ((AllKeyFields?.Count ?? 0) > 0)
        {
            foreach (var k in AllKeyFields!.Where(k => !res.ContainsKey(k)))
            {
                res[k] = typeof(string);
            }
        }

        if ((AllDataFields?.Count ?? 0) > 0)
        {
            foreach (var k in AllDataFields!.Where(k => !res.ContainsKey(k)))
            {
                res[k] = typeof(double);
            }
        }

        // if ((_pivotData?.Length ?? 0) > 0)
        // {
        //     foreach (var k in _pivotData[0].GetDynamicMemberNames())
        //     {
        //         if (!res.ContainsKey(k))
        //             res[k] = typeof(double);
        //     }
        // }

        return res;
    }

    private async Task CopyCsvInternal()
    {
        if (!IsExportEnabled)
            return;

        try
        {
            var csv = _pivotRows![0].PivotData.CopyToCsv();
            await Js.InvokeVoidAsync("DashboardUtils.CopyToClipboard", csv);
        }
        catch (Exception e)
        {
            await ModalDialogUtils.ShowExceptionDialog(Modal, "Error copying pivot data", e);
        }
    }

    private async Task ExportCsvInternal(string destFileName)
    {
        if (!IsExportEnabled)
            return;

        try
        {
            await using var writer = new StringWriter();
            _pivotRows![0].PivotData.WriteToCsv(writer);
            var data = writer.ToString();

            var url = await PivotSharingService.ExportToCsv(
                destFileName,
                data
            );

            @* var url = await DownloadController.GetDownloadLink( *@
            @*     _storage, *@
            @*     _passwordManager, *@
            @*     _singleUseTokenService, *@
            @*     fileName => *@
            @*     { *@
            @*         _pivotRows![0].PivotData.WriteToCsv(fileName); *@
            @*         return Task.CompletedTask; *@
            @*     }, *@
            @*     destFileName *@
            @*     ); *@

            await Js.InvokeVoidAsync("open", $"{NavigationManager.BaseUri}{url}", "_blank");
        }
        catch (Exception e)
        {
            await ModalDialogUtils.ShowExceptionDialog(Modal, "Error exporting pivot data", e);
        }
    }

    // ReSharper disable once UnusedMember.Local
    private List<dynamic> FilteredRows
    {
        get => _filteredRows;
        set
        {
            _filteredRows = value;
            _totals.Update(_pivotData!, _filteredRows.OfType<PivotRow>().Select( x => x.Row).ToArray());
            
            if (_filteredRows.Count == 0 || !(CurrentPivot?.MakeLineChart ?? false))
                return;

            _delayedUpdate.Run(UpdateFilteredChart, "UpdateFilteredChart");
        }
    }

    private Task UpdateFilteredChart(CancellationToken token)
    {
        try
        {

            if (token.IsCancellationRequested)
                return Task.CompletedTask;

            var filteredPivot = new PivotFilteredView(_filteredRows.Cast<PivotRow>().ToList());

            if (token.IsCancellationRequested)
                return Task.CompletedTask;

            ChartHelper.UpdateLineChart(CurrentPivot!, filteredPivot, x => _pivotRows![0].GetFormat(x));
        }
        catch (Exception)
        {
            // ignore
        }
        return InvokeAsync(StateHasChanged);
    }

    public static string GetCellStyle(dynamic row, TableColumnControl col)
    {
        if ( row is not PivotRow r || string.IsNullOrWhiteSpace(col.Field) )
            return "";

        var desc = r.GetColumnDescriptor(col.Field);
        if ( desc == null )
            return "";

        // for some reason comparison with 0x00000 and Color.Black are not working
        if ( desc.Background is { R: 0, G: 0, B: 0 })
            return "";

        return $"background-color:#{desc.Background.A:x2}{desc.Background.R:x2}{desc.Background.G:x2}{desc.Background.B:x2}";
    }
    
    private string GetCellClassCallback(dynamic row, TableColumnControl col)
    {
        var cellClass = "";

        // make a copy as GetFieldDescriptor may take noticeable time
        var cp = CurrentPivot;

        if ( cp == null || cp.HighlightTopPercent <= 0.0 )
            return cellClass;

        if ( row is not PivotRow r || string.IsNullOrWhiteSpace(col.Field) )
            return cellClass;

        var mm = _totals.TryGet(col.Name);
        if ( mm == null )
            return cellClass;

        var desc = r.GetFieldDescriptor(col.Field);
        if ( desc != null && desc.Purpose != PivotFieldPurpose.Data )
            return cellClass;

        double val = GetCellValue(row, col.Field);

        cellClass = val switch
        {
            > 0 when val > mm.MaxValue - mm.MaxValue * cp.HighlightTopPercent / 100.0 =>
                $"{cellClass} glow-pos",
            < 0 when val < mm.MinValue - mm.MinValue * cp.HighlightTopPercent / 100.0 =>
                $"{cellClass} glow-neg",
            _ => cellClass
        };

        // NaN here most likely means string column
        if ( !double.IsNaN(val) && IsLastImportant(r, col.Name, mm) )
            cellClass += " glow-last";

        return cellClass;
    }

    private double GetCellValue(PivotRow row, string col)
    {
        if ( !_shadowHeaders.TryGetValue(col, out var idx) )
            return double.NaN;

        var val = row.PivotData.Get(idx, row.Row);
        return val switch
        {
            double d => d,
            int i    => i,
            _        => double.NaN
        };
    }

    private bool IsLastImportant(PivotRow row, string colName, MinMax mm)
    {
        if ( SortColumn != colName || SortMode != TableControl.SortModeType.DescendingAbsolute) 
            return false;

        var sum    = 0.0;
        var target = mm.AbsTotal * CurrentPivot!.HighlightTopPercent / 100.0;

        foreach ( var r in FilteredRows.OfType<PivotRow>() )
        {
            var val = GetCellValue(r, colName);
            if ( double.IsNaN(val) )
                return false;
            
            sum += Math.Abs(val);
            
            if ( sum >= target )
                return row.Row == r.Row;
        }

        return false;
    }

}
