// Copyright (C) 2023 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use size 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.

import m from 'mithril';
import {allUnique, range} from '../base/array_utils';
import {
  compareUniversal,
  comparingBy,
  ComparisonFn,
  SortableValue,
  SortDirection,
  withDirection,
} from '../base/comparison_utils';
import {scheduleFullRedraw} from './raf';
import {MenuItem, PopupMenu2} from './menu';
import {Button} from './button';

// For a table column that can be sorted; the standard popup icon should
// reflect the current sorting direction. This function returns an icon
// corresponding to optional SortDirection according to which the column is
// sorted. (Optional because column might be unsorted)
export function popupMenuIcon(sortDirection?: SortDirection) {
  switch (sortDirection) {
    case undefined:
      return 'more_horiz';
    case 'DESC':
      return 'arrow_drop_down';
    case 'ASC':
      return 'arrow_drop_up';
  }
}

export interface ColumnDescriptorAttrs<T> {
  // Context menu items displayed on the column header.
  contextMenu?: m.Child[];

  // Unique column ID, used to identify which column is currently sorted.
  columnId?: string;

  // Sorting predicate: if provided, column would be sortable.
  ordering?: ComparisonFn<T>;

  // Simpler way to provide a sorting: instead of full predicate, the function
  // can map the row for "sorting key" associated with the column.
  sortKey?: (value: T) => SortableValue;
}

export class ColumnDescriptor<T> {
  name: string;
  render: (row: T) => m.Child;
  id: string;
  contextMenu?: m.Child[];
  ordering?: ComparisonFn<T>;

  constructor(
    name: string,
    render: (row: T) => m.Child,
    attrs?: ColumnDescriptorAttrs<T>,
  ) {
    this.name = name;
    this.render = render;
    this.id = attrs?.columnId === undefined ? name : attrs.columnId;

    if (attrs === undefined) {
      return;
    }

    if (attrs.sortKey !== undefined && attrs.ordering !== undefined) {
      throw new Error('only one way to order a column should be specified');
    }

    if (attrs.sortKey !== undefined) {
      this.ordering = comparingBy(attrs.sortKey, compareUniversal);
    }
    if (attrs.ordering !== undefined) {
      this.ordering = attrs.ordering;
    }
  }
}

export function numberColumn<T>(
  name: string,
  getter: (t: T) => number,
  contextMenu?: m.Child[],
): ColumnDescriptor<T> {
  return new ColumnDescriptor<T>(name, getter, {contextMenu, sortKey: getter});
}

export function stringColumn<T>(
  name: string,
  getter: (t: T) => string,
  contextMenu?: m.Child[],
): ColumnDescriptor<T> {
  return new ColumnDescriptor<T>(name, getter, {contextMenu, sortKey: getter});
}

export function widgetColumn<T>(
  name: string,
  getter: (t: T) => m.Child,
): ColumnDescriptor<T> {
  return new ColumnDescriptor<T>(name, getter);
}

interface SortingInfo<T> {
  columnId: string;
  direction: SortDirection;
  // TODO(ddrone): figure out if storing this can be avoided.
  ordering: ComparisonFn<T>;
}

// Encapsulated table data, that contains the input to be displayed, as well as
// some helper information to allow sorting.
export class TableData<T> {
  data: T[];
  private _sortingInfo?: SortingInfo<T>;
  private permutation: number[];

  constructor(data: T[]) {
    this.data = data;
    this.permutation = range(data.length);
  }

  *iterateItems(): Generator<T> {
    for (const index of this.permutation) {
      yield this.data[index];
    }
  }

  items(): T[] {
    return Array.from(this.iterateItems());
  }

  setItems(newItems: T[]) {
    this.data = newItems;
    this.permutation = range(newItems.length);
    if (this._sortingInfo !== undefined) {
      this.reorder(this._sortingInfo);
    }
    scheduleFullRedraw();
  }

  resetOrder() {
    this.permutation = range(this.data.length);
    this._sortingInfo = undefined;
    scheduleFullRedraw();
  }

  get sortingInfo(): SortingInfo<T> | undefined {
    return this._sortingInfo;
  }

  reorder(info: SortingInfo<T>) {
    this._sortingInfo = info;
    this.permutation.sort(
      withDirection(
        comparingBy((index: number) => this.data[index], info.ordering),
        info.direction,
      ),
    );
    scheduleFullRedraw();
  }
}

export interface TableAttrs<T> {
  data: TableData<T>;
  columns: ColumnDescriptor<T>[];
}

function directionOnIndex(
  columnId: string,
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  info?: SortingInfo<any>,
): SortDirection | undefined {
  if (info === undefined) {
    return undefined;
  }
  return info.columnId === columnId ? info.direction : undefined;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export class Table implements m.ClassComponent<TableAttrs<any>> {
  renderColumnHeader(
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    vnode: m.Vnode<TableAttrs<any>>,
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    column: ColumnDescriptor<any>,
  ): m.Child {
    let currDirection: SortDirection | undefined = undefined;

    let items = column.contextMenu;
    if (column.ordering !== undefined) {
      const ordering = column.ordering;
      currDirection = directionOnIndex(column.id, vnode.attrs.data.sortingInfo);
      const newItems: m.Child[] = [];
      if (currDirection !== 'ASC') {
        newItems.push(
          m(MenuItem, {
            label: 'Sort ascending',
            onclick: () => {
              vnode.attrs.data.reorder({
                columnId: column.id,
                direction: 'ASC',
                ordering,
              });
            },
          }),
        );
      }
      if (currDirection !== 'DESC') {
        newItems.push(
          m(MenuItem, {
            label: 'Sort descending',
            onclick: () => {
              vnode.attrs.data.reorder({
                columnId: column.id,
                direction: 'DESC',
                ordering,
              });
            },
          }),
        );
      }
      if (currDirection !== undefined) {
        newItems.push(
          m(MenuItem, {
            label: 'Restore original order',
            onclick: () => {
              vnode.attrs.data.resetOrder();
            },
          }),
        );
      }
      items = [...newItems, ...(items ?? [])];
    }

    return m(
      'td',
      column.name,
      items &&
        m(
          PopupMenu2,
          {
            trigger: m(Button, {icon: popupMenuIcon(currDirection)}),
          },
          items,
        ),
    );
  }

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  checkValid(attrs: TableAttrs<any>) {
    if (!allUnique(attrs.columns.map((c) => c.id))) {
      throw new Error('column IDs should be unique');
    }
  }

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  oncreate(vnode: m.VnodeDOM<TableAttrs<any>, this>) {
    this.checkValid(vnode.attrs);
  }

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  onupdate(vnode: m.VnodeDOM<TableAttrs<any>, this>) {
    this.checkValid(vnode.attrs);
  }

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  view(vnode: m.Vnode<TableAttrs<any>>): m.Child {
    const attrs = vnode.attrs;

    return m(
      'table.generic-table',
      m(
        'thead',
        m(
          'tr.header',
          attrs.columns.map((column) => this.renderColumnHeader(vnode, column)),
        ),
      ),
      attrs.data.items().map((row) =>
        m(
          'tr',
          attrs.columns.map((column) => m('td', column.render(row))),
        ),
      ),
    );
  }
}
