import TinyQueue from 'tinyqueue';

interface PolyLabelResult
{
  x: number;
  y: number;
  d: number;
}

//
// polyLabel: given polygon, return contained point furthest from any edge, and that distance
//

export function polyLabel(poly: any, precision?: number, debug?: boolean): PolyLabelResult
{
  precision = precision || 1.0;

  // find the bounding box of the outer ring
  let minX: number, minY: number, maxX: number, maxY: number;
  for (let i: number = 0; i < poly[0].length; i++)
  {
      let p = poly[0][i];
      if (!i || p[0] < minX) minX = p[0];
      if (!i || p[1] < minY) minY = p[1];
      if (!i || p[0] > maxX) maxX = p[0];
      if (!i || p[1] > maxY) maxY = p[1];
  }

  let width: number = maxX - minX;
  let height: number = maxY - minY;
  let cellSize: number = Math.min(width, height);
  let h: number = cellSize / 2;

  if (cellSize === 0) return { x: minX, y: minY, d: 0 };

  // a priority queue of cells in order of their "potential" (max distance to polygon)
  let cellQueue = new TinyQueue<Cell>(undefined, compareMax);

  // cover polygon with initial cells
  for (let x: number = minX; x < maxX; x += cellSize)
  {
    for (let y: number = minY; y < maxY; y += cellSize)
      cellQueue.push(new Cell(x + h, y + h, h, poly));
  }

  // take centroid as the first best guess
  let bestCell: any = getCentroidCell(poly);

  // special case for rectangular polygons
  let bboxCell: Cell = new Cell(minX + width / 2, minY + height / 2, 0, poly);
  if (bboxCell.d > bestCell.d) bestCell = bboxCell;

  let numProbes = cellQueue.length;

  while (cellQueue.length)
  {
    // pick the most promising cell from the queue
    let cell = cellQueue.pop();

    // update the best cell if we found a better one
    if (cell.d > bestCell.d)
    {
      bestCell = cell;
      if (debug) console.log('found best %d after %d probes', Math.round(1e4 * cell.d) / 1e4, numProbes);
    }

    // do not drill down further if there's no chance of a better solution
    if (cell.max - bestCell.d <= precision) continue;

    // split the cell into four cells
    h = cell.h / 2;
    cellQueue.push(new Cell(cell.x - h, cell.y - h, h, poly));
    cellQueue.push(new Cell(cell.x + h, cell.y - h, h, poly));
    cellQueue.push(new Cell(cell.x - h, cell.y + h, h, poly));
    cellQueue.push(new Cell(cell.x + h, cell.y + h, h, poly));
    numProbes += 4;
  }

  if (debug)
  {
    console.log('num probes: ' + numProbes);
    console.log('best distance: ' + bestCell.d);
  }

  return { x: bestCell.x, y: bestCell.y, d: bestCell.d };
}

function compareMax(a: Cell, b: Cell) { return b.max - a.max; }

class Cell
{
  x: number;
  y: number;
  h: number;
  d: number;
  max: number;

  constructor(x: number, y: number, h: number, poly: any)
  {
    this.x = x; // cell center x
    this.y = y; // cell center y
    this.h = h; // half the cell size
    this.d = pointToPolygonDist(x, y, poly); // distance from cell center to polygon
    this.max = this.d + this.h * Math.SQRT2; // max distance to polygon within a cell
  }
}

// signed distance from point to polygon outline (negative if point is outside)
function pointToPolygonDist(x: number, y: number, poly: any): number
{
  let inside: boolean = false;
  let minDistSq: number = Infinity;

  for (let k: number = 0; k < poly.length; k++)
  {
    let ring: any = poly[k];

    for (let i: number = 0, len: number = ring.length, j: number = len - 1; i < len; j = i++)
    {
      let a: any = ring[i];
      let b: any = ring[j];

      if ((a[1] > y !== b[1] > y) &&
          (x < (b[0] - a[0]) * (y - a[1]) / (b[1] - a[1]) + a[0])) inside = !inside;

      minDistSq = Math.min(minDistSq, getSegDistSq(x, y, a, b));
    }
  }

  return (inside ? 1 : -1) * Math.sqrt(minDistSq);
}

// get polygon centroid
function getCentroidCell(poly: any): Cell
{
  let area: number = 0;
  let x: number = 0;
  let y: number = 0;
  let points: any = poly[0];

  for (let i: number = 0, len: number = points.length, j: number = len - 1; i < len; j = i++)
  {
    let a: any = points[i];
    let b: any = points[j];
    let f: number = a[0] * b[1] - b[0] * a[1];
    x += (a[0] + b[0]) * f;
    y += (a[1] + b[1]) * f;
    area += f * 3;
  }
  if (area === 0) return new Cell(points[0][0], points[0][1], 0, poly);
  return new Cell(x / area, y / area, 0, poly);
}

// get squared distance from a point to a segment
function getSegDistSq(px: number, py: number, a: any, b: any): number
{
  let x: number = a[0];
  let y: number = a[1];
  let dx: number = b[0] - x;
  let dy: number = b[1] - y;

  if (dx !== 0 || dy !== 0)
  {
    let t: number = ((px - x) * dx + (py - y) * dy) / (dx * dx + dy * dy);

    if (t > 1)
    {
      x = b[0];
      y = b[1];
    }
    else if (t > 0)
    {
      x += dx * t;
      y += dy * t;
    }
  }

  dx = px - x;
  dy = py - y;

  return dx * dx + dy * dy;
}
