import { getRenderer, getRendererForLayer, listRenderers } from "./registry.js";
import { bindPointDrag } from "./interactions/drag.js";
import { bindBrush, removeBrush } from "./interactions/brush.js";
import { bindAnnotation, removeAnnotationBindings } from "./interactions/annotate.js";
import { bindLinked, cleanupLinked } from "./interactions/linked.js";
import { registerLinkedCursor } from "./interactions/linked-cursor.js";
import { bindSliders, removeSliders } from "./interactions/slider.js";
import { bindRollover } from "./interactions/rollover.js";
import { deriveChartRender, applyDerivedScales } from "./derive/chart-render.js";
import { validateLayers } from "./derive/validate.js";
import { transitionGrouped, transitionStacked, getGroupedDataObject } from "./renderers/groupedBarHelpers.js";
import { syncAxes } from "./layout/axes.js";
import { syncLegend, syncOrdinalLegendData } from "./layout/legend.js";
import { syncReferenceLines } from "./layout/reference-lines.js";
import { getChartHeight, initializeScaffold, renderChartTitle, updateScaffoldLayout } from "./layout/scaffold.js";
import { hideChartTooltip, initializeTooltip, removeHoverOverlay } from "./tooltip.js";
import { addFAB, closePanel, openPanel } from "./interactions/bottom-sheet.js";
import { linearRegression } from "./utils/math.js";
import { tagName } from "./utils/responsive.js";
import { ThemeManager } from "./theme/theme-manager.js";
import { FacetController } from "./layout/facet-controller.js";
import { applyARIA } from "./a11y/aria.js";
import { KeyboardNavigator } from "./a11y/keyboard-nav.js";
import { DataTableFallback } from "./a11y/data-table.js";
import { initializeKeyframes, destroyKeyframes } from "./interactions/keyframes.js";

const MIN_CHART_WIDTH = 280;
const RESIZE_DEBOUNCE_MS = 100;

const EventEmitter = {
  on(event, handler) {
    this._listeners = this._listeners || {};
    this._listeners[event] = this._listeners[event] || [];
    this._listeners[event].push(handler);
    return this;
  },
  off(event, handler) {
    if (!this._listeners || !this._listeners[event]) {
      return this;
    }
    this._listeners[event] = handler
      ? this._listeners[event].filter(function(candidate) { return candidate !== handler; })
      : [];
    return this;
  },
  emit(event, payload) {
    if (!this._listeners || !this._listeners[event]) {
      return this;
    }
    this._listeners[event].forEach(function(handler) {
      handler(payload);
    });
    return this;
  }
};

export class myIOchart {
  constructor(opts) {
    Object.assign(this, EventEmitter);
    this._listeners = {};
    this.config = opts.config;
    this.dom = { element: opts.element };
    this.derived = {};
    this.runtime = {
      renderGen: 0,
      resizeTimer: null,
      width: Math.max(opts.width, MIN_CHART_WIDTH),
      height: opts.height,
      totalWidth: Math.max(opts.width, MIN_CHART_WIDTH),
      layout: "grouped",
      activeY: null,
      activeYFormat: null,
      tooltipHideTimer: null
    };
    if (this.config.sparkline) {
      this.applySparklineOverrides();
    }
    if (window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
      this.config.transitions.speed = 0;
    }
    this.runtime.width = this.runtime.totalWidth;
    this.syncLegacyAliases();
    this.draw();
  }

  syncLegacyAliases() {
    this.element = this.dom ? this.dom.element : null;
    this.svg = this.dom ? this.dom.svg : null;
    this.plot = this.dom ? this.dom.plot : null;
    this.chart = this.dom ? this.dom.chartArea : null;
    this.legendArea = this.dom ? this.dom.legendArea : null;
    this.clipPath = this.dom ? this.dom.clipPath : null;
    this.tooltip = this.dom ? this.dom.tooltip : null;
    this.toolTipTitle = this.dom ? this.dom.tooltipTitle : null;
    this.toolTipBody = this.dom ? this.dom.tooltipBody : null;
    this.plotLayers = this.config ? this.config.layers : null;
    this.options = this.config ? {
      margin: this.config.layout.margin,
      suppressLegend: this.config.layout.suppressLegend,
      suppressAxis: this.config.layout.suppressAxis,
      xlim: this.config.scales.xlim,
      ylim: this.config.scales.ylim,
      categoricalScale: this.config.scales.categoricalScale,
      flipAxis: this.config.scales.flipAxis,
      colorScheme: this.config.scales.colorScheme
        ? (this.config.scales.colorScheme.enabled
            ? [this.config.scales.colorScheme.colors, this.config.scales.colorScheme.domain, "on"]
            : [this.config.scales.colorScheme.colors, this.config.scales.colorScheme.domain, "off"])
        : null,
      xAxisFormat: this.config.axes.xAxisFormat,
      yAxisFormat: this.config.axes.yAxisFormat,
      toolTipFormat: this.config.axes.toolTipFormat,
      xTickLabels: this.config.axes.xTickLabels,
      xAxisLabel: this.config.axes.xAxisLabel,
      yAxisLabel: this.config.axes.yAxisLabel,
      dragPoints: this.config.interactions.dragPoints,
      toggleY: this.config.interactions.toggleY && this.config.interactions.toggleY.variable
        ? [this.config.interactions.toggleY.variable, this.config.interactions.toggleY.format]
        : null,
      toolTipOptions: this.config.interactions.toolTipOptions,
      transition: this.config.transitions,
      referenceLine: this.config.referenceLines
    } : null;
    this.margin = this.config ? this.config.layout.margin : null;
    this.width = this.runtime ? this.runtime.width : null;
    this.height = this.runtime ? this.runtime.height : null;
    this.totalWidth = this.runtime ? this.runtime.totalWidth : null;
    this.layout = this.runtime ? this.runtime.layout : null;
    this.newY = this.runtime ? this.runtime.activeY : null;
    this.newScaleY = this.runtime ? this.runtime.activeYFormat : null;
    this.toolLine = this.runtime ? this.runtime.toolLine : null;
    this.toolTipBox = this.runtime ? this.runtime.toolTipBox : null;
    this.toolPointLayer = this.runtime ? this.runtime.toolPointLayer : null;
    this.xScale = this.derived ? this.derived.xScale : null;
    this.yScale = this.derived ? this.derived.yScale : null;
    this.colorDiscrete = this.derived ? this.derived.colorDiscrete : null;
    this.colorContinuous = this.derived ? this.derived.colorContinuous : null;
    this.x_banded = this.derived ? this.derived.xBanded : null;
    this.y_banded = this.derived ? this.derived.yBanded : null;
    this.x_check = this.derived ? this.derived.xCheck : null;
    this.currentLayers = this.derived ? this.derived.currentLayers : null;
    this.layerIndex = this.derived ? this.derived.layerIndex : null;
  }

  captureLegacyAliases() {
    if (!this.dom || !this.runtime || !this.derived) {
      return;
    }
    this.dom.svg = this.svg || this.dom.svg;
    this.dom.plot = this.plot || this.dom.plot;
    this.dom.chartArea = this.chart || this.dom.chartArea;
    this.dom.legendArea = this.legendArea || this.dom.legendArea;
    this.dom.clipPath = this.clipPath || this.dom.clipPath;
    this.dom.tooltip = this.tooltip || this.dom.tooltip;
    this.dom.tooltipTitle = this.toolTipTitle || this.dom.tooltipTitle;
    this.dom.tooltipBody = this.toolTipBody || this.dom.tooltipBody;
    this.runtime.layout = this.layout || this.runtime.layout;
    this.runtime.activeY = this.newY || this.runtime.activeY;
    this.runtime.activeYFormat = this.newScaleY || this.runtime.activeYFormat;
    this.runtime.toolLine = this.toolLine || this.runtime.toolLine;
    this.runtime.toolTipBox = this.toolTipBox || this.runtime.toolTipBox;
    this.runtime.toolPointLayer = this.toolPointLayer || this.runtime.toolPointLayer;
    this.derived.xScale = this.xScale || this.derived.xScale;
    this.derived.yScale = this.yScale || this.derived.yScale;
    this.derived.colorDiscrete = this.colorDiscrete || this.derived.colorDiscrete;
    this.derived.colorContinuous = this.colorContinuous || this.derived.colorContinuous;
    this.derived.xBanded = this.x_banded || this.derived.xBanded;
    this.derived.yBanded = this.y_banded || this.derived.yBanded;
    this.derived.xCheck = this.x_check || this.derived.xCheck;
    this.derived.currentLayers = this.currentLayers || this.derived.currentLayers;
    this.derived.layerIndex = this.layerIndex || this.derived.layerIndex;
    this.syncLegacyAliases();
  }

  draw() {
    initializeScaffold(this);
    this.captureLegacyAliases();
    this.initialize();
  }

  initialize() {
    this.derived.currentLayers = this.config.layers;
    this.syncLegacyAliases();
    this.themeManager = new ThemeManager(this.dom.element, this.config);
    this.themeManager.initialize();
    initializeTooltip(this);
    if (!this.config.sparkline) {
      this.keyboardNav = new KeyboardNavigator(this);
      this.keyboardNav.initialize();
      this.dataTable = new DataTableFallback(this);
      this.dataTable.initialize();
      applyARIA(this);
    }
    initializeKeyframes(this);
    this.derived.currentLayers = this.config.layers;
    this.syncLegacyAliases();
    this.captureLegacyAliases();
    if (this.derived.currentLayers.length > 0) {
      this.setClipPath(this.derived.currentLayers[0].type);
    }
    this.renderCurrentLayers({ isInitialRender: true });
  }

  applySparklineOverrides() {
    this.config.layout.margin = { top: 1, right: 1, bottom: 1, left: 1 };
    this.config.layout.suppressLegend = true;
    this.config.layout.suppressAxis = { xAxis: true, yAxis: true };

    if (this.config.interactions.brush) this.config.interactions.brush.enabled = false;
    if (this.config.interactions.annotation) this.config.interactions.annotation.enabled = false;
    if (this.config.interactions.linked) this.config.interactions.linked.enabled = false;
    this.config.interactions.sliders = [];
    this.config.interactions.dragPoints = false;

    this.config.referenceLines = { x: null, y: null };

    this.dom.element.dataset.sparkline = "true";
  }

  renderCurrentLayers(opts) {
    const options = opts || {};
    const generation = ++this.runtime.renderGen;
    const isCurrent = () => this.runtime && this.runtime.renderGen === generation;

    // Faceted rendering: delegate to FacetController
    if (this.config.facet && this.config.facet.enabled) {
      if (!this.facetController) {
        this.facetController = new FacetController(this);
      }
      this.facetController.initialize();
      return;
    } else if (this.facetController) {
      this.facetController.destroy();
      this.facetController = null;
    }

    try {
      if (this.dom.chartArea) {
        this.dom.chartArea.selectAll("*").interrupt();
        var activeLabels = this.derived.currentLayers.map(function(l) { return l.label; });
        var allLabels = this.config.layers.map(function(l) { return l.label; });
        var chartArea = this.dom.chartArea;
        allLabels.forEach(function(label) {
          if (activeLabels.indexOf(label) === -1) {
            var safeName = String(label).replace(/\s+/g, "");
            chartArea.selectAll("[class*='tag-'][class*='-" + safeName + "']").remove();
          }
        });
      }
      this.emit("beforeRender", { options });
      renderChartTitle(this);
      this.derived.currentLayers = validateLayers(this);
      this.syncLegacyAliases();
      this.clearEmptyState();
      if (!isCurrent()) {
        return;
      }
      if (this.derived.currentLayers.length === 0) {
        this.renderEmptyState();
        if (!this.config.sparkline) {
          applyARIA(this);
        }
        return;
      }
      const state = deriveChartRender(this);
      applyDerivedScales(this, state);
      this.syncLegacyAliases();
      if (!isCurrent()) {
        return;
      }
      addFAB(this);
      this.emit("afterScales", { state });
      syncAxes(this, state, options);
      this.routeLayers(this.derived.currentLayers);
      syncReferenceLines(this, state, options);
      syncLegend(this, state);
      bindRollover(this);
      removeBrush(this);
      if (this.config.interactions.brush && this.config.interactions.brush.enabled) {
        bindBrush(this);
      }
      if (this.config.interactions.annotation && this.config.interactions.annotation.enabled) {
        bindAnnotation(this);
      }
      if (this.config.interactions.linked && this.config.interactions.linked.enabled) {
        bindLinked(this);
      }
      if (this.config.interactions.linked && this.config.interactions.linked.cursor === true) {
        registerLinkedCursor(this);
      }
      if (this.config.interactions.sliders && this.config.interactions.sliders.length > 0) {
        bindSliders(this);
      }
      this.emit("afterRender", { state });
      if (!this.config.sparkline) {
        applyARIA(this);
      }
    } catch (error) {
      console.warn("[myIO] Render error:", error.message);
      this.emit("error", { message: error.message, error });
      throw error;
    }
  }

  clearEmptyState() {
    if (this.dom && this.dom.svg) {
      this.dom.svg.selectAll(".myIO-empty-state").remove();
    }
    if (this.dom && this.dom.element) {
      d3.select(this.dom.element).select(".myIO-fab").style("display", null);
    }
  }

  renderEmptyState() {
    if (this.dom.chartArea) {
      this.dom.chartArea.selectAll("*").interrupt().remove();
    }
    if (this.dom.plot) {
      this.dom.plot.selectAll(".x-axis, .y-axis").interrupt().remove();
      this.dom.plot.selectAll(".ref-x-line, .ref-y-line").remove();
    }
    removeHoverOverlay(this);
    hideChartTooltip(this);
    if (this.runtime && this.runtime._sheetOpen) {
      closePanel(this, { returnFocus: false });
    }
    if (this.dom.element) {
      d3.select(this.dom.element).select(".myIO-fab").style("display", "none");
    }
    if (this.dom.svg) {
      this.dom.svg.selectAll(".myIO-empty-state").remove();
      this.dom.svg.append("text")
        .attr("class", "myIO-empty-state")
        .attr("x", this.runtime.totalWidth / 2)
        .attr("y", this.runtime.height / 2)
        .text("No data to display");
    }
  }

  addButtons() {
    addFAB(this);
  }

  toggleVarY(newY) {
    this.runtime.activeY = newY[0];
    this.runtime.activeYFormat = newY[1];
    this.syncLegacyAliases();
    this.renderCurrentLayers();
  }

  toggleGroupedLayout(layers) {
    var data = getGroupedDataObject(layers, this);
    var colors = layers.map(function(layer) {
      return layer.color;
    });
    var bandwidth = ((this.runtime.width - (this.config.layout.margin.right + this.config.layout.margin.left)) / (data[0].length + 1)) / colors.length;

    if (this.runtime.layout === "stacked") {
      transitionGrouped(this, data, colors, bandwidth);
      this.runtime.layout = "grouped";
    } else {
      transitionStacked(this, data, colors, bandwidth);
      this.runtime.layout = "stacked";
    }
    this.syncLegacyAliases();
  }

  setClipPath(type) {
    switch (type) {
      case "donut":
      case "gauge":
        break;
      default:
        var chartHeight = getChartHeight(this);
        this.dom.clipPath = this.dom.chartArea.append("defs").append("svg:clipPath")
          .attr("id", this.dom.element.id + "clip")
          .append("svg:rect")
          .attr("x", 0)
          .attr("y", 0)
          .attr("width", this.runtime.width - (this.config.layout.margin.left + this.config.layout.margin.right))
          .attr("height", chartHeight - (this.config.layout.margin.top + this.config.layout.margin.bottom));
        this.dom.chartArea.attr("clip-path", "url(#" + this.dom.element.id + "clip)");
        this.syncLegacyAliases();
    }
  }

  routeLayers(layers) {
    var that = this;
    this.derived.layerIndex = this.config.layers.map(function(d) { return d.label; });
    this.syncLegacyAliases();
    layers.forEach(function(layer) {
      var renderer = getRendererForLayer(layer);
      if (renderer && typeof renderer.render === "function") {
        renderer.render(that, layer, layers);
        that.captureLegacyAliases();

        // Apply per-layer opacity
        var opacity = (layer.options && layer.options.opacity != null)
          ? layer.options.opacity : 1;
        if (opacity < 1) {
          var safeName = String(layer.label).replace(/\s+/g, "");
          that.dom.chartArea.selectAll("[class*='tag-'][class*='-" + safeName + "']")
            .style("opacity", opacity);
        }
      }
    });
  }

  removeLayers(labels) {
    labels.forEach((label) => {
      listRenderers().forEach(function(renderer) {
        if (typeof renderer.remove === "function") {
          renderer.remove(this, { label });
        } else {
          ["line", "bar", "point", "regression-line", "hexbin", "area", "crosshairY", "crosshairX"].forEach(function(prefix) {
            d3.selectAll("." + tagName(prefix, this.dom.element.id, label)).transition().duration(500).style("opacity", 0).remove();
          }, this);
        }
      }, this);
    });
  }

  dragPoints(layer) {
    bindPointDrag(this, layer);
  }

  updateOrdinalColorLegend(ly) {
    syncOrdinalLegendData(this, ly);
  }

  updateRegression(color, label) {
    const pointLayer = (this.config.layers || []).find(function(layer) {
      return layer.label === label && layer.type === "point";
    });
    if (!pointLayer) {
      return;
    }

    (this.config.layers || []).forEach(function(layer) {
      if (layer.type !== "line" || layer.transform !== "lm") {
        return;
      }
      if (!layer.mapping || !pointLayer.mapping) {
        return;
      }
      if (layer.mapping.x_var !== pointLayer.mapping.x_var || layer.mapping.y_var !== pointLayer.mapping.y_var) {
        return;
      }

      const regression = linearRegression(pointLayer.data, pointLayer.mapping.y_var, pointLayer.mapping.x_var);
      const derivedData = pointLayer.data
        .map(function(row) {
          return {
            ...row,
            [layer.mapping.y_var]: regression.fn(row[layer.mapping.x_var])
          };
        })
        .sort(function(a, b) {
          return a[layer.mapping.x_var] - b[layer.mapping.x_var];
        });

      layer.data = derivedData;
      getRenderer("line").render(this, { ...layer, color: color || layer.color }, this.config.layers);
    }, this);
  }

  updateChart(newConfig) {
    const oldLabels = this.derived.layerIndex || [];
    this.config = newConfig;
    this.derived.currentLayers = this.config.layers;
    this.syncLegacyAliases();
    const newLabels = this.config.layers.map(function(layer) { return layer.label; });
    const removed = oldLabels.filter(function(label) { return !newLabels.includes(label); });
    this.removeLayers(removed);
    this.renderCurrentLayers();
  }

  // Shiny proxy partial-update (myIOProxy): swap the data of existing layers by
  // label and re-render through the same data-join path as a normal update.
  // Unlike a full renderValue (which destroys + recreates the chart), this
  // preserves brush/zoom/toggle state and animates the transition. Unknown
  // labels are ignored; `updates` is [{ label, data }].
  updateData(updates) {
    if (!Array.isArray(updates) || !this.config || !Array.isArray(this.config.layers)) {
      return;
    }
    // Null-prototype map so a layer label like "__proto__"/"constructor" cannot
    // pollute Object.prototype, and an incoming update.label of "__proto__"
    // cannot spoof a match; the hasOwnProperty guard reinforces this.
    const byLabel = Object.create(null);
    this.config.layers.forEach(function(layer) { byLabel[layer.label] = layer; });
    updates.forEach(function(update) {
      if (update && Object.prototype.hasOwnProperty.call(byLabel, update.label)) {
        const layer = byLabel[update.label];
        const validData = Array.isArray(update.data) ||
          (layer.type === "treemap" && update.data !== null &&
            typeof update.data === "object" && !Array.isArray(update.data));
        if (validData) layer.data = update.data;
      }
    });
    // Mutating the shared layer objects updates whatever subset is currently
    // visible (derived.currentLayers references the same objects), so we do NOT
    // reset currentLayers here — that would re-show legend-toggled-off layers.
    this.syncLegacyAliases();
    this.renderCurrentLayers();
  }

  resize(width, height) {
    if (!width || !height || width < 2 || height < 2) return;
    const wasSheetOpen = this.runtime && this.runtime._sheetOpen === true;
    if (wasSheetOpen) {
      closePanel(this, { returnFocus: false });
    }
    this.runtime.totalWidth = Math.max(width, MIN_CHART_WIDTH);
    this.runtime.width = this.runtime.totalWidth;
    this.runtime.height = height;
    this.syncLegacyAliases();
    clearTimeout(this.runtime.resizeTimer);
    this.runtime.resizeTimer = setTimeout(() => {
      updateScaffoldLayout(this);
      this.captureLegacyAliases();
      this.renderCurrentLayers();
      if (wasSheetOpen && this.derived && this.derived.currentLayers && this.derived.currentLayers.length > 0) {
        openPanel(this);
      }
      this.emit("resize", { width: this.runtime.width, height: this.runtime.height });
    }, RESIZE_DEBOUNCE_MS);
  }

  destroy() {
    this.emit("destroy", {});
    destroyKeyframes(this);
    clearTimeout(this.runtime && this.runtime.resizeTimer);
    clearTimeout(this.runtime && this.runtime.tooltipHideTimer);
    if (this.facetController) {
      this.facetController.destroy();
      this.facetController = null;
    }
    if (this.keyboardNav) this.keyboardNav.destroy();
    if (this.dataTable) this.dataTable.destroy();
    if (this.themeManager) {
      this.themeManager.destroy();
    }
    if (this.runtime && this.runtime._sheetOpen) {
      closePanel(this, { returnFocus: false });
    }
    clearTimeout(this.runtime && this.runtime._sheetCloseTimer);
    removeBrush(this);
    removeAnnotationBindings(this);
    cleanupLinked(this);
    removeSliders(this);
    if (this.dom && this.dom.element) {
      d3.select(this.dom.element).on("keydown.brush", null);
    }
    if (this.dom && this.dom.chartArea) {
      this.dom.chartArea.selectAll("*").interrupt();
    }
    if (this.dom && this.dom.svg) {
      this.dom.svg.remove();
    }
    if (this.dom && this.dom.tooltip) {
      this.dom.tooltip.remove();
    }
    if (this.dom && this.dom.element) {
      d3.select(this.dom.element).selectAll(".myIO-fab, .myIO-panel, .myIO-sheet-backdrop").remove();
    }
    removeHoverOverlay(this);
    this._listeners = {};
    this.config = null;
    this.derived = null;
    this.dom = null;
    this.runtime = null;
  }
}
