import { initWasm } from './wasm.js';
import { setupCanvas, resizeCanvas } from './canvas.js';
import { createShaderProgram } from './shaders.js';
import { draw } from './tsp-gl.js';
import { initPoints } from './utils.js';

const DEFAULT_POINT_COUNT = 36;
const MIN_POINTS = 4;
const MAX_POINTS = 96;
const RADIUS_SIZE = 0.006;

function clampPointCount(value) {
  if (Number.isNaN(value)) {
    return DEFAULT_POINT_COUNT;
  }

  return Math.max(MIN_POINTS, Math.min(MAX_POINTS, value));
}

function createPointFragmentShader(pointCount) {
  return `
  precision mediump float;

  uniform vec2 u_resolution;
  uniform vec2 u_centers[${pointCount}];
  
  void main() {
      vec2 position = gl_FragCoord.xy / u_resolution;
      float aspect = u_resolution.x / u_resolution.y;
      vec2 scaledPosition = vec2(position.x * aspect, position.y);
      float radius = ${RADIUS_SIZE};

      for (int i = 0; i < ${pointCount}; i++) {
          vec2 center = u_centers[i];
          vec2 scaledCenter = vec2(center.x * aspect, center.y);
          float distance = length(scaledPosition - scaledCenter) * (1.0 / aspect);

          if (distance <= radius) {
              gl_FragColor = vec4(0.5, center, 1.0);
              return;
          }
      }

      gl_FragColor = vec4(0.1, 0.1, 0.1, 1.0);
  }
  `;
}

function createLineFragmentShader() {
  return `
    precision mediump float;

    void main(void) {
        gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
    }
  `;
}

async function run() {
  const canvas = document.getElementById('tsp-canvas');
  const pointCountInput = document.getElementById('pointCountInput');
  const applyPointCountButton = document.getElementById(
    'applyPointCountButton'
  );
  const pointCountStatus = document.getElementById('pointCountStatus');

  if (
    !canvas ||
    !pointCountInput ||
    !applyPointCountButton ||
    !pointCountStatus
  ) {
    return;
  }

  const gl = canvas.getContext('webgl2');
  if (!gl) {
    pointCountStatus.textContent = 'WebGL2 unavailable in this browser.';
    return;
  }

  gl.clearColor(0.1, 0.1, 0.1, 1);

  const { initialisePoints, getPointOrder, memory } = await initWasm();

  const vertexCode = `
  attribute vec2 a_position;
  void main(void) {
      gl_Position = vec4(a_position, 0.0, 1.0);
  }
`;

  setupCanvas(gl, canvas);

  const positionBuffer = gl.createBuffer();
  const lineBuffer = gl.createBuffer();

  if (!positionBuffer || !lineBuffer) {
    pointCountStatus.textContent = 'Unable to initialise WebGL buffers.';
    return;
  }

  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
  const positions = [-1, -1, 3, -1, -1, 3];
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);

  const state = {
    scene: null,
    draggingIndex: -1,
  };

  function updateStatus(pointCount) {
    pointCountStatus.textContent = `${pointCount} point${pointCount === 1 ? '' : 's'}`;
  }

  function recalculateRoute(scene) {
    const pointOrderPtr = getPointOrder(scene.pointsPtr, scene.pointCount);
    scene.pointOrder = Array.from(
      new Uint32Array(memory.buffer, pointOrderPtr, scene.pointCount)
    );
  }

  function syncPointsToMemory(scene) {
    scene.pointArray = new Float32Array(
      memory.buffer,
      scene.pointsPtr,
      scene.pointCount * 2
    );

    for (let i = 0; i < scene.points.length; i++) {
      scene.pointArray[i * 2] = scene.points[i].cx;
      scene.pointArray[i * 2 + 1] = scene.points[i].cy;
    }
  }

  function initialiseScene(rawPointCount) {
    const pointCount = clampPointCount(rawPointCount);

    if (state.scene) {
      gl.deleteProgram(state.scene.program);
      gl.deleteProgram(state.scene.lineProgram);
    }

    const pointsPtr = initialisePoints(pointCount, Date.now());
    const pointArray = new Float32Array(
      memory.buffer,
      pointsPtr,
      pointCount * 2
    );
    const points = initPoints(pointArray);
    const program = createShaderProgram(
      gl,
      vertexCode,
      createPointFragmentShader(pointCount)
    );
    const lineProgram = createShaderProgram(
      gl,
      vertexCode,
      createLineFragmentShader()
    );

    state.scene = {
      pointCount,
      pointsPtr,
      pointArray,
      points,
      pointOrder: [],
      program,
      lineProgram,
    };

    recalculateRoute(state.scene);
    resizeCanvas(gl, canvas, program);
    updateStatus(pointCount);
    pointCountInput.value = String(pointCount);
  }

  canvas.addEventListener('mousedown', (e) => {
    if (!state.scene) {
      return;
    }

    const rect = canvas.getBoundingClientRect();
    const x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
    const y = 1 - ((e.clientY - rect.top) / rect.height) * 2;

    for (let i = 0; i < state.scene.points.length; i++) {
      const point = state.scene.points[i];
      const dx = 0.5 * x + 0.5 - point.cx;
      const dy = 0.5 * y + 0.5 - point.cy;
      const distance = Math.hypot(dx, dy);

      if (distance <= RADIUS_SIZE) {
        state.draggingIndex = i;
        break;
      }
    }
  });

  canvas.addEventListener('mousemove', (e) => {
    if (!state.scene || state.draggingIndex === -1) {
      return;
    }

    const rect = canvas.getBoundingClientRect();
    state.scene.points[state.draggingIndex].cx =
      (e.clientX - rect.left) / rect.width;
    state.scene.points[state.draggingIndex].cy =
      1 - (e.clientY - rect.top) / rect.height;

    syncPointsToMemory(state.scene);
    recalculateRoute(state.scene);
  });

  function stopDragging() {
    state.draggingIndex = -1;
  }

  canvas.addEventListener('mouseup', stopDragging);
  canvas.addEventListener('mouseleave', stopDragging);

  function applyPointCount() {
    const nextPointCount = clampPointCount(
      Number.parseInt(pointCountInput.value, 10)
    );
    initialiseScene(nextPointCount);
  }

  applyPointCountButton.addEventListener('click', applyPointCount);
  pointCountInput.addEventListener('keydown', (event) => {
    if (event.key === 'Enter') {
      applyPointCount();
    }
  });

  pointCountInput.addEventListener('blur', () => {
    pointCountInput.value = String(
      clampPointCount(Number.parseInt(pointCountInput.value, 10))
    );
  });

  window.addEventListener('resize', () => {
    if (!state.scene) {
      return;
    }

    resizeCanvas(gl, canvas, state.scene.program);
  });

  initialiseScene(DEFAULT_POINT_COUNT);

  function render() {
    if (state.scene) {
      draw(
        gl,
        state.scene.points,
        state.scene.program,
        state.scene.lineProgram,
        positionBuffer,
        lineBuffer,
        state.scene.pointOrder,
        1
      );
    }

    requestAnimationFrame(render);
  }

  render();
}

await run();
