import { onMounted, onWillUnmount, signal, useProps } from "@odoo/owl";
import { Chart, ChartConfiguration } from "chart.js/auto";
import {
  chartJsExtensionRegistry,
  registerChartJSExtensions,
} from "../../../../helpers/figures/charts/chart_js_extension";
import { deepCopy, deepEquals } from "../../../../helpers/misc";
import { Component, useLayoutEffect } from "../../../../owl3_compatibility_layer";
import { useStore } from "../../../../store_engine/store_hooks";
import { ChartJSRuntime } from "../../../../types/chart/chart";
import { SpreadsheetChildEnv } from "../../../../types/spreadsheet_env";
import { Store } from "../../../../types/store_engine";
import { types } from "../../../props_validation";
import { ChartAnimationStore } from "./chartjs_animation_store";
import { chartBackgroundPlugin } from "./chartjs_background_plugin";
import { getCalendarChartController } from "./chartjs_calendar_chart";
import { chartColorScalePlugin } from "./chartjs_colorscale_plugin";
import {
  funnelTooltipPositioner,
  getFunnelChartController,
  getFunnelChartElement,
} from "./chartjs_funnel_chart";
import { geoProjectionPlugin } from "./chartjs_geo_projection_plugin";
import { chartMinorGridPlugin } from "./chartjs_minor_grid_plugin";
import { chartShowValuesPlugin } from "./chartjs_show_values_plugin";
import { sunburstHoverPlugin } from "./chartjs_sunburst_hover_plugin";
import { sunburstLabelsPlugin } from "./chartjs_sunburst_labels_plugin";
import { waterfallLinesPlugin } from "./chartjs_waterfall_plugin";
import { zoomWindowPlugin } from "./zoomable_chart/zoomable_chartjs_plugins";

chartJsExtensionRegistry.add("chartShowValuesPlugin", {
  register: (Chart) => Chart.register(chartShowValuesPlugin),
  unregister: (Chart) => Chart.unregister(chartShowValuesPlugin),
});
chartJsExtensionRegistry.add("chartMinorGridPlugin", {
  register: (Chart) => Chart.register(chartMinorGridPlugin),
  unregister: (Chart) => Chart.unregister(chartMinorGridPlugin),
});
chartJsExtensionRegistry.add("waterfallLinesPlugin", {
  register: (Chart) => Chart.register(waterfallLinesPlugin),
  unregister: (Chart) => Chart.unregister(waterfallLinesPlugin),
});
chartJsExtensionRegistry.add("funnelController", {
  register: (Chart) => Chart.register(getFunnelChartController()),
  unregister: (Chart) => Chart.unregister(getFunnelChartController()),
});
chartJsExtensionRegistry.add("funnelElement", {
  register: (Chart) => Chart.register(getFunnelChartElement()),
  unregister: (Chart) => Chart.unregister(getFunnelChartElement()),
});
chartJsExtensionRegistry.add("funnelTooltipPositioner", {
  register: (Chart) =>
    (Chart.Tooltip.positioners.funnelTooltipPositioner = funnelTooltipPositioner),
  unregister: (Chart) => (Chart.Tooltip.positioners.funnelTooltipPositioner = undefined),
});
chartJsExtensionRegistry.add("sunburstLabelsPlugin", {
  register: (Chart) => Chart.register(sunburstLabelsPlugin),
  unregister: (Chart) => Chart.unregister(sunburstLabelsPlugin),
});
chartJsExtensionRegistry.add("sunburstHoverPlugin", {
  register: (Chart) => Chart.register(sunburstHoverPlugin),
  unregister: (Chart) => Chart.unregister(sunburstHoverPlugin),
});
chartJsExtensionRegistry.add("chartColorScalePlugin", {
  register: (Chart) => Chart.register(chartColorScalePlugin),
  unregister: (Chart) => Chart.unregister(chartColorScalePlugin),
});
chartJsExtensionRegistry.add("calendarController", {
  register: (Chart) => Chart.register(getCalendarChartController()),
  unregister: (Chart) => Chart.unregister(getCalendarChartController()),
});
chartJsExtensionRegistry.add("zoomWindowPlugin", {
  register: (Chart) => Chart.register(zoomWindowPlugin),
  unregister: (Chart) => Chart.unregister(zoomWindowPlugin),
});
chartJsExtensionRegistry.add("chartBackgroundPlugin", {
  register: (Chart) => Chart.register(chartBackgroundPlugin),
  unregister: (Chart) => Chart.unregister(chartBackgroundPlugin),
});
chartJsExtensionRegistry.add("geoProjectionPlugin", {
  register: (Chart) => Chart.register(geoProjectionPlugin),
  unregister: (Chart) => Chart.unregister(geoProjectionPlugin),
});

export class ChartJsComponent extends Component<SpreadsheetChildEnv> {
  static template = "o-spreadsheet-ChartJsComponent";

  protected props = useProps({
    chartId: types.string(),
    isFullScreen: types.boolean().optional(),
  });

  protected canvas = signal.ref(HTMLCanvasElement);
  protected chart?: Chart;
  protected currentRuntime!: ChartJSRuntime;
  protected animationStore: Store<ChartAnimationStore> | undefined;

  private currentDevicePixelRatio = window.devicePixelRatio;

  get chartRuntime(): ChartJSRuntime {
    const runtime = this.env.model.getters.getChartRuntime(this.props.chartId);
    if (!("chartJsConfig" in runtime)) {
      throw new Error("Unsupported chart runtime");
    }
    return runtime;
  }

  setup() {
    if (this.shouldAnimate) {
      this.animationStore = useStore(ChartAnimationStore);
    }
    onMounted(() => {
      registerChartJSExtensions();
      const runtime = this.chartRuntime;
      this.currentRuntime = runtime;
      // Note: chartJS modify the runtime in place, so it's important to give it a copy
      this.createChart(deepCopy(runtime));
    });
    onWillUnmount(this.unmount.bind(this));
    useLayoutEffect(() => {
      const runtime = this.chartRuntime;
      if (
        runtime !== this.currentRuntime &&
        (!("errorMessage" in this.chartRuntime) || !this.chartRuntime.errorMessage)
      ) {
        if (runtime.chartJsConfig.type !== this.currentRuntime.chartJsConfig.type) {
          this.chart?.destroy();
          this.createChart(deepCopy(runtime));
        } else {
          this.updateChartJs(deepCopy(runtime));
        }
        this.currentRuntime = runtime;
      } else if (this.currentDevicePixelRatio !== window.devicePixelRatio) {
        this.currentDevicePixelRatio = window.devicePixelRatio;
        this.updateChartJs(deepCopy(this.currentRuntime));
      }
    });
  }

  protected unmount() {
    this.chart?.destroy();
  }

  private get shouldAnimate(): boolean {
    return this.env.model.getters.isDashboard();
  }

  protected createChart(chartRuntime: ChartJSRuntime) {
    if (!globalThis.Chart) {
      throw new Error("Chart.js library is not loaded");
    }
    let chartData = chartRuntime.chartJsConfig as ChartConfiguration<any>;
    if (this.shouldAnimate && this.animationStore) {
      const chartType = this.env.model.getters.getChartDefinition(this.props.chartId)?.type;
      if (chartType && this.animationStore.animationPlayed[this.animationChartId] !== chartType) {
        chartData = this.enableAnimationInChartData(chartData);
        this.animationStore.disableAnimationForChart(this.animationChartId, chartType);
      }
    }

    const canvas = this.canvas();
    if (!canvas) {
      return;
    }
    const ctx = canvas.getContext("2d")!;
    this.chart = new globalThis.Chart(ctx, chartData);
  }

  protected updateChartJs(chartRuntime: ChartJSRuntime) {
    let chartData = chartRuntime.chartJsConfig as ChartConfiguration<any>;
    if (this.shouldAnimate) {
      const chartType = this.env.model.getters.getChartDefinition(this.props.chartId)?.type;
      if (chartType && this.hasChartDataChanged() && this.animationStore) {
        chartData = this.enableAnimationInChartData(chartData);
        this.animationStore.disableAnimationForChart(this.animationChartId, chartType);
      }
    }

    if (chartData.data && chartData.data.datasets) {
      this.chart!.data = chartData.data;
      if (chartData.options?.plugins?.title) {
        this.chart!.config.options!.plugins!.title = chartData.options.plugins.title;
      }
    } else {
      this.chart!.data.datasets = [];
    }
    this.chart!.config.options = chartData.options;
    this.chart!.update();
  }

  private hasChartDataChanged() {
    return !deepEquals(
      this.getChartDataInRuntime(this.currentRuntime),
      this.getChartDataInRuntime(this.chartRuntime)
    );
  }

  protected enableAnimationInChartData(
    chartData: ChartConfiguration<any>
  ): ChartConfiguration<any> {
    return {
      ...chartData,
      options: { ...chartData.options, animation: { animateRotate: true } },
    };
  }

  private getChartDataInRuntime(runtime: ChartJSRuntime) {
    const data = runtime.chartJsConfig.data;
    return {
      labels: data.labels,
      dataset: data.datasets.map((dataset) => ({
        data: dataset.data,
        label: dataset.label,
        tree: dataset.tree,
      })),
    };
  }

  get animationChartId() {
    return this.props.isFullScreen ? this.props.chartId + "-fullscreen" : this.props.chartId;
  }
}
