const RENDER_TYPE = {
  LATEX: 1,
  FUNC: 2,
};
const THRESHOLD = 1e-12;
const FONT = "Courier New";
const FONT_HEIGHT_PX = 24;
const DX = 4;

const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
let state = {};
const initializeState = () => {
  const xLabels = [
    "Jan",
    "Feb",
    "Mar",
    "Apr",
    "May",
    "Jun",
    "Jul",
    "Aug",
    "Sept",
    "Oct",
    "Nov",
    "Dec",
    "Jan",
  ];
  const yLabels = ["Great", "Good", "Meh", "Bad", "Horrible"];
  return {
    width: canvas.parentElement.clientWidth,
    height: canvas.parentElement.clientHeight,
    xLabels,
    yLabels,
    yLabelPadding: 12,
    heights: Array(xLabels.length * DX - 1).fill(0),
  };
};

const dft = (heights, render = RENDER_TYPE.LATEX, threshold = THRESHOLD) => {
  const n = heights.length;
  return Array(n)
    .fill()
    .map((x, w) => {
      const rate = -2 * Math.PI * w;
      const s = heights.reduce(
        (a, x, i) => ({
          re: a.re + x * Math.cos((rate * i) / n),
          im: a.im + x * Math.sin((rate * i) / n),
        }),
        { re: 0, im: 0 }
      );

      Object.entries(s).forEach(
        ([key, value]) =>
          (s[key] = ((Math.abs(value) < threshold ? 0 : 1) * value) / n)
      );

      const amp = Math.sqrt(s.re * s.re + s.im * s.im);
      const phase = Math.atan2(s.im, s.re);

      switch (render) {
        case RENDER_TYPE.LATEX:
          return `${amp}\\cos\\left(${w}\\frac{${
            2 * DX
          }\\pi}{${n}}x+${phase}\\right)`;
        case RENDER_TYPE.FUNC:
          return (t) => amp * Math.cos(w * t + phase);
      }
    });
};

const resizeCanvas = ({ width, height }) => {
  canvas.width = width;
  canvas.style.width = width;
  canvas.height = height;
  canvas.style.height = height;
};

const loop = () => {
  const stateChanges = Object.keys(state.diff);
  if (stateChanges.length > 0) {
    state = { ...state, ...state.diff };
    if (
      state.diff.width ||
      state.diff.height ||
      state.diff.xLabels ||
      state.diff.yLabels
    ) {
      resizeCanvas(state.diff);
      ctx.font = `${FONT_HEIGHT_PX}px ${FONT}`;
      state.maxYLabelWidth = state.yLabels.reduce(
        (a, label) => Math.max(ctx.measureText(label).width, a),
        -Infinity
      );
      
      state.gridBoxWidth =
        state.width - state.maxYLabelWidth - state.yLabelPadding;
      state.gridBoxHeight = state.height - 2.5 * FONT_HEIGHT_PX; // 2.5 to include bottom part of tall letters ("g", "y", etc.)

      state.topLeftGridPos = {
        x: state.maxYLabelWidth + state.yLabelPadding,
        y: FONT_HEIGHT_PX,
      };

      state.bottomRightGridPos = {
        x: state.topLeftGridPos.x + state.gridBoxWidth,
        y: state.topLeftGridPos.y + state.gridBoxHeight,
      };
    }
    if (state.diff.heights) drawDesmos();
    draw(state);
    state.diff = {};
  }

  requestAnimationFrame(loop);
};

const drawLine = (pos1, pos2) => {
  ctx.beginPath();
  ctx.moveTo(pos1.x, pos1.y);
  ctx.lineTo(pos2.x, pos2.y);
  ctx.stroke();
};

const drawDividers = (
  xDividers,
  yDividers,
  topLeftGridPos,
  bottomRightGridPos,
  yLabelPadding
) => {
  ctx.font = `${FONT_HEIGHT_PX}px ${FONT}`;
  xDividers.forEach(({ label, position }) => {
    ctx.fillText(label, position.x - ctx.measureText(label).width, position.y);
    drawLine(
      { ...position, y: topLeftGridPos.y },
      { ...position, y: bottomRightGridPos.y }
    );
  });
  yDividers.forEach(({ label, position }) => {
    ctx.fillText(
      label,
      topLeftGridPos.x - yLabelPadding - ctx.measureText(label).width,
      position.y
    );
    drawLine(
      { ...position, x: topLeftGridPos.x },
      { ...position, x: bottomRightGridPos.x }
    );
  });
};

const draw = ({
  heights,
  gridBoxWidth,
  gridBoxHeight,
  topLeftGridPos,
  bottomRightGridPos,
  maxYLabelWidth,
  xLabels,
  yLabels,
  width,
  height,
}) => {
  ctx.clearRect(0, 0, width, height);

  const xDividers = xLabels.map((label, i) => ({
    label,
    position: {
      x: topLeftGridPos.x + (gridBoxWidth / (xLabels.length - 1)) * i,
      y: bottomRightGridPos.y + FONT_HEIGHT_PX,
    },
  }));

  const yDividers = yLabels.map((label, i) => ({
    label,
    position: {
      x: 0,
      y: topLeftGridPos.y + (gridBoxHeight / (yLabels.length - 1)) * i,
    },
  }));

  drawDividers(xDividers, yDividers, topLeftGridPos, bottomRightGridPos, 12);

  const dx = gridBoxWidth / (DX * (xLabels.length - 1));
  const prevStrokeStyle = ctx.strokeStyle;
  ctx.strokeStyle = "red";
  for (let i = 0; i < heights.length; ++i) {
    const x = dx * i + topLeftGridPos.x;
    drawLine(
      { x, y: (gridBoxHeight / 2) * (1 - heights[i]) + topLeftGridPos.y },
      {
        x: x + dx,
        y: (gridBoxHeight / 2) * (1 - heights[i + 1]) + topLeftGridPos.y,
      }
    );
  }
  ctx.strokeStyle = prevStrokeStyle;
};

const calculator = Desmos.GraphingCalculator(
  document.getElementById("calculator"),
  {
    expressionsCollapsed: true,
    autosize: true,
  }
);
calculator.setMathBounds({
  left: -0.8,
  right: 12,
  bottom: -3,
  top: 3,
});

const drawDesmos = () => {
  const equations = dft(state.heights);

  calculator.setExpression({
    id: `graph-total`,
    latex: equations.map((_x, i) => `y_{${i}}`).join(" + "),
  });
  equations.forEach((x, i) =>
    calculator.setExpression({
      id: `graph${i}`,
      latex: `y_{${i}}=${x}`,
      hidden: true,
    })
  );
};

let isDown = false;
canvas.addEventListener(
  "mousedown",
  (e) => {
    e.preventDefault();
    isDown = true;
  },
  true
);

canvas.addEventListener(
  "mouseup",
  (e) => {
    e.preventDefault();
    isDown = false;
  },
  true
);

canvas.addEventListener(
  "mousemove",
  (e) => {
    e.preventDefault();
    if (isDown) {
      const rect = canvas.getBoundingClientRect();
      const [x, y] = [e.clientX - rect.left, e.clientY - rect.top];

      const {
        topLeftGridPos,
        bottomRightGridPos,
        gridBoxWidth,
        gridBoxHeight,
        heights,
        xLabels,
      } = state;
      const delta = gridBoxWidth / (DX * (xLabels.length - 1));

      const bin = Math.min(
        Math.round(Math.max(x - topLeftGridPos.x, 0) / delta),
        heights.length - 1
      );
      heights[bin] = Math.min(
        Math.max(1 - (2 * (y - topLeftGridPos.y)) / gridBoxHeight, -1),
        1
      );
      state.diff.heights = heights;
    }
  },
  true
);

window.addEventListener("resize", () => {
  state.diff = {
    ...state.diff,
    width: canvas.parentElement.clientWidth,
    height: canvas.parentElement.clientHeight,
  };
});

(() => {
  state.diff = initializeState();
  window.requestAnimationFrame(loop);
})();
