// Copyright (C) 2024 The Android Open Source Project
//
// 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.

import './sql_table.scss';
import m from 'mithril';
import {MenuDivider, MenuItem} from '../../../../widgets/menu';
import {buildSqlQuery} from './query_builder';
import {Icons} from '../../../../base/semantic_icons';
import type {Row} from '../../../../trace_processor/query_result';
import {Spinner} from '../../../../widgets/spinner';
import {
  Grid,
  GridCell,
  type GridColumn,
  GridHeaderCell,
  renderSortMenuItems,
  type SortDirection,
} from '../../../../widgets/grid';

import type {SqlTableState} from './state';
import type {SqlTableDescription} from './table_description';
import {
  type RenderedCell,
  type TableColumn,
  type RenderCellContext,
  tableColumnId,
} from './table_column';
import {type SqlColumn, sqlColumnId} from './sql_column';
import {SelectColumnMenu} from './menus/select_column_menu';
import {renderColumnFilterOptions} from './menus/add_column_filter_menu';
import {renderCastColumnMenu} from './menus/cast_column_menu';
import {renderTransformColumnMenu} from './menus/transform_column_menu';

export interface SqlTableConfig {
  readonly state: SqlTableState;
  // For additional menu items to add to the column header menus
  readonly addColumnMenuItems?: (column: TableColumn) => m.Children;
  // For additional filter actions
  readonly extraAddFilterActions?: (
    op: string,
    column: string,
    value?: string,
  ) => void;
  readonly extraRemoveFilterActions?: (filterSqlStr: string) => void;
}

type AdditionalColumnMenuItems = Record<string, m.Children>;

function renderCell(
  column: TableColumn,
  row: Row,
  state: SqlTableState,
  addColumn: (column: TableColumn) => void,
): RenderedCell {
  const {columns} = state.getCurrentRequest();
  const sqlValue = row[columns[sqlColumnId(column.display ?? column.column)]];

  const result = column.renderCell(
    sqlValue,
    getRenderCellContext(state, addColumn),
  );

  return result;
}

export function columnTitle(column: TableColumn): string {
  if (column.getTitle !== undefined) {
    const title = column.getTitle();
    if (title !== undefined) return title;
  }
  return sqlColumnId(column.column);
}

interface AddColumnMenuItemAttrs {
  table: SqlTable;
  state: SqlTableState;
  index: number;
}

// This is separated into a separate class to store the index of the column to be
// added and increment it when multiple columns are added from the same popup menu.
class AddColumnMenuItem implements m.ClassComponent<AddColumnMenuItemAttrs> {
  // Index where the new column should be inserted.
  // In the regular case, a click would close the popup (destroying this class) and
  // the `index` would not change during its lifetime.
  // However, for mod-click, we want to keep adding columns to the right of the recently
  // added column, so to achieve that we keep track of the index and increment it for
  // each new column added.
  index: number;

  constructor({attrs}: m.Vnode<AddColumnMenuItemAttrs>) {
    this.index = attrs.index;
  }

  view({attrs}: m.Vnode<AddColumnMenuItemAttrs>) {
    return m(
      MenuItem,
      {label: 'Add column', icon: Icons.Add},
      attrs.table.renderAddColumnOptions((column) => {
        attrs.state.addColumn(column, this.index++);
      }),
    );
  }
}

export class SqlTable implements m.ClassComponent<SqlTableConfig> {
  private readonly table: SqlTableDescription;

  private state: SqlTableState;

  constructor(vnode: m.Vnode<SqlTableConfig>) {
    this.state = vnode.attrs.state;
    this.table = this.state.config;
  }

  renderAddColumnOptions(addColumn: (column: TableColumn) => void): m.Children {
    // We do not want to add columns which already exist, so we track the
    // columns which we are already showing here.
    // TODO(altimin): Theoretically a single table can have two different
    // arg_set_ids, so we should track (arg_set_id_column, arg_name) pairs here.
    const existingColumnIds = new Set<string>();

    for (const column of this.state.getSelectedColumns()) {
      existingColumnIds.add(tableColumnId(column));
    }

    return m(SelectColumnMenu, {
      columns: this.table.columns.map((column) => ({
        key: columnTitle(column),
        column,
      })),
      filters: this.state.filters,
      trace: this.state.trace,
      getSqlQuery: (columns: {[key: string]: SqlColumn}) =>
        buildSqlQuery({
          table: this.state.config.name,
          columns,
          filters: this.state.filters.get(),
          orderBy: this.state.getOrderedBy(),
        }),
      existingColumnIds,
      onColumnSelected: addColumn,
    });
  }

  getAdditionalColumnMenuItems(
    addColumnMenuItems?: (
      column: TableColumn,
      columnAlias: string,
    ) => m.Children,
  ) {
    if (addColumnMenuItems === undefined) return;

    const additionalColumnMenuItems: AdditionalColumnMenuItems = {};
    this.state.getSelectedColumns().forEach((column) => {
      const columnAlias =
        this.state.getCurrentRequest().columns[sqlColumnId(column.column)];

      additionalColumnMenuItems[columnAlias] = addColumnMenuItems(
        column,
        columnAlias,
      );
    });

    return additionalColumnMenuItems;
  }

  view({attrs}: m.Vnode<SqlTableConfig>) {
    const rows = this.state.getDisplayedRows();
    const additionalColumnMenuItems = this.getAdditionalColumnMenuItems(
      attrs.addColumnMenuItems,
    );

    const columns = this.state.getSelectedColumns();

    // Build VirtualGrid columns
    const virtualGridColumns = columns.map((column, i) => {
      const sorted = this.state.isSortedBy(column);
      const menuItems: m.Children = [
        renderSortMenuItems(sorted, (direction) =>
          this.state.sortBy({column, direction}),
        ),
        m(MenuDivider),
        this.state.getSelectedColumns().length > 1 &&
          m(MenuItem, {
            label: 'Hide',
            icon: Icons.Hide,
            onclick: () => this.state.hideColumnAtIndex(i),
          }),
        // Use the new getColumnSpecificMenuItems method if available
        column.getColumnSpecificMenuItems?.({
          replaceColumn: (newColumn: TableColumn) =>
            this.state.replaceColumnAtIndex(i, newColumn),
        }),
        m(
          MenuItem,
          {label: 'Cast', icon: Icons.Change},
          renderCastColumnMenu(column, i, this.state),
        ),
        renderTransformColumnMenu(column, i, this.state),
        m(
          MenuItem,
          {label: 'Add filter', icon: Icons.Filter},
          renderColumnFilterOptions(column, this.state),
        ),
        additionalColumnMenuItems &&
          additionalColumnMenuItems[
            this.state.getCurrentRequest().columns[sqlColumnId(column.column)]
          ],
        // Menu items before divider apply to selected column
        m(MenuDivider),
        // Menu items after divider apply to entire table
        m(AddColumnMenuItem, {
          table: this,
          state: this.state,
          index: i,
        }),
      ];
      const columnKey = tableColumnId(column);

      const gridColumn: GridColumn = {
        key: columnKey,
        header: m(
          GridHeaderCell,
          {
            sort: sorted,
            onSort: (direction: SortDirection) => {
              this.state.sortBy({column, direction});
            },
            menuItems,
          },
          columnTitle(column),
        ),
        reorderable: {reorderGroup: 'column'},
      };

      return gridColumn;
    });

    // Build VirtualGrid rows
    const virtualGridRows = rows.map((row) => {
      return columns.map((col, i) => {
        const {content, menu, isNumerical, isNull} = renderCell(
          col,
          row,
          this.state,
          (column) => {
            this.state.addColumn(column, i);
          },
        );
        return m(
          GridCell,
          {
            menuItems: menu,
            align: isNull ? 'center' : isNumerical ? 'right' : 'left',
            nullish: isNull,
          },
          content,
        );
      });
    });

    return [
      m(Grid, {
        className: 'sql-table',
        columns: virtualGridColumns,
        rowData: virtualGridRows,
        fillHeight: true,
        onColumnReorder: (from, to, position) => {
          if (typeof from === 'string' && typeof to === 'string') {
            // Convert column names to indices
            const fromIndex = columns.findIndex(
              (col) => tableColumnId(col) === from,
            );
            const toIndex = columns.findIndex(
              (col) => tableColumnId(col) === to,
            );

            if (fromIndex !== -1 && toIndex !== -1) {
              const targetIndex = position === 'before' ? toIndex : toIndex + 1;
              this.state.moveColumn(fromIndex, targetIndex);
            }
          }
        },
      }),
      this.state.isLoading() && m(Spinner),
      this.state.getQueryError() !== undefined &&
        m('.query-error', this.state.getQueryError()),
    ];
  }
}

function getRenderCellContext(
  state: SqlTableState,
  addColumn: (column: TableColumn) => void,
): RenderCellContext {
  return {
    filters: state.filters,
    trace: state.trace,
    getSqlQuery: (columns: {[key: string]: SqlColumn}) =>
      buildSqlQuery({
        table: state.config.name,
        columns,
        filters: state.filters.get(),
        orderBy: state.getOrderedBy(),
      }),
    hasColumn: (column: TableColumn) => {
      const selectedColumns = state.getSelectedColumns();
      return !selectedColumns.some(
        (c) => tableColumnId(c) === tableColumnId(column),
      );
    },
    addColumn,
  };
}
