import CoordinateList from '../../geom/CoordinateList'
import hasInterface from '../../../../../hasInterface'
import ArrayList from '../../../../../java/util/ArrayList'
import KdNodeVisitor from './KdNodeVisitor'
import Envelope from '../../geom/Envelope'
import List from '../../../../../java/util/List'
import KdNode from './KdNode'

export default class KdTree {
  constructor () {
    this._root = null
    this._numberOfNodes = null
    this._tolerance = null
    if (arguments.length === 0) {
      KdTree.call(this, 0.0)
    } else if (arguments.length === 1) {
      const tolerance = arguments[0]
      this._tolerance = tolerance
    }
  }
  insert () {
    if (arguments.length === 1) {
      const p = arguments[0]
      return this.insert(p, null)
    } else if (arguments.length === 2) {
      const p = arguments[0]
      const data = arguments[1]
      if (this._root === null) {
        this._root = new KdNode(p, data)
        return this._root
      }
      if (this._tolerance > 0) {
        const matchNode = this.findBestMatchNode(p)
        if (matchNode !== null) {
          matchNode.increment()
          return matchNode
        }
      }
      return this.insertExact(p, data)
    }
  }
  query () {
    if (arguments.length === 1) {
      const queryEnv = arguments[0]
      const result = new ArrayList()
      this.query(queryEnv, result)
      return result
    } else if (arguments.length === 2) {
      if (arguments[0] instanceof Envelope && hasInterface(arguments[1], List)) {
        const queryEnv = arguments[0]
        const result = arguments[1]
        this.queryNoe(this._root, queryEnv, true, {
          interfaces_: function () {
            return [KdNodeVisitor]
          },
          visit: function (node) {
            result.add(node)
          }
        })
      } else if (arguments[0] instanceof Envelope && hasInterface(arguments[1], KdNodeVisitor)) {
        const queryEnv = arguments[0]
        const visitor = arguments[1]
        this.queryNode(this._root, queryEnv, true, visitor)
      }
    }
  }
  queryNode (currentNode, queryEnv, odd, visitor) {
    if (currentNode === null) return null
    let min = null
    let max = null
    let discriminant = null
    if (odd) {
      min = queryEnv.getMinX()
      max = queryEnv.getMaxX()
      discriminant = currentNode.getX()
    } else {
      min = queryEnv.getMinY()
      max = queryEnv.getMaxY()
      discriminant = currentNode.getY()
    }
    const searchLeft = min < discriminant
    const searchRight = discriminant <= max
    if (searchLeft) {
      this.queryNode(currentNode.getLeft(), queryEnv, !odd, visitor)
    }
    if (queryEnv.contains(currentNode.getCoordinate())) {
      visitor.visit(currentNode)
    }
    if (searchRight) {
      this.queryNode(currentNode.getRight(), queryEnv, !odd, visitor)
    }
  }
  findBestMatchNode (p) {
    const visitor = new BestMatchVisitor(p, this._tolerance)
    this.query(visitor.queryEnvelope(), visitor)
    return visitor.getNode()
  }
  isEmpty () {
    if (this._root === null) return true
    return false
  }
  insertExact (p, data) {
    let currentNode = this._root
    let leafNode = this._root
    let isOddLevel = true
    let isLessThan = true
    while (currentNode !== null) {
      if (currentNode !== null) {
        const isInTolerance = p.distance(currentNode.getCoordinate()) <= this._tolerance
        if (isInTolerance) {
          currentNode.increment()
          return currentNode
        }
      }
      if (isOddLevel) {
        isLessThan = p.x < currentNode.getX()
      } else {
        isLessThan = p.y < currentNode.getY()
      }
      leafNode = currentNode
      if (isLessThan) {
        currentNode = currentNode.getLeft()
      } else {
        currentNode = currentNode.getRight()
      }
      isOddLevel = !isOddLevel
    }
    this._numberOfNodes = this._numberOfNodes + 1
    const node = new KdNode(p, data)
    if (isLessThan) {
      leafNode.setLeft(node)
    } else {
      leafNode.setRight(node)
    }
    return node
  }
  interfaces_ () {
    return []
  }
  getClass () {
    return KdTree
  }
  static toCoordinates () {
    if (arguments.length === 1) {
      const kdnodes = arguments[0]
      return KdTree.toCoordinates(kdnodes, false)
    } else if (arguments.length === 2) {
      const kdnodes = arguments[0]
      const includeRepeated = arguments[1]
      const coord = new CoordinateList()
      for (const it = kdnodes.iterator(); it.hasNext();) {
        const node = it.next()
        const count = includeRepeated ? node.getCount() : 1
        for (let i = 0; i < count; i++) {
          coord.add(node.getCoordinate(), true)
        }
      }
      return coord.toCoordinateArray()
    }
  }
  static get BestMatchVisitor () { return BestMatchVisitor }
}

class BestMatchVisitor {
  constructor () {
    this._tolerance = null
    this._matchNode = null
    this._matchDist = 0.0
    this._p = null
    const p = arguments[0]
    const tolerance = arguments[1]
    this._p = p
    this._tolerance = tolerance
  }
  // visit removed since it is unstable for Rollup build
  visit (node) {
    // const dist = this._p.distance(node.getCoordinate())
    // const isInTolerance = dist <= this._tolerance
    // if (!isInTolerance) return null
    // let update = false
    // if (this._matchNode === null || dist < this._matchDist) || this._matchNode !== null && dist === this._matchDist && node.getCoordinate().compareTo(this._matchNode.getCoordinate()) < 1) update = true
    // if (update) {
    //   this._matchNode = node
    //   this._matchDist = dist
    // }
  }
  queryEnvelope () {
    const queryEnv = new Envelope(this._p)
    queryEnv.expandBy(this._tolerance)
    return queryEnv
  }
  getNode () {
    return this._matchNode
  }
  interfaces_ () {
    return [KdNodeVisitor]
  }
  getClass () {
    return BestMatchVisitor
  }
}
