import { signal, useProps } from "@odoo/owl";
import { isDefined } from "../../../../helpers/misc";
import {
  AGGREGATORS,
  changeCalculatedMeasuresOnMeasureChange,
  isDateOrDatetimeField,
} from "../../../../helpers/pivot/pivot_helpers";
import { PivotRuntimeDefinition } from "../../../../helpers/pivot/pivot_runtime_definition";
import { Component } from "../../../../owl3_compatibility_layer";
import { _t } from "../../../../translation";
import { SortDirection } from "../../../../types/misc";
import {
  Aggregator,
  Granularity,
  PivotCoreDefinition,
  PivotCoreDimension,
  PivotCoreMeasure,
  PivotDimension as PivotDimensionType,
  PivotField,
  PivotMeasure,
} from "../../../../types/pivot";
import { SpreadsheetChildEnv } from "../../../../types/spreadsheet_env";
import {
  getBoundingRectWithMargins,
  hasInteractiveElementInEventTree,
} from "../../../helpers/dom_helpers";
import { useDragAndDropListItems } from "../../../helpers/drag_and_drop_dom_items_hook";
import { types } from "../../../props_validation";
import { SidePanelCollapsible } from "../../components/collapsible/side_panel_collapsible";
import { PivotCustomGroupsCollapsible } from "../pivot_custom_groups_collapsible/pivot_custom_groups_collapsible";
import { AddDimensionButton } from "./add_dimension_button/add_dimension_button";
import { PivotDimension } from "./pivot_dimension/pivot_dimension";
import { PivotDimensionGranularity } from "./pivot_dimension_granularity/pivot_dimension_granularity";
import { PivotDimensionOrder } from "./pivot_dimension_order/pivot_dimension_order";
import { PivotMeasureEditor } from "./pivot_measure/pivot_measure";
import { PivotSortSection } from "./pivot_sort_section/pivot_sort_section";

export class PivotLayoutConfigurator extends Component<SpreadsheetChildEnv> {
  static template = "o-spreadsheet-PivotLayoutConfigurator";
  static components = {
    AddDimensionButton,
    PivotDimension,
    PivotDimensionOrder,
    PivotDimensionGranularity,
    PivotMeasureEditor,
    PivotSortSection,
    PivotCustomGroupsCollapsible,
    SidePanelCollapsible,
  };
  protected props = useProps({
    definition: types.instanceOf(PivotRuntimeDefinition),
    onDimensionsUpdated: types.function<(definition: Partial<PivotCoreDefinition>) => void>(),
    onFiltersUpdated: types.function<(definition: Partial<PivotCoreDefinition>) => void>(),
    unusedGroupableFields: types.array(types.PivotField()),
    measureFields: types.array(types.PivotField()),
    unusedGranularities: types.RecordOf<Set<string>>(),
    dateGranularities: types.array(types.string()),
    datetimeGranularities: types.array(types.string()),
    getScrollableContainerEl: types.function<() => HTMLElement>().optional(),
    pivotId: types.UID(),
  });

  private dimensionsRef = signal.ref();
  private dragAndDrop = useDragAndDropListItems();
  AGGREGATORS = AGGREGATORS;

  isDateOrDatetimeField = isDateOrDatetimeField;

  startDragAndDrop(dimension: PivotDimensionType, event: MouseEvent) {
    if (event.button !== 0 || hasInteractiveElementInEventTree(event)) {
      return;
    }

    const rects = this.getDimensionElementsRects();
    const definition = this.props.definition;
    const { columns, rows } = definition;
    const draggableIds = [
      ...columns.map((col) => col.nameWithGranularity),
      "__rows_title__",
      ...rows.map((row) => row.nameWithGranularity),
    ];
    const allDimensions = columns.concat(rows);
    const offset = 1; // column title
    const draggableItems = draggableIds.map((id, index) => ({
      id,
      size: rects[index + offset].height,
      position: rects[index + offset].y,
    }));
    this.dragAndDrop.start("vertical", {
      draggedItemId: dimension.nameWithGranularity,
      initialMousePosition: event.clientY,
      items: draggableItems,
      scrollableContainerEl: this.props.getScrollableContainerEl?.() || this.dimensionsRef()!,
      onDragEnd: (dimensionName, finalIndex) => {
        const originalIndex = draggableIds.findIndex((id) => id === dimensionName);
        if (originalIndex === finalIndex) {
          return;
        }
        const draggedItems = [...draggableIds];
        draggedItems.splice(originalIndex, 1);
        draggedItems.splice(finalIndex, 0, dimensionName);
        const columns = draggedItems.slice(0, draggedItems.indexOf("__rows_title__"));
        const rows = draggedItems.slice(draggedItems.indexOf("__rows_title__") + 1);
        this.props.onDimensionsUpdated({
          columns: columns
            .map((nameWithGranularity) =>
              allDimensions.find(
                (dimension) => dimension.nameWithGranularity === nameWithGranularity
              )
            )
            .filter(isDefined),
          rows: rows
            .map((nameWithGranularity) =>
              allDimensions.find(
                (dimension) => dimension.nameWithGranularity === nameWithGranularity
              )
            )
            .filter(isDefined),
        });
      },
    });
  }

  getGranularitiesFor(field: PivotField) {
    if (!isDateOrDatetimeField(field)) {
      return [];
    }
    return field.type === "date" ? this.props.dateGranularities : this.props.datetimeGranularities;
  }

  startDragAndDropMeasures(measure: PivotMeasure, event: MouseEvent) {
    if (event.button !== 0 || hasInteractiveElementInEventTree(event)) {
      return;
    }

    const rects = this.getDimensionElementsRects();
    const definition = this.props.definition;
    const { measures, columns, rows } = definition;
    const draggableIds = measures.map((m) => m.id);
    const offset = 3 + columns.length + rows.length; // column title, row title, measure title
    const draggableItems = draggableIds.map((id, index) => ({
      id,
      size: rects[index + offset].height,
      position: rects[index + offset].y,
    }));
    this.dragAndDrop.start("vertical", {
      draggedItemId: measure.id,
      initialMousePosition: event.clientY,
      items: draggableItems,
      scrollableContainerEl: this.props.getScrollableContainerEl?.() || this.dimensionsRef()!,
      onDragEnd: (measureName, finalIndex) => {
        const originalIndex = draggableIds.findIndex((id) => id === measureName);
        if (originalIndex === finalIndex) {
          return;
        }
        const draggedItems = [...draggableIds];
        draggedItems.splice(originalIndex, 1);
        draggedItems.splice(finalIndex, 0, measureName);
        this.props.onDimensionsUpdated({
          measures: draggedItems
            .map((measureId) => measures.find((measure) => measure.id === measureId))
            .filter(isDefined),
        });
      },
    });
  }

  getDimensionElementsRects() {
    return Array.from(this.dimensionsRef()!.children).map((el) => getBoundingRectWithMargins(el));
  }

  removeDimension(dimension: PivotDimensionType) {
    const { columns, rows } = this.props.definition;
    this.props.onDimensionsUpdated({
      columns: columns.filter((col) => col.nameWithGranularity !== dimension.nameWithGranularity),
      rows: rows.filter((row) => row.nameWithGranularity !== dimension.nameWithGranularity),
    });
  }

  removeMeasureDimension(measure: PivotMeasure) {
    const { measures } = this.props.definition;
    this.props.onDimensionsUpdated({
      measures: measures.filter((m) => m.id !== measure.id),
    });
  }

  addColumnDimension(fieldName: string) {
    const { columns }: { columns: PivotCoreDimension[] } = this.props.definition;
    this.props.onDimensionsUpdated({
      columns: columns.concat([{ fieldName: fieldName, order: "asc" }]),
    });
  }

  addRowDimension(fieldName: string) {
    const { rows }: { rows: PivotCoreDimension[] } = this.props.definition;
    this.props.onDimensionsUpdated({
      rows: rows.concat([{ fieldName: fieldName, order: "asc" }]),
    });
  }

  addMeasureDimension(fieldName: string) {
    const { measures }: { measures: PivotCoreMeasure[] } = this.props.definition;
    const aggregator = this.getDefaultMeasureAggregator(fieldName);
    this.props.onDimensionsUpdated({
      measures: measures.concat([
        { id: this.getMeasureId(fieldName, aggregator), fieldName, aggregator },
      ]),
    });
  }

  updateMeasure(measure: PivotMeasure, newMeasure: PivotMeasure) {
    const { measures }: { measures: PivotCoreMeasure[] } = this.props.definition;

    const newMeasures = changeCalculatedMeasuresOnMeasureChange(measure, newMeasure, measures);
    const update: Partial<PivotCoreDefinition> = { measures: newMeasures };
    if (this.props.definition.sortedColumn?.measure === measure.id) {
      update.sortedColumn = { ...this.props.definition.sortedColumn, measure: newMeasure.id };
    }
    this.props.onDimensionsUpdated(update);
  }

  private getMeasureId(fieldName: string, aggregator?: string) {
    const baseId = fieldName.replaceAll("'", "") + (aggregator ? `:${aggregator}` : "");
    let id = baseId;
    let i = 2;
    while (this.props.definition.measures.some((m) => m.id === id)) {
      id = `${baseId}:${i}`;
      i++;
    }
    return id;
  }

  private getDefaultMeasureAggregator(fieldName: string): Aggregator | string {
    const field = this.props.measureFields.find((f) => f.name === fieldName);
    return field?.aggregator ? field.aggregator : "count";
  }

  addCalculatedMeasure() {
    const { measures }: { measures: PivotCoreMeasure[] } = this.props.definition;
    const measureName = this.env.model.getters.generateNewCalculatedMeasureName(measures);
    const aggregator = "sum";
    this.props.onDimensionsUpdated({
      measures: measures.concat([
        {
          id: this.getMeasureId(measureName, aggregator),
          fieldName: measureName,
          aggregator,
          computedBy: {
            sheetId: this.env.model.getters.getActiveSheetId(),
            formula: "=0",
          },
        },
      ]),
    });
  }

  getCustomField(dimension: PivotDimensionType) {
    const definition = this.env.model.getters.getPivotCoreDefinition(this.props.pivotId);
    return definition.customFields?.[dimension.nameWithGranularity];
  }

  updateOrder(updateDimension: PivotDimensionType, order?: SortDirection) {
    const { rows, columns, sortedColumn } = this.props.definition;
    const isRow = rows.some(
      (row) => row.nameWithGranularity === updateDimension.nameWithGranularity
    );
    this.props.onDimensionsUpdated({
      rows: rows.map((row) => {
        if (row.nameWithGranularity === updateDimension.nameWithGranularity) {
          return { ...row, order: order || undefined };
        }
        return row;
      }),
      columns: columns.map((col) => {
        if (col.nameWithGranularity === updateDimension.nameWithGranularity) {
          return { ...col, order: order || undefined };
        }
        return col;
      }),
      sortedColumn: isRow ? undefined : sortedColumn,
    });
  }

  updateGranularity(dimension: PivotDimensionType, granularity: Granularity) {
    const { rows, columns } = this.props.definition;
    this.props.onDimensionsUpdated({
      rows: rows.map((row) => {
        if (row.nameWithGranularity === dimension.nameWithGranularity) {
          return { ...row, granularity };
        }
        return row;
      }),
      columns: columns.map((col) => {
        if (col.nameWithGranularity === dimension.nameWithGranularity) {
          return { ...col, granularity };
        }
        return col;
      }),
    });
  }

  getHugeDimensionErrorMessage(dimension: PivotDimensionType) {
    const pivot = this.env.model.getters.getPivot(this.props.pivotId);
    const possibleValues = pivot.getPossibleFieldValues(dimension);
    return possibleValues.length > 100
      ? _t(
          "This dimension contains a lot of values (%s), and might slow down the pivot table.",
          possibleValues.length
        )
      : undefined;
  }

  get hasSortedColumn() {
    return !!this.props.definition.sortedColumn;
  }
}
