// Position of the vertical axis.
const X_LINE = 50;
// General-use margin size.
const MARGIN = 5;
// Position of the horizontal axis, relative to the bottom.
const Y_LINE = 35;
// Minimum distance between time tick labels.
const MIN_TICK_DIST = 50;
// Radius for rounded rectangle corners.
const RADIUS = 3;
// Height of unit boxes.
const BOX_HEIGHT = 25;
// Distance between Y tick marks on the unit graph.
const Y_TICK_DIST = BOX_HEIGHT + 2;
// Rects used for mouseover detection.
// Objects of {x, y, x2, y2, i} where `i` is the index into UNIT_DATA.
let HIT_BOXES = [];
// Index into UNIT_DATA of the last unit hovered over by mouse.
let LAST_HOVER = null;
// Key is unit index, value is {x, y, width, rmeta_x} of the box.
let UNIT_COORDS = {};
// Map of unit index to the index it was unlocked by.
let REVERSE_UNIT_DEPS = {};
let REVERSE_UNIT_RMETA_DEPS = {};
for (let n=0; n<UNIT_DATA.length; n++) {
  let unit = UNIT_DATA[n];
  for (let unlocked of unit.unlocked_units) {
    REVERSE_UNIT_DEPS[unlocked] = n;
  }
  for (let unlocked of unit.unlocked_rmeta_units) {
    REVERSE_UNIT_RMETA_DEPS[unlocked] = n;
  }
}

function render_pipeline_graph() {
  if (UNIT_DATA.length == 0) {
    return;
  }
  let g = document.getElementById('pipeline-graph');
  HIT_BOXES.length = 0;
  g.onmousemove = pipeline_mousemove;
  const min_time = document.getElementById('min-unit-time').valueAsNumber;

  const units = UNIT_DATA.filter(unit => unit.duration >= min_time);

  const graph_height = Y_TICK_DIST * units.length;
  const {ctx, graph_width, canvas_width, canvas_height, px_per_sec} = draw_graph_axes('pipeline-graph', graph_height);
  const container = document.getElementById('pipeline-container');
  container.style.width = canvas_width;
  container.style.height = canvas_height;

  // Canvas for hover highlights. This is a separate layer to improve performance.
  const linectx = setup_canvas('pipeline-graph-lines', canvas_width, canvas_height);
  linectx.clearRect(0, 0, canvas_width, canvas_height);

  // Draw Y tick marks.
  for (let n=1; n<units.length; n++) {
    const y = graph_height - (n * Y_TICK_DIST);
    ctx.beginPath();
    ctx.moveTo(X_LINE, y);
    ctx.lineTo(X_LINE-5, y);
    ctx.stroke();
  }

  // Draw Y labels.
  ctx.textAlign = 'end';
  for (let n=0; n<units.length; n++) {
    let y = MARGIN + (Y_TICK_DIST * (n + 1)) - 13;
    ctx.fillText(n+1, X_LINE-4, y);
  }

  // Draw the graph.
  ctx.save();
  ctx.translate(X_LINE, MARGIN);

  // Compute x,y coordinate of each block.
  UNIT_COORDS = {};
  for (i=0; i<units.length; i++) {
    let unit = units[i];
    let y = i * Y_TICK_DIST + 1;
    let x = px_per_sec * unit.start;
    let rmeta_x = null;
    if (unit.rmeta_time != null) {
      rmeta_x = x + px_per_sec * unit.rmeta_time;
    }
    let width = Math.max(px_per_sec * unit.duration, 1.0);
    UNIT_COORDS[unit.i] = {x, y, width, rmeta_x};
  }

  // Draw the blocks.
  for (i=0; i<units.length; i++) {
    let unit = units[i];
    let {x, y, width, rmeta_x} = UNIT_COORDS[unit.i];

    HIT_BOXES.push({x: X_LINE+x, y:MARGIN+y, x2: X_LINE+x+width, y2: MARGIN+y+BOX_HEIGHT, i: unit.i});

    ctx.beginPath();
    ctx.fillStyle = unit.mode == 'run-custom-build' ? '#f0b165' : '#95cce8';
    roundedRect(ctx, x, y, width, BOX_HEIGHT, RADIUS);
    ctx.fill();

    if (unit.rmeta_time != null) {
      ctx.beginPath();
      ctx.fillStyle = '#aa95e8';
      let ctime = unit.duration - unit.rmeta_time;
      roundedRect(ctx, rmeta_x, y, px_per_sec * ctime, BOX_HEIGHT, RADIUS);
      ctx.fill();
    }
    ctx.fillStyle = "#000";
    ctx.textAlign = 'start';
    ctx.textBaseline = 'hanging';
    ctx.font = '14px sans-serif';
    const label = `${unit.name}${unit.target} ${unit.duration}s`;
    const text_info = ctx.measureText(label);
    const label_x = Math.min(x + 5.0, canvas_width - text_info.width - X_LINE);
    ctx.fillText(label, label_x, y + BOX_HEIGHT / 2 - 6);
    draw_dep_lines(ctx, unit.i, false);
  }
  ctx.restore();
}

// Draws lines from the given unit to the units it unlocks.
function draw_dep_lines(ctx, unit_idx, highlighted) {
  const unit = UNIT_DATA[unit_idx];
  const {x, y, rmeta_x} = UNIT_COORDS[unit_idx];
  ctx.save();
  for (const unlocked of unit.unlocked_units) {
    draw_one_dep_line(ctx, x, y, unlocked, highlighted);
  }
  for (const unlocked of unit.unlocked_rmeta_units) {
    draw_one_dep_line(ctx, rmeta_x, y, unlocked, highlighted);
  }
  ctx.restore();
}

function draw_one_dep_line(ctx, from_x, from_y, to_unit, highlighted) {
  if (to_unit in UNIT_COORDS) {
    let {x: u_x, y: u_y} = UNIT_COORDS[to_unit];
    ctx.strokeStyle = highlighted ? '#000' : '#ddd';
    ctx.setLineDash([2]);
    ctx.beginPath();
    ctx.moveTo(from_x, from_y+BOX_HEIGHT/2);
    ctx.lineTo(from_x-5, from_y+BOX_HEIGHT/2);
    ctx.lineTo(from_x-5, u_y+BOX_HEIGHT/2);
    ctx.lineTo(u_x, u_y+BOX_HEIGHT/2);
    ctx.stroke();
  }
}

function render_timing_graph() {
  if (CONCURRENCY_DATA.length == 0) {
    return;
  }
  const HEIGHT = 400;
  const AXIS_HEIGHT = HEIGHT - MARGIN - Y_LINE;
  const TOP_MARGIN = 10;
  const GRAPH_HEIGHT = AXIS_HEIGHT - TOP_MARGIN;

  const {canvas_width, graph_width, ctx} = draw_graph_axes('timing-graph', AXIS_HEIGHT);

  // Draw Y tick marks and labels.
  let max_v = 0;
  for (c of CONCURRENCY_DATA) {
    max_v = Math.max(max_v, c.active, c.waiting, c.inactive);
  }
  const px_per_v = GRAPH_HEIGHT / max_v;
  const {step, tick_dist, num_ticks} = split_ticks(max_v, px_per_v, GRAPH_HEIGHT);
  ctx.textAlign = 'end';
  for (n=0; n<num_ticks; n++) {
    let y = HEIGHT - Y_LINE - ((n + 1) * tick_dist);
    ctx.beginPath();
    ctx.moveTo(X_LINE, y);
    ctx.lineTo(X_LINE-5, y);
    ctx.stroke();
    ctx.fillText((n+1) * step, X_LINE-10, y+5);
  }

  // Label the Y axis.
  let label_y = (HEIGHT - Y_LINE) / 2;
  ctx.save();
  ctx.translate(15, label_y);
  ctx.rotate(3*Math.PI/2);
  ctx.textAlign = 'center';
  ctx.fillText('# Units', 0, 0);
  ctx.restore();

  // Draw the graph.
  ctx.save();
  ctx.translate(X_LINE, MARGIN);

  function coord(t, v) {
    return {
      x: graph_width * (t/DURATION),
      y: TOP_MARGIN + GRAPH_HEIGHT * (1.0 - (v / max_v))
    };
  }

  const cpuFillStyle = 'rgba(250, 119, 0, 0.2)';
  if (CPU_USAGE.length > 1) {
    ctx.beginPath();
    ctx.fillStyle = cpuFillStyle;
    let bottomLeft = coord(CPU_USAGE[0][0], 0);
    ctx.moveTo(bottomLeft.x, bottomLeft.y);
    for (let i=0; i < CPU_USAGE.length; i++) {
      let [time, usage] = CPU_USAGE[i];
      let {x, y} = coord(time, usage / 100.0 * max_v);
      ctx.lineTo(x, y);
    }
    let bottomRight = coord(CPU_USAGE[CPU_USAGE.length - 1][0], 0);
    ctx.lineTo(bottomRight.x, bottomRight.y);
    ctx.fill();
  }

  function draw_line(style, key) {
    let first = CONCURRENCY_DATA[0];
    let last = coord(first.t, key(first));
    ctx.strokeStyle = style;
    ctx.beginPath();
    ctx.moveTo(last.x, last.y);
    for (let i=1; i<CONCURRENCY_DATA.length; i++) {
      let c = CONCURRENCY_DATA[i];
      let {x, y} = coord(c.t, key(c));
      ctx.lineTo(x, last.y);
      ctx.lineTo(x, y);
      last = {x, y};
    }
    ctx.stroke();
  }

  draw_line('blue', function(c) {return c.inactive;});
  draw_line('red', function(c) {return c.waiting;});
  draw_line('green', function(c) {return c.active;});

  // Draw a legend.
  ctx.restore();
  ctx.save();
  ctx.translate(canvas_width-200, MARGIN);
  // background
  ctx.fillStyle = '#fff';
  ctx.strokeStyle = '#000';
  ctx.lineWidth = 1;
  ctx.textBaseline = 'middle'
  ctx.textAlign = 'start';
  ctx.beginPath();
  ctx.rect(0, 0, 150, 82);
  ctx.stroke();
  ctx.fill();

  ctx.fillStyle = '#000'
  ctx.beginPath();
  ctx.lineWidth = 2;
  ctx.strokeStyle = 'red';
  ctx.moveTo(5, 10);
  ctx.lineTo(50, 10);
  ctx.stroke();
  ctx.fillText('Waiting', 54, 11);

  ctx.beginPath();
  ctx.strokeStyle = 'blue';
  ctx.moveTo(5, 30);
  ctx.lineTo(50, 30);
  ctx.stroke();
  ctx.fillText('Inactive', 54, 31);

  ctx.beginPath();
  ctx.strokeStyle = 'green';
  ctx.moveTo(5, 50);
  ctx.lineTo(50, 50);
  ctx.stroke();
  ctx.fillText('Active', 54, 51);

  ctx.beginPath();
  ctx.fillStyle = cpuFillStyle
  ctx.fillRect(15, 60, 30, 15);
  ctx.fill();
  ctx.fillStyle = 'black';
  ctx.fillText('CPU Usage', 54, 71);

  ctx.restore();
}

function setup_canvas(id, width, height) {
  let g = document.getElementById(id);
  let dpr = window.devicePixelRatio || 1;
  g.width = width * dpr;
  g.height = height * dpr;
  g.style.width = width;
  g.style.height = height;
  let ctx = g.getContext('2d');
  ctx.scale(dpr, dpr);
  return ctx;
}

function draw_graph_axes(id, graph_height) {
  const scale = document.getElementById('scale').valueAsNumber;
  // Cap the size of the graph. It is hard to view if it is too large, and
  // browsers may not render a large graph because it takes too much memory.
  // 4096 is still ridiculously large, and probably won't render on mobile
  // browsers, but should be ok for many desktop environments.
  const graph_width = Math.min(scale * DURATION, 4096);
  const px_per_sec = graph_width / DURATION;
  const canvas_width = Math.max(graph_width + X_LINE + 30, X_LINE + 250);
  const canvas_height = graph_height + MARGIN + Y_LINE;
  let ctx = setup_canvas(id, canvas_width, canvas_height);
  ctx.fillStyle = '#f7f7f7';
  ctx.fillRect(0, 0, canvas_width, canvas_height);

  ctx.lineWidth = 2;
  ctx.font = '16px sans-serif';
  ctx.textAlign = 'center';

  // Draw main axes.
  ctx.beginPath();
  ctx.moveTo(X_LINE, MARGIN);
  ctx.lineTo(X_LINE, graph_height + MARGIN);
  ctx.lineTo(X_LINE+graph_width+20, graph_height + MARGIN);
  ctx.stroke();

  // Draw X tick marks.
  const {step, tick_dist, num_ticks} = split_ticks(DURATION, px_per_sec, graph_width);
  ctx.fillStyle = '#303030';
  for (let n=0; n<num_ticks; n++) {
    const x = X_LINE + ((n + 1) * tick_dist);
    ctx.beginPath();
    ctx.moveTo(x, canvas_height-Y_LINE);
    ctx.lineTo(x, canvas_height-Y_LINE+5);
    ctx.stroke();

    ctx.fillText(`${(n+1) * step}s`, x, canvas_height - Y_LINE + 20);
  }

  // Draw vertical lines.
  ctx.strokeStyle = '#e6e6e6';
  ctx.setLineDash([2, 4]);
  for (n=0; n<num_ticks; n++) {
    const x = X_LINE + ((n + 1) * tick_dist);
    ctx.beginPath();
    ctx.moveTo(x, MARGIN);
    ctx.lineTo(x, MARGIN+graph_height);
    ctx.stroke();
  }
  ctx.strokeStyle = '#000';
  ctx.setLineDash([]);
  return {canvas_width, canvas_height, graph_width, graph_height, ctx, px_per_sec};
}

// Determine the spacing and number of ticks along an axis.
function split_ticks(max_value, px_per_v, max_px) {
  const max_ticks = Math.floor(max_px / MIN_TICK_DIST);
  if (max_ticks <= 1) {
    // Graph is too small for even 1 tick.
    return {step: max_value, tick_dist: max_px, num_ticks: 1};
  }
  let step;
  if (max_value <= max_ticks) {
    step = 1;
  } else if (max_value <= max_ticks * 2) {
    step = 2;
  } else if (max_value <= max_ticks * 4) {
    step = 4;
  } else if (max_value <= max_ticks * 5) {
    step = 5;
  } else {
    step = 10;
    let count = 0;
    while (true) {
      if (count > 100) {
        throw Error("tick loop too long");
      }
      count += 1;
      if (max_value <= max_ticks * step) {
        break;
      }
      step += 10;
    }
  }
  const tick_dist = px_per_v * step;
  const num_ticks = Math.floor(max_value / step);
  return {step, tick_dist, num_ticks};
}

function codegen_time(unit) {
  if (unit.rmeta_time == null) {
    return null;
  }
  let ctime = unit.duration - unit.rmeta_time;
  return [unit.rmeta_time, ctime];
}

function roundedRect(ctx, x, y, width, height, r) {
  r = Math.min(r, width, height);
  ctx.beginPath();
  ctx.moveTo(x+r, y);
  ctx.lineTo(x+width-r, y);
  ctx.arc(x+width-r, y+r, r, 3*Math.PI/2, 0);
  ctx.lineTo(x+width, y+height-r);
  ctx.arc(x+width-r, y+height-r, r, 0, Math.PI/2);
  ctx.lineTo(x+r, y+height);
  ctx.arc(x+r, y+height-r, r, Math.PI/2, Math.PI);
  ctx.lineTo(x, y-r);
  ctx.arc(x+r, y+r, r, Math.PI, 3*Math.PI/2);
  ctx.closePath();
}

function pipeline_mouse_hit(event) {
  // This brute-force method can be optimized if needed.
  for (let box of HIT_BOXES) {
    if (event.offsetX >= box.x && event.offsetX <= box.x2 &&
        event.offsetY >= box.y && event.offsetY <= box.y2) {
      return box;
    }
  }
}

function pipeline_mousemove(event) {
  // Highlight dependency lines on mouse hover.
  let box = pipeline_mouse_hit(event);
  if (box) {
    if (box.i != LAST_HOVER) {
      LAST_HOVER = box.i;
      let g = document.getElementById('pipeline-graph-lines');
      let ctx = g.getContext('2d');
      ctx.clearRect(0, 0, g.width, g.height);
      ctx.save();
      ctx.translate(X_LINE, MARGIN);
      ctx.lineWidth = 2;
      draw_dep_lines(ctx, box.i, true);

      if (box.i in REVERSE_UNIT_DEPS) {
        const dep_unit = REVERSE_UNIT_DEPS[box.i];
        if (dep_unit in UNIT_COORDS) {
          const {x, y, rmeta_x} = UNIT_COORDS[dep_unit];
          draw_one_dep_line(ctx, x, y, box.i, true);
        }
      }
      if (box.i in REVERSE_UNIT_RMETA_DEPS) {
        const dep_unit = REVERSE_UNIT_RMETA_DEPS[box.i];
        if (dep_unit in UNIT_COORDS) {
          const {x, y, rmeta_x} = UNIT_COORDS[dep_unit];
          draw_one_dep_line(ctx, rmeta_x, y, box.i, true);
        }
      }
      ctx.restore();
    }
  }
}

render_pipeline_graph();
render_timing_graph();

// Set up and handle controls.
{
  const range = document.getElementById('min-unit-time');
  const time_output = document.getElementById('min-unit-time-output');
  time_output.innerHTML = `${range.value}s`;
  range.oninput = event => {
    time_output.innerHTML = `${range.value}s`;
    render_pipeline_graph();
  };

  const scale = document.getElementById('scale');
  const scale_output = document.getElementById('scale-output');
  scale_output.innerHTML = `${scale.value}`;
  scale.oninput = event => {
    scale_output.innerHTML = `${scale.value}`;
    render_pipeline_graph();
    render_timing_graph();
  };
}
