import { plotCriteria, badgeCriteriaNone } from "./criteriaPlot.js";

const margin = { top: 30, right: 15, bottom: 45, left: 50 };

const symbolTypes = {
  circle: d3.symbolCircle, // default
  triangle: d3.symbolTriangle,
  cross: d3.symbolCross,
  wye: d3.symbolWye,
  star: d3.symbolStar,
  square: d3.symbolSquare,
  diamond: d3.symbolDiamond,
};

function getMetadataPlotConfiguration(metadata) {
  const markType = metadata.plotConfiguration.marks?.type || "circle";
  const markSize = metadata.plotConfiguration.marks?.size || "64";
  const markFill = metadata.plotConfiguration.marks?.fill || "007bff";
  const markStroke = metadata.plotConfiguration.marks?.stroke || "007bff";
  const markStrokeWidth = metadata.plotConfiguration.marks?.strokeWidth || 1;
  const markOpacity = metadata.plotConfiguration.marks?.opacity || 1;
  return {
    markType,
    markSize,
    markFill,
    markStroke,
    markStrokeWidth,
    markOpacity,
  };
}

export function scatterPlot(id, dataKey) {
  let data = preprocessData(dataKey.fetchedData);
  const metadata = dataKey.fetchedMetadata;

  clearPlotArea(id);

  const { width, height, svg } = createSVGContainer(id);

  if (!data || data.length === 0) {
    showNoDataMessage(svg, width, height, id);
    return;
  }

  setTitle(svg, width, metadata);

  formatData(data);

  const { x, y } = setScales(data, metadata, width, height);

  addClipPath(svg, id, width, height);

  plotCriteria(svg, width, height, y, metadata, id);

  drawLine(svg, data, x, y, id);
  drawErrorBars(svg, data, x, y, id, metadata);
  drawPoints(svg, data, x, y, id, metadata);

  addXAxis(svg, x, height, width, metadata);
  addYAxis(svg, y, height, metadata);
}

// --- Helper Functions ---

function preprocessData(data) {
  // If data is an object with x and y arrays returns tuple for each point
  if (
    data &&
    !Array.isArray(data) &&
    typeof data === "object" &&
    Array.isArray(data.x) &&
    Array.isArray(data.y)
  ) {
    return data.x.map((xVal, i) => ({
      x: xVal,
      y: data.y[i],
      xerr: data.xerr ? data.xerr[i] : undefined,
      yerr: data.yerr ? data.yerr[i] : undefined,
    }));
  }
  return data;
}

function clearPlotArea(id) {
  // Remove any precedent SVG element
  d3.select("#" + id)
    .selectAll("svg")
    .remove();
  // Remove placeholder title
  d3.select("#" + id + " h5").remove();
  // clean div element if previous error was shown
  d3.select("#" + id).text("");
}

function createSVGContainer(id) {
  const container = document.getElementById(id);
  // Dynamically retrieve container sizes
  const boundingRect = container.getBoundingClientRect();
  const width = boundingRect.width - margin.left - margin.right;
  const height = boundingRect.height - margin.top - margin.bottom;
  // Append the svg object to the body of the page
  const svg = d3
    .select("#" + id)
    .append("svg")
    .attr("width", width + margin.left + margin.right) // boundingRect.width
    .attr("height", height + margin.top + margin.bottom) // boundingRect.height
    .append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

  return { width, height, svg };
}

function showNoDataMessage(svg, width, height, id) {
  svg
    .append("text")
    .attr("x", width / 2)
    .attr("y", height / 2)
    .attr("text-anchor", "middle")
    .attr("font-size", 18)
    .attr("fill", "#888")
    .text("No data found for this plot.");
  badgeCriteriaNone(id);
}

function setTitle(svg, width, metadata) {
  svg
    .append("text")
    .attr("x", width / 2)
    .attr("y", -margin.top / 3)
    .attr("class", "scatterPlot-title")
    .text(
      metadata.plotConfiguration.title
        ? metadata.plotConfiguration.title
        : "Title placeholder"
    );
}

function formatData(data) {
  // Coerce the strings to numbers.
  data.forEach(function (d) {
    d.x = +d.x;
    d.y = +d.y;
    if (d.xerr !== undefined) d.xerr = +d.xerr;
    if (d.yerr !== undefined) d.yerr = +d.yerr;
  });
}

function getMin(data, key, errKey, scaleType) {
  if (scaleType === "log") {
    // Only consider values where (d[key] - d[errKey]) > 0
    return d3.min(
      data
        .map((d) => {
          if (d[errKey] !== undefined) {
            return d[key] < d[errKey] ? d[key] : d[key] - d[errKey];
          }
          return d[key];
        })
        .filter((v) => v > 0)
    );
  }
  return d3.min(data, (d) =>
    d[errKey] !== undefined ? d[key] - d[errKey] : d[key]
  );
}

function getMax(data, key, errKey) {
  return d3.max(data, (d) =>
    d[errKey] !== undefined ? d[key] + d[errKey] : d[key]
  );
}

// Map between scale type and d3 scale function
const scaleMap = {
  linear: () => d3.scaleLinear(),
  log: () => d3.scaleLog(),
  time: () => d3.scaleTime(), // Not implemented yet
  ordinal: () => d3.scaleOrdinal(), // Not implemented yet
  band: () => d3.scaleBand(), // Not implemented yet
  point: () => d3.scalePoint(), // Not implemented yet
};

function checkLogScale(type, arr, domain, axisName) {
  if (type === "log") {
    if (arr.some((v) => v <= 0)) {
      throw new Error(`Log scale requires all ${axisName} values > 0`);
    }
    if (domain && domain[0] <= 0) {
      throw new Error(`Log scale requires ${axisName} axis minimum domain > 0`);
    }
  }
}

function checkTimeScale(type, arr, axisName) {
  if (
    type === "time" &&
    arr.some((v) => !(v instanceof Date || typeof v === "number"))
  ) {
    throw new Error(
      `Time scale requires ${axisName} values to be Date objects or timestamps`
    );
  }
}

function setScales(data, metadata, width, height) {
  // Get scale type from metadata (default: linear)
  const xScaleType = metadata.plotConfiguration.x.scale || "linear";
  const yScaleType = metadata.plotConfiguration.y.scale || "linear";
  const xDomain = metadata.plotConfiguration.x.domain;
  const yDomain = metadata.plotConfiguration.y.domain;

  checkLogScale(
    xScaleType,
    data.map((d) => d.x),
    xDomain,
    "x"
  );
  checkLogScale(
    yScaleType,
    data.map((d) => d.y),
    yDomain,
    "y"
  );
  checkTimeScale(
    xScaleType,
    data.map((d) => d.x),
    "x"
  );
  checkTimeScale(
    yScaleType,
    data.map((d) => d.y),
    "y"
  );

  const x = scaleMap[xScaleType]
    ? scaleMap[xScaleType]().range([0, width])
    : d3.scaleLinear().range([0, width]);
  const y = scaleMap[yScaleType]
    ? scaleMap[yScaleType]().range([height, 0])
    : d3.scaleLinear().range([height, 0]);

  // Determine Min and Max for quantitative axes
  function setDomain(scaleType, domain, dataArr, key, errKey, scaleObj) {
    // Get distinct values for ordinal/band/point
    const getDistinct = (arr) => Array.from(new Set(arr));

    if (["linear", "log", "time"].includes(scaleType)) {
      const min = domain ? domain[0] : getMin(dataArr, key, errKey, scaleType);
      const max = domain ? domain[1] : getMax(dataArr, key, errKey);
      scaleObj.domain([min, max]);
    } else if (["ordinal", "point", "band"].includes(scaleType)) {
      scaleObj.domain(getDistinct(dataArr.map((d) => d[key])));
    }
  }

  setDomain(xScaleType, xDomain, data, "x", "xerr", x);
  setDomain(yScaleType, yDomain, data, "y", "yerr", y);

  return { x, y };
}

function addClipPath(svg, id, width, height) {
  // Create a clipPath to restrict drawing to the plot area
  svg
    .append("clipPath")
    .attr("id", `clip-${id}`)
    .append("rect")
    .attr("x", 0)
    .attr("y", 0)
    .attr("width", width)
    .attr("height", height);
}

function drawLine(svg, data, x, y, id) {
  // define the line connecting points
  const valueline = d3
    .line()
    .x((d) => x(d.x))
    .y((d) => y(d.y));
  // Draw the line, clipped to the plot area
  svg
    .append("path")
    .data([data])
    .attr("class", "scatterPlot-line")
    .attr("d", valueline)
    .attr("clip-path", `url(#clip-${id})`);
}

function drawPoints(svg, data, x, y, id, metadata) {
  // Get mark configuration from metadata
  const {
    markType,
    markSize,
    markFill,
    markStroke,
    markStrokeWidth,
    markOpacity,
  } = getMetadataPlotConfiguration(metadata);

  const symbolType = symbolTypes[markType] || d3.symbolCircle;
  const symbol = d3.symbol().type(symbolType).size(markSize);
  // Create subgroup with clip-path, to avoid conflict with transform function
  const pointsGroup = svg.append("g").attr("clip-path", `url(#clip-${id})`);

  pointsGroup
    .selectAll(".scatterPlot-point")
    .data(data)
    .enter()
    .append("path")
    .attr("transform", (d) => `translate(${x(d.x)},${y(d.y)})`)
    .attr("class", "scatterPlot-point")
    .attr("d", symbol)
    .attr("opacity", markOpacity)
    // using .attr("style", ...) to enforce priority over CSS rules
    .attr(
      "style",
      [
        `fill: ${markFill};`,
        `stroke: ${markStroke};`,
        `stroke-width: ${markStrokeWidth}px;`,
        `} !important;`,
      ].join(" ")
    );
}

function negativeLogWarning() {
  console.warn(
    `Crop negative values to avoid issues with log scale representation.`
  );
}

function drawErrorBars(svg, data, x, y, id, metadata) {
  const yScaleType = metadata.plotConfiguration.y.scale || "linear";
  const xScaleType = metadata.plotConfiguration.x.scale || "linear";
  const xMin = x.domain()[0];
  const yMin = y.domain()[0];

  // Vertical error bars (yerr)
  svg
    .selectAll(".scatterPlot-yerror")
    .data(data.filter((d) => d.yerr !== undefined))
    .enter()
    .append("line")
    .attr("class", "scatterPlot-yerror")
    .attr("x1", (d) => x(d.x))
    .attr("x2", (d) => x(d.x))
    .attr("y1", (d) => {
      if (yScaleType === "log" && d.yerr >= d.y) {
        negativeLogWarning();
        return y(yMin);
      }
      return y(d.y - d.yerr);
    })
    .attr("y2", (d) => y(d.y + d.yerr))
    .attr("clip-path", `url(#clip-${id})`);

  // Horizontal error bars (xerr)
  svg
    .selectAll(".scatterPlot-xerror")
    .data(data.filter((d) => d.xerr !== undefined))
    .enter()
    .append("line")
    .attr("class", "scatterPlot-xerror")
    .attr("y1", (d) => y(d.y))
    .attr("y2", (d) => y(d.y))
    .attr("x1", (d) => {
      if (xScaleType === "log" && d.xerr >= d.x) {
        negativeLogWarning();
        return x(xMin);
      }
      return x(d.x - d.xerr);
    })
    .attr("x2", (d) => x(d.x + d.xerr))
    .attr("stroke", "#333")
    .attr("stroke-width", 1)
    .attr("clip-path", `url(#clip-${id})`);
}

function addXAxis(svg, x, height, width, metadata) {
  let metadataX = metadata.plotConfiguration.x;
  let xLabelText = metadataX.label;
  if (metadataX.unit) {
    xLabelText += ` [${metadataX.unit}]`;
  }

  svg
    .append("g")
    .attr("transform", "translate(0," + height + ")")
    .call(d3.axisBottom(x))
    .append("text")
    .attr("y", (margin.bottom * 3) / 4)
    .attr("x", width / 2)
    .attr("class", "scatterPlot-xlabel")
    .attr("fill", "#000")
    .text(xLabelText);
}

function addYAxis(svg, y, height, metadata) {
  let metadataY = metadata.plotConfiguration.y;
  let yLabelText = metadataY.label;
  if (metadataY.unit) {
    yLabelText += ` [${metadataY.unit}]`;
  }

  svg
    .append("g")
    .call(d3.axisLeft(y))
    .append("text")
    .attr("transform", "rotate(-90)")
    .attr("x", -height / 2)
    .attr("y", (-margin.left * 2) / 3)
    .attr("class", "scatterPlot-ylabel")
    .attr("fill", "#000")
    .text(yLabelText);
}
