Source: face.js

/** @module */

import { signedArea, pointInPoly, polygonize } from './utils'
import { insertFace, updateFaceTree, trigger } from './topo'
import { sid } from './edge'

/**
 * Face definition
 *
 * @typedef {Object} Face
 * @property {number} id Face ID
 */

function getNodeByFace (topo, face) {
  // TODO: only within face mbr
  return topo.nodes.filter(n => n.face === face)
}

export function getFaceByPoint (topo, c, tol) {
  const result = topo.facesTree.search({
    minX: c[0] - tol,
    minY: c[1] - tol,
    maxX: c[0] + tol,
    maxY: c[1] + tol
  })

  const fs = result
    .map(f => ({
      f,
      s: getFaceGeometry(topo, f)[0]
    }))

  const candidates = fs
    .filter(fs => pointInPoly(c, fs.s))
    .map(fs => fs.f)

  // TODO: throw exception on more than one candidate?

  return candidates
}

/**
 * Returns the ordered set of signed edge identifiers met by walking on an a given edge side.
 */
export function getRingEdges (topo, edge, dir, limit, foundEdges) {
  foundEdges = foundEdges || []
  foundEdges.push({ edge, dir })

  const nextEdge = dir ? edge.nextLeft : edge.nextRight
  const nextDir = dir ? edge.nextLeftDir : edge.nextRightDir

  if (!foundEdges.some(fe => fe.edge === nextEdge && fe.dir === nextDir)) {
    return getRingEdges(topo, nextEdge, nextDir, 0, foundEdges)
  }

  return foundEdges
}

function getEdgeByFace (topo, face, mbr) {
  return topo.edges.filter(e => e.leftFace === face || e.rightFace === face)
  // TODO: include within mbr
}

/**
 * Returns the polygon in the given topology with the specified face.
 * @param {module:topo~Topo} topo
 * @param {module:face~Face} face
 * @return {number[][][]}
 */
export function getFaceGeometry (topo, face) {
  const edges = getEdgeByFace(topo, face, false)
  if (edges.length === 0) {
    return []
  } else {
    const css = edges.map(e => e.coordinates)
    return polygonize(css)
  }
}

function getInteriorEdgePoint (coordinates) {
  // TODO: too naive
  return coordinates[1]
}

/**
 * @param {module:topo~Topo} topo
 * @param {module:edge~Edge} edge
 * @param {boolean} dir
 * @param {module:face~Face} face
 * @param {boolean} mbrOnly
 * @return {module:face~Face}
 * @private
 */
export function addFaceSplit (topo, edge, dir, face, mbrOnly) {
  console.debug(`addFaceSplit called on edge ${sid(edge, dir)} and face ${face.id}`)

  const universe = topo.faces[0]

  const sedges = getRingEdges(topo, edge, dir, 0)

  sedges.forEach((se, i) => console.debug(`Component ${i} in ring of edge ${edge.id} is edge ${sid(se.edge, se.dir)}`))

  if (sedges.length === 0) throw new Error('no ring edges for edge' + edge.id)

  console.debug(`getRingEdges returned ${sedges.length} edges`)

  if (sedges.some(se => se.edge === edge && se.dir === !dir)) {
    console.debug('not a ring')
    return 0
  }

  console.debug(`Edge ${sid(edge, dir)} split face ${face.id} (mbr_only:${mbrOnly})`)

  const newFace = { id: topo.facesSeq }

  // const ringEdges = sedges.map(se => se.edge).filter((elem, pos, arr) => arr.indexOf(elem) === pos)

  sedges.forEach((se, i) => {
    console.debug(`Edge ${i} in ring of edge ${sid(edge, dir)} is edge ${sid(se.edge, se.dir)}`)
  })

  const shell = sedges
    .map(e => e.dir ? e.edge.coordinates : e.edge.coordinates.slice().reverse())
    .reduce((a, b) => a.concat(b), [])

  const isccw = signedArea(shell) <= 0
  console.debug(`Ring of edge ${edge.id} is ${isccw ? 'counter' : ''}clockwise`)

  if (face === universe) {
    if (!isccw) {
      console.debug('The left face of this clockwise ring is the universe, will not create a new face there.')
      return -1
    }
  }

  if (mbrOnly && face !== universe) {
    if (isccw) {
      // TODO: update mbr for face
    }
    return -1
  }

  if (face !== universe && !isccw) {
    // TODO: newFace mbr shuld be same as face
  } else {
    // TODO: newFace mbr shuld be shellbox
  }

  const newFaceIsOutside = face !== universe && !isccw

  if (newFaceIsOutside) {
    console.debug('New face is on the outside of the ring, updating rings in former shell')
  } else {
    console.debug('New face is on the inside of the ring, updating forward edges in new ring')
  }

  const faceEdges = getEdgeByFace(topo, face, newFace)

  console.debug(`getEdgeByFace returned ${faceEdges.length} edges`)

  faceEdges.forEach(e => {
    let found = 0
    sedges.every(se => {
      if (e === se.edge) {
        if (se.dir) {
          console.debug(`Edge ${e.id} is a forward edge of the new ring`)
          e.leftFace = newFace
        } else {
          console.debug(`Edge ${e.id} is a backward edge of the new ring`)
          e.rightFace = newFace
        }
        found++
        if (found === 2) return false
      }
      return true
    })
    if (found > 0) return

    const ep = getInteriorEdgePoint(edge.coordinates)
    const contains = pointInPoly(ep, shell)

    if (newFaceIsOutside) {
      if (contains) {
        console.debug(`Edge ${e.id} contained in an hole of the new face`)
        return
      }
    } else {
      if (!contains) {
        console.debug(`Edge ${e.id} not contained in the face shell`)
        return
      }
    }

    if (e.leftFace === face) {
      console.debug(`Edge ${e.id} has new face on the left side`)
      e.leftFace = newFace
    }

    if (e.rightFace === face) {
      console.debug(`Edge ${e.id} has new face on the right side`)
      e.rightFace = newFace
    }
  })

  const nodes = getNodeByFace(topo, face)

  nodes.forEach(n => {
    const contains = pointInPoly(n.coordinate, shell)
    console.debug(`Node ${n.id} is ${contains ? '' : 'not '}contained in new ring, newface is ${newFaceIsOutside ? 'outside' : 'inside'}`)
    if (newFaceIsOutside) {
      if (contains) {
        console.debug(`Node ${n.id} contained in an hole of the new face`)
        return
      }
    } else {
      if (!contains) {
        console.debug(`Node ${n.id} contained in the face shell`)
        return
      }
    }
    n.face = newFace
  })

  insertFace(topo, newFace)
  updateFaceTree(topo, newFace)

  trigger(topo, 'addface', newFace)

  return newFace
}