import QuadEdge from './QuadEdge'
import CoordinateList from '../../geom/CoordinateList'
import HashSet from '../../../../../java/util/HashSet'
// import WKTWriter from '../../io/WKTWriter'
import GeometryFactory from '../../geom/GeometryFactory'
import Coordinate from '../../geom/Coordinate'
import IllegalArgumentException from '../../../../../java/lang/IllegalArgumentException'
import Stack from '../../../../../java/util/Stack'
import LastFoundQuadEdgeLocator from './LastFoundQuadEdgeLocator'
import LocateFailureException from './LocateFailureException'
import Vertex from './Vertex'
import System from '../../../../../java/lang/System'
import LineSegment from '../../geom/LineSegment'
import ArrayList from '../../../../../java/util/ArrayList'
import Envelope from '../../geom/Envelope'
import Triangle from '../../geom/Triangle'
import TriangleVisitor from './TriangleVisitor'

export default class QuadEdgeSubdivision {
  constructor () {
    this._visitedKey = 0
    this._quadEdges = new ArrayList()
    this._startingEdge = null
    this._tolerance = null
    this._edgeCoincidenceTolerance = null
    this._frameVertex = new Array(3).fill(null)
    this._frameEnv = null
    this._locator = null
    this._seg = new LineSegment()
    this._triEdges = new Array(3).fill(null)
    const env = arguments[0]
    const tolerance = arguments[1]
    this._tolerance = tolerance
    this._edgeCoincidenceTolerance = tolerance / QuadEdgeSubdivision.EDGE_COINCIDENCE_TOL_FACTOR
    this.createFrame(env)
    this._startingEdge = this.initSubdiv()
    this._locator = new LastFoundQuadEdgeLocator(this)
  }
  getTriangleVertices (includeFrame) {
    const visitor = new TriangleVertexListVisitor()
    this.visitTriangles(visitor, includeFrame)
    return visitor.getTriangleVertices()
  }
  isFrameVertex (v) {
    if (v.equals(this._frameVertex[0])) return true
    if (v.equals(this._frameVertex[1])) return true
    if (v.equals(this._frameVertex[2])) return true
    return false
  }
  isVertexOfEdge (e, v) {
    if (v.equals(e.orig(), this._tolerance) || v.equals(e.dest(), this._tolerance)) {
      return true
    }
    return false
  }
  connect (a, b) {
    const q = QuadEdge.connect(a, b)
    this._quadEdges.add(q)
    return q
  }
  getVoronoiCellPolygon (qe, geomFact) {
    const cellPts = new ArrayList()
    const startQE = qe
    do {
      const cc = qe.rot().orig().getCoordinate()
      cellPts.add(cc)
      qe = qe.oPrev()
    } while (qe !== startQE)
    const coordList = new CoordinateList()
    coordList.addAll(cellPts, false)
    coordList.closeRing()
    if (coordList.size() < 4) {
      System.out.println(coordList)
      coordList.add(coordList.get(coordList.size() - 1), true)
    }
    const pts = coordList.toCoordinateArray()
    const cellPoly = geomFact.createPolygon(geomFact.createLinearRing(pts), null)
    const v = startQE.orig()
    cellPoly.setUserData(v.getCoordinate())
    return cellPoly
  }
  setLocator (locator) {
    this._locator = locator
  }
  initSubdiv () {
    const ea = this.makeEdge(this._frameVertex[0], this._frameVertex[1])
    const eb = this.makeEdge(this._frameVertex[1], this._frameVertex[2])
    QuadEdge.splice(ea.sym(), eb)
    const ec = this.makeEdge(this._frameVertex[2], this._frameVertex[0])
    QuadEdge.splice(eb.sym(), ec)
    QuadEdge.splice(ec.sym(), ea)
    return ea
  }
  isFrameBorderEdge (e) {
    const leftTri = new Array(3).fill(null)
    QuadEdgeSubdivision.getTriangleEdges(e, leftTri)
    const rightTri = new Array(3).fill(null)
    QuadEdgeSubdivision.getTriangleEdges(e.sym(), rightTri)
    const vLeftTriOther = e.lNext().dest()
    if (this.isFrameVertex(vLeftTriOther)) return true
    const vRightTriOther = e.sym().lNext().dest()
    if (this.isFrameVertex(vRightTriOther)) return true
    return false
  }
  makeEdge (o, d) {
    const q = QuadEdge.makeEdge(o, d)
    this._quadEdges.add(q)
    return q
  }
  visitTriangles (triVisitor, includeFrame) {
    this._visitedKey++
    const edgeStack = new Stack()
    edgeStack.push(this._startingEdge)
    const visitedEdges = new HashSet()
    while (!edgeStack.empty()) {
      const edge = edgeStack.pop()
      if (!visitedEdges.contains(edge)) {
        const triEdges = this.fetchTriangleToVisit(edge, edgeStack, includeFrame, visitedEdges)
        if (triEdges !== null) triVisitor.visit(triEdges)
      }
    }
  }
  isFrameEdge (e) {
    if (this.isFrameVertex(e.orig()) || this.isFrameVertex(e.dest())) return true
    return false
  }
  isOnEdge (e, p) {
    this._seg.setCoordinates(e.orig().getCoordinate(), e.dest().getCoordinate())
    const dist = this._seg.distance(p)
    return dist < this._edgeCoincidenceTolerance
  }
  getEnvelope () {
    return new Envelope(this._frameEnv)
  }
  createFrame (env) {
    const deltaX = env.getWidth()
    const deltaY = env.getHeight()
    let offset = 0.0
    if (deltaX > deltaY) {
      offset = deltaX * 10.0
    } else {
      offset = deltaY * 10.0
    }
    this._frameVertex[0] = new Vertex((env.getMaxX() + env.getMinX()) / 2.0, env.getMaxY() + offset)
    this._frameVertex[1] = new Vertex(env.getMinX() - offset, env.getMinY() - offset)
    this._frameVertex[2] = new Vertex(env.getMaxX() + offset, env.getMinY() - offset)
    this._frameEnv = new Envelope(this._frameVertex[0].getCoordinate(), this._frameVertex[1].getCoordinate())
    this._frameEnv.expandToInclude(this._frameVertex[2].getCoordinate())
  }
  getTriangleCoordinates (includeFrame) {
    const visitor = new TriangleCoordinatesVisitor()
    this.visitTriangles(visitor, includeFrame)
    return visitor.getTriangles()
  }
  getVertices (includeFrame) {
    const vertices = new HashSet()
    for (let i = this._quadEdges.iterator(); i.hasNext();) {
      const qe = i.next()
      const v = qe.orig()
      if (includeFrame || !this.isFrameVertex(v)) vertices.add(v)
      const vd = qe.dest()
      if (includeFrame || !this.isFrameVertex(vd)) vertices.add(vd)
    }
    return vertices
  }
  fetchTriangleToVisit (edge, edgeStack, includeFrame, visitedEdges) {
    let curr = edge
    let edgeCount = 0
    let isFrame = false
    do {
      this._triEdges[edgeCount] = curr
      if (this.isFrameEdge(curr)) isFrame = true
      const sym = curr.sym()
      if (!visitedEdges.contains(sym)) edgeStack.push(sym)
      visitedEdges.add(curr)
      edgeCount++
      curr = curr.lNext()
    } while (curr !== edge)
    if (isFrame && !includeFrame) return null
    return this._triEdges
  }
  getEdges () {
    if (arguments.length === 0) {
      return this._quadEdges
    } else if (arguments.length === 1) {
      let geomFact = arguments[0]
      const quadEdges = this.getPrimaryEdges(false)
      const edges = new Array(quadEdges.size()).fill(null)
      let i = 0
      for (const it = quadEdges.iterator(); it.hasNext();) {
        const qe = it.next()
        edges[i++] = geomFact.createLineString([qe.orig().getCoordinate(), qe.dest().getCoordinate()])
      }
      return geomFact.createMultiLineString(edges)
    }
  }
  getVertexUniqueEdges (includeFrame) {
    const edges = new ArrayList()
    const visitedVertices = new HashSet()
    for (let i = this._quadEdges.iterator(); i.hasNext();) {
      const qe = i.next()
      const v = qe.orig()
      if (!visitedVertices.contains(v)) {
        visitedVertices.add(v)
        if (includeFrame || !this.isFrameVertex(v)) {
          edges.add(qe)
        }
      }
      const qd = qe.sym()
      const vd = qd.orig()
      if (!visitedVertices.contains(vd)) {
        visitedVertices.add(vd)
        if (includeFrame || !this.isFrameVertex(vd)) {
          edges.add(qd)
        }
      }
    }
    return edges
  }
  getTriangleEdges (includeFrame) {
    const visitor = new TriangleEdgesListVisitor()
    this.visitTriangles(visitor, includeFrame)
    return visitor.getTriangleEdges()
  }
  getPrimaryEdges (includeFrame) {
    this._visitedKey++
    const edges = new ArrayList()
    const edgeStack = new Stack()
    edgeStack.push(this._startingEdge)
    const visitedEdges = new HashSet()
    while (!edgeStack.empty()) {
      const edge = edgeStack.pop()
      if (!visitedEdges.contains(edge)) {
        const priQE = edge.getPrimary()
        if (includeFrame || !this.isFrameEdge(priQE)) edges.add(priQE)
        edgeStack.push(edge.oNext())
        edgeStack.push(edge.sym().oNext())
        visitedEdges.add(edge)
        visitedEdges.add(edge.sym())
      }
    }
    return edges
  }
  delete (e) {
    QuadEdge.splice(e, e.oPrev())
    QuadEdge.splice(e.sym(), e.sym().oPrev())
    const eSym = e.sym()
    const eRot = e.rot()
    const eRotSym = e.rot().sym()
    this._quadEdges.remove(e)
    this._quadEdges.remove(eSym)
    this._quadEdges.remove(eRot)
    this._quadEdges.remove(eRotSym)
    e.delete()
    eSym.delete()
    eRot.delete()
    eRotSym.delete()
  }
  locateFromEdge (v, startEdge) {
    let iter = 0
    const maxIter = this._quadEdges.size()
    let e = startEdge
    while (true) {
      iter++
      if (iter > maxIter) {
        throw new LocateFailureException(e.toLineSegment())
      }
      if (v.equals(e.orig()) || v.equals(e.dest())) {
        break
      } else if (v.rightOf(e)) {
        e = e.sym()
      } else if (!v.rightOf(e.oNext())) {
        e = e.oNext()
      } else if (!v.rightOf(e.dPrev())) {
        e = e.dPrev()
      } else {
        break
      }
    }
    return e
  }
  getTolerance () {
    return this._tolerance
  }
  getVoronoiCellPolygons (geomFact) {
    this.visitTriangles(new TriangleCircumcentreVisitor(), true)
    const cells = new ArrayList()
    const edges = this.getVertexUniqueEdges(false)
    for (let i = edges.iterator(); i.hasNext();) {
      const qe = i.next()
      cells.add(this.getVoronoiCellPolygon(qe, geomFact))
    }
    return cells
  }
  getVoronoiDiagram (geomFact) {
    const vorCells = this.getVoronoiCellPolygons(geomFact)
    return geomFact.createGeometryCollection(GeometryFactory.toGeometryArray(vorCells))
  }
  getTriangles (geomFact) {
    const triPtsList = this.getTriangleCoordinates(false)
    const tris = new Array(triPtsList.size()).fill(null)
    let i = 0
    for (const it = triPtsList.iterator(); it.hasNext();) {
      const triPt = it.next()
      tris[i++] = geomFact.createPolygon(geomFact.createLinearRing(triPt), null)
    }
    return geomFact.createGeometryCollection(tris)
  }
  insertSite (v) {
    let e = this.locate(v)
    if (v.equals(e.orig(), this._tolerance) || v.equals(e.dest(), this._tolerance)) {
      return e
    }
    let base = this.makeEdge(e.orig(), v)
    QuadEdge.splice(base, e)
    const startEdge = base
    do {
      base = this.connect(e, base.sym())
      e = base.oPrev()
    } while (e.lNext() !== startEdge)
    return startEdge
  }
  locate () {
    if (arguments.length === 1) {
      if (arguments[0] instanceof Vertex) {
        let v = arguments[0]
        return this._locator.locate(v)
      } else if (arguments[0] instanceof Coordinate) {
        let p = arguments[0]
        return this._locator.locate(new Vertex(p))
      }
    } else if (arguments.length === 2) {
      const p0 = arguments[0]
      const p1 = arguments[1]
      const e = this._locator.locate(new Vertex(p0))
      if (e === null) return null
      let base = e
      if (e.dest().getCoordinate().equals2D(p0)) base = e.sym()
      let locEdge = base
      do {
        if (locEdge.dest().getCoordinate().equals2D(p1)) return locEdge
        locEdge = locEdge.oNext()
      } while (locEdge !== base)
      return null
    }
  }
  interfaces_ () {
    return []
  }
  getClass () {
    return QuadEdgeSubdivision
  }
  static getTriangleEdges (startQE, triEdge) {
    triEdge[0] = startQE
    triEdge[1] = triEdge[0].lNext()
    triEdge[2] = triEdge[1].lNext()
    if (triEdge[2].lNext() !== triEdge[0]) throw new IllegalArgumentException('Edges do not form a triangle')
  }
  static get TriangleCircumcentreVisitor () { return TriangleCircumcentreVisitor }
  static get TriangleEdgesListVisitor () { return TriangleEdgesListVisitor }
  static get TriangleVertexListVisitor () { return TriangleVertexListVisitor }
  static get TriangleCoordinatesVisitor () { return TriangleCoordinatesVisitor }
  static get EDGE_COINCIDENCE_TOL_FACTOR () { return 1000 }
}

class TriangleCircumcentreVisitor {
  visit (triEdges) {
    const a = triEdges[0].orig().getCoordinate()
    const b = triEdges[1].orig().getCoordinate()
    const c = triEdges[2].orig().getCoordinate()
    const cc = Triangle.circumcentre(a, b, c)
    const ccVertex = new Vertex(cc)
    for (let i = 0; i < 3; i++) {
      triEdges[i].rot().setOrig(ccVertex)
    }
  }
  interfaces_ () {
    return [TriangleVisitor]
  }
  getClass () {
    return TriangleCircumcentreVisitor
  }
}

class TriangleEdgesListVisitor {
  constructor () {
    this._triList = new ArrayList()
  }
  getTriangleEdges () {
    return this._triList
  }
  visit (triEdges) {
    this._triList.add(triEdges.clone())
  }
  interfaces_ () {
    return [TriangleVisitor]
  }
  getClass () {
    return TriangleEdgesListVisitor
  }
}

class TriangleVertexListVisitor {
  constructor () {
    this._triList = new ArrayList()
  }
  visit (triEdges) {
    this._triList.add([triEdges[0].orig(), triEdges[1].orig(), triEdges[2].orig()])
  }
  getTriangleVertices () {
    return this._triList
  }
  interfaces_ () {
    return [TriangleVisitor]
  }
  getClass () {
    return TriangleVertexListVisitor
  }
}

class TriangleCoordinatesVisitor {
  constructor () {
    this._coordList = new CoordinateList()
    this._triCoords = new ArrayList()
  }
  checkTriangleSize (pts) {
    // let loc = ''
    // if (pts.length >= 2) loc = WKTWriter.toLineString(pts[0], pts[1]); else {
    //   if (pts.length >= 1) loc = WKTWriter.toPoint(pts[0])
    // }
  }
  visit (triEdges) {
    this._coordList.clear()
    for (let i = 0; i < 3; i++) {
      const v = triEdges[i].orig()
      this._coordList.add(v.getCoordinate())
    }
    if (this._coordList.size() > 0) {
      this._coordList.closeRing()
      const pts = this._coordList.toCoordinateArray()
      if (pts.length !== 4) {
        return null
      }
      this._triCoords.add(pts)
    }
  }
  getTriangles () {
    return this._triCoords
  }
  interfaces_ () {
    return [TriangleVisitor]
  }
  getClass () {
    return TriangleCoordinatesVisitor
  }
}
