import Vector from 'math/vector';

class Tree {
  
  constructor() {
    this.children = null;
  }

  isLeaf() {
    return this.children == null;
  }

  leafs(callback) {
    if (this.isLeaf()) {
      callback(this);
    } else {
      for (const child of this.children) {
        child.leafs(callback);
      }
    }
  }
}

export class Tile extends Tree {
  
  constructor(surface, bottom, right, top, left) {
    super();
    this.surface = surface;
    this.edges = [bottom, right, top, left]
    this.edges.forEach(e => e.tile = this);
  }
  
  divideHalf(vert) {
    const shift = vert ? 0 : 1;

    const bottom = this.edges[0 + shift];
    const right = this.edges[1 + shift];
    const up = this.edges[2 + shift];
    const left = this.edges[(3 + shift) % 4];

    const bottomDivs = bottom.split();
    const upDivs = up.split();
    const newEdge = new TileEdge(bottomDivs[0].b, upDivs[0].b);
    if (vert) {
      this.children = [
        new Tile(this.surface, bottomDivs[0], newEdge, upDivs[1], left),
        new Tile(this.surface, bottomDivs[1], right, upDivs[0], newEdge.twin),
      ];
    } else {
      this.children = [
        new Tile(this.surface, left, bottomDivs[0], newEdge, upDivs[1]),
        new Tile(this.surface, newEdge.twin, bottomDivs[1], right, upDivs[0]),
      ];
    }
  }


  divideVertically() {
    this.divideHalf(true);
  }

  divideHorizontally() {
    this.divideHalf(false);
  }

  divideBoth() {
    throw 'not implemented'    
  }

  center() {
    if (!this._center) {
      this._center = new UVPoint(mid(this.edges[0].a.u, this.edges[0].b.u), mid(this.edges[1].a.v, this.edges[1].b.v), this.surface);
    }
    return this._center;
  }
}

export class TileEdge extends Tree {

  constructor(a, b, outer, twin) {
    super();
    this.tile = null; // to be set by a Tile
    this.a = a;
    this.b = b;
    this.outer = outer;
    if (twin == undefined) {
      twin = new TileEdge(b, a, outer, this);
    }
    this.twin = twin;
  }

  split() {
    if (this.children == null) {
      const midPoint = new UVPoint(mid(this.a.u, this.b.u), mid(this.a.v, this.b.v), this.tile.surface);
      this.children = [
        new TileEdge(this.a, midPoint, this.outer),
        new TileEdge(midPoint, this.b, this.outer),
      ];
      this.twin.children = [
        this.children[1].twin,
        this.children[0].twin
      ]
    }
    return this.children; 
  }
}

class UVPoint {
  constructor(u, v, surface) {
    this.u = u;
    this.v = v;
    this.surface = surface;
  }
  
  normal() {
    if (!this._normal) {
      this._normal = new Vector().set3(this.surface.normal(this.u, this.v))._normalize();
    }
    return this._normal;
  }
}

export function initTiles(surface, opts) {

  const data = surface._data;
  let nSplitsU = (data.controlPoints.length - 1);
  let nSplitsV = (data.controlPoints[0].length - 1);

  if (opts.maxUSplits && nSplitsU > opts.maxUSplits) {
    nSplitsU = opts.maxUSplits;
  }
  
  if (opts.maxVSplits && nSplitsV > opts.maxVSplits) {
    nSplitsV = opts.maxVSplits;
  }
  
  
  const umax = data.knotsU[data.knotsU.length - 1];
  const umin = data.knotsU[0];
  const vmax = data.knotsV[data.knotsV.length - 1];
  const vmin = data.knotsV[0];
  const du = (umax - umin) / nSplitsU;
  const dv = (vmax - vmin) / nSplitsV;

  const table = [];

  for (let vIdx = 0; vIdx < nSplitsV + 1; ++vIdx) {
    const row = [];
    table.push(row);
    for (let uIdx = 0; uIdx < nSplitsU + 1; ++uIdx) {
      const u = umin + du * uIdx;
      const v = vmin + dv * vIdx;
      row.push(new UVPoint(u, v, surface));
    }
    if (row.length <= 1) {
      return [];
    }
  }

  if (table.length <= 1) {
    return [];
  }
  
  const tiles = [];


  for (let vIdx = 0; vIdx < nSplitsV; ++vIdx) {
    const row = [];
    tiles.push(row);
    for (let uIdx = 0; uIdx < nSplitsU; ++uIdx) {

      const bottomOuter = vIdx == 0            ? 'bottom' : undefined;
      const leftOuter   = uIdx == 0            ? 'left'   : undefined;
      const topOuter    = vIdx == nSplitsV - 1 ? 'top'    : undefined;
      const rightOuter  = uIdx == nSplitsU - 1 ? 'right'  : undefined;

      const left = uIdx != 0 ? row[uIdx - 1].edges[1].twin : new TileEdge(table[vIdx+1][uIdx], table[vIdx][uIdx], leftOuter);
      const bottom = vIdx != 0 ? tiles[vIdx - 1][uIdx].edges[2].twin : new TileEdge(table[vIdx][uIdx], table[vIdx][uIdx+1], bottomOuter);
      const right = new TileEdge(table[vIdx][uIdx+1], table[vIdx+1][uIdx+1], rightOuter);
      const top = new TileEdge(table[vIdx+1][uIdx+1], table[vIdx+1][uIdx], topOuter);
      row.push(new Tile(surface, bottom, right, top, left));
    }
  }
  return tiles;
}

function mid(a, b) {
  return a == b ? a : ((a + b) / 2.0);
}

export function refine(tiles, opts) {
  opts = opts || {};
  const uMax = opts.uMax === undefined ? 10 : opts.uMax;
  const vMax = opts.vMax === undefined ? 10 : opts.vMax;
  const normTol = 8.5e-2;
  
  function curvature(a, b) {
    return a.normal().minus(b.normal()).lengthSquared();
  }

  function edgeCurvature(e) {
    return curvature(e.a, e.b);
  }

  function check(tile, uLevel, vLevel) {

    const horizLimit = vLevel >= vMax;
    const vertLimit  = uLevel >= uMax;
    
    const splitHoriz = !horizLimit && edgeCurvature(tile.edges[1]) > normTol || edgeCurvature(tile.edges[3]) > normTol;
    let splitVert = false;
    if (!splitHoriz && !vertLimit) {
      splitVert =  edgeCurvature(tile.edges[0]) > normTol || edgeCurvature(tile.edges[2]) > normTol;
      if (!splitVert) {
        const center = tile.center();
        splitVert = 
          curvature(center, tile.edges[0].a) > normTol ||
          curvature(center, tile.edges[1].a) > normTol ||
          curvature(center, tile.edges[2].a) > normTol ||
          curvature(center, tile.edges[3].a) > normTol;
      }
    }

    if (splitHoriz) {
      tile.divideHorizontally();
      vLevel ++;
    } else if (splitVert) {
      tile.divideVertically();
      uLevel ++;
    }

    if (tile.children != null) {
      for (const subTile of tile.children) {
        check(subTile, uLevel, vLevel);
      }
    }   
  }
  
  for (const row of tiles) {
    for (const tile of row) {
      check(tile, 0, 0);
    }
  }
}