

import * as THREE from '../node_modules/three/src/Three';

import { _vec2, _vec3, raycaster, awaitSelection, ptObj, setHover, custPtMat } from './shared'

import { drawOnClick1, drawOnClick2, drawPreClick2, drawOnClick3, drawPreClick3, drawClear, drawPoint } from './drawEvents'
import { onHover, onDrag, onPick, onRelease, clearSelection } from './mouseEvents'
import { setCoincident, setOrdinate, setTangent } from './constraintEvents'
import { get3PtArc } from './drawArc'
import { replacer, reviver } from './utils'
import { drawDimension, _onMoveDimension, setDimLines, updateDim } from './drawDimension';



class Sketch {


  constructor(scene, preload) {

    // [0]:x, [1]:y, [2]:z
    this.ptsBuf = new Float32Array(this.max_pts * 3).fill(NaN)

    // [0]:type, [1]:pt1, [2]:pt2, [3]:pt3, [4]:pt4
    this.linksBuf = new Float32Array(this.max_links * 5).fill(NaN)

    // [0]:type, [1]:val, [2]:pt1, [3]:pt2, [4]:lk1, [5]:lk2, [6]: other, [7]: other
    this.constraintsBuf = new Float32Array(this.max_constraints * 8).fill(NaN)


    this.plane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0);

    this.labelContainer = document.getElementById('labels')

    if (preload === undefined) {

      this.obj3d = new THREE.Group()
      this.obj3d.name = "s" + scene.sid++
      this.obj3d.userData.type = "sketch"
      this.obj3d.matrixAutoUpdate = false;

      this.objIdx = new Map()

      this.linkedObjs = new Map()
      this.l_id = 0;

      this.constraints = new Map()
      this.c_id = 1;

      this.dimGroup = new THREE.Group()
      this.dimGroup.name = 'dimensions'
      this.obj3d.add(this.dimGroup);
      this.geomStartIdx = this.obj3d.children.length
      this.obj3d.userData.geomStartIdx = this.geomStartIdx

      this.labels = []


      const p1 = ptObj()
      p1.matrixAutoUpdate = false;
      p1.userData.constraints = []
      this.obj3d.add(p1)
      this.updatePointsBuffer()





    } else {


      this.obj3d = preload.obj3d
      this.obj3d.inverse = preload.obj3d.matrix.clone().invert()

      this.objIdx = JSON.parse(preload.objIdx, reviver)

      this.linkedObjs = JSON.parse(preload.linkedObjs, reviver)
      this.l_id = preload.l_id;

      this.constraints = JSON.parse(preload.constraints, reviver)
      this.c_id = preload.c_id;

      this.geomStartIdx = this.obj3d.userData.geomStartIdx
      this.dimGroup = this.obj3d.children[this.geomStartIdx - 1]

      this.updatePointsBuffer()
      this.updateOtherBuffers()

      this.plane.applyMatrix4(this.obj3d.matrix)
    }


    this.scene = scene;
    this.camera = scene.camera
    this.canvas = scene.canvas
    this.rect = scene.rect






    this.bindHandlers()

    // this.selected = this.scene.selected
    // this.selected = []

    this.hovered = []
    this.scene.mode = ""
    this.subsequent = false;
  }

  toJSON() {
    return {
      obj3d: this.obj3d,

      objIdx: JSON.stringify(this.objIdx, replacer),

      linkedObjs: JSON.stringify(this.linkedObjs, replacer),
      l_id: this.l_id,

      constraints: JSON.stringify(this.constraints, replacer),
      c_id: this.c_id,
    }
  }


  bindHandlers() {
    this.drawOnClick1 = drawOnClick1.bind(this);
    this.drawPreClick2 = drawPreClick2.bind(this);
    this.drawOnClick2 = drawOnClick2.bind(this);

    this.drawPreClick3 = drawPreClick3.bind(this);
    this.drawOnClick3 = drawOnClick3.bind(this);

    this.drawDimension = drawDimension.bind(this)
    this._onMoveDimension = _onMoveDimension.bind(this)
    this.setDimLines = setDimLines.bind(this)
    this.updateDim = updateDim.bind(this)

    this.awaitSelection = awaitSelection.bind(this);

    this.onHover = onHover.bind(this);
    this.onPick = onPick.bind(this);
    this.onDrag = onDrag.bind(this);
    this.onRelease = onRelease.bind(this);
    this.onKeyPress = this.onKeyPress.bind(this);

    this.setHover = setHover.bind(this);
    this.clearSelection = clearSelection.bind(this)

  }


  setClean() {
    this.hasChanged = false
    this.idOnActivate = id
    this.c_idOnActivate = this.c_id

    const changeDetector = (e) => {
      if (this.scene.selected.length && e.buttons) {
        this.canvas.removeEventListener('pointermove', changeDetector)
        this.hasChanged = true
      }
    }

    this.canvas.addEventListener('pointermove', changeDetector)
  }


  activate() {
    window.addEventListener('keydown', this.onKeyPress)
    this.canvas.addEventListener('pointerdown', this.onPick)
    this.canvas.addEventListener('pointermove', this.onHover)


    this.setDimLines()

    this.obj3d.traverse(e => e.layers.enable(2))
    this.obj3d.visible = true
    this.scene.axes.matrix = this.obj3d.matrix
    this.scene.axes.visible = true
    this.scene.activeSketch = this

    window.sketcher = this

    this.setClean()

  }

  deactivate() {
    window.removeEventListener('keydown', this.onKeyPress)
    this.canvas.removeEventListener('pointerdown', this.onPick)
    this.canvas.removeEventListener('pointermove', this.onHover)

    this.labelContainer.innerHTML = ""
    this.obj3d.visible = false
    this.obj3d.traverse(e => e.layers.disable(2))
    this.scene.axes.visible = false
    this.scene.activeSketch = null

    this.disableLineHover = false
    if (this.scene.newSketch) {
      this.scene.newSketch = false
    }

    this.clearSelection()

  }


  align(origin, x_dir, y_dir) {
    const up = _vec3.subVectors(y_dir, origin).normalize();
    const te = _m1.elements;
    _x.subVectors(x_dir, origin).normalize();
    _z.crossVectors(_x, up).normalize();
    _y.crossVectors(_z, _x);

    te[0] = _x.x; te[4] = _y.x; te[8] = _z.x;
    te[1] = _x.y; te[5] = _y.y; te[9] = _z.y;
    te[2] = _x.z; te[6] = _y.z; te[10] = _z.z;

    this.obj3d.quaternion.setFromRotationMatrix(_m1);
    const parent = this.obj3d.parent;
    _m1.extractRotation(parent.matrixWorld);
    _q1.setFromRotationMatrix(_m1);
    this.obj3d.quaternion.premultiply(_q1.invert());
    this.obj3d.updateMatrix();
    this.obj3d.matrix.setPosition(origin)

    this.plane.applyMatrix4(this.obj3d.matrix)
    this.obj3d.inverse = this.obj3d.matrix.clone().invert()
  }



  onKeyPress(e) {

    if (e.isTrusted && e.key == 'Escape') {
      this.disableLineHover = false
      drawClear.call(this)

      this.scene.store.dispatch({ type: 'set-mode', mode: '' })
    } else {
      const keyToMode = {
        l: 'line',
        a: 'arc',
        p: 'point',
        d: 'dimension',
        c: 'coincident',
        v: 'vertical',
        h: 'horizontal',
        t: 'tangent',
        Delete: 'delete',
        Backspace: 'delete'
      }
      this.command(keyToMode[e.key])
    }
  }



  command(com) {
    this.disableLineHover = false
    drawClear.call(this)

    let mode;

    switch (com) {
      case 'delete':
        this.deleteSelected()
        break;
      case 'coincident':
      case 'vertical':
      case 'horizontal':
      case 'tangent':
        // need to fire synthetic event to clear any earlier awaitSelection (see shared.js)
        window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
      case 'line':
      case 'arc':
      case 'point':
      case 'dimension':
        if (this.scene.mode == com) {
          mode = ''
        } else {
          mode = com

          switch (com) {
            case 'line':
            case 'arc':
              this.disableLineHover = true
              this.canvas.addEventListener('pointerdown', this.drawOnClick1, { once: true });
              break;
            case 'point':
              this.disableLineHover = true
              this.canvas.addEventListener('pointerdown', (e) => {
                if (this.scene.mode !== 'point') return
                const pt = ptObj()

                pt.matrixAutoUpdate = false;
                pt.userData.constraints = []

                pt.geometry.attributes.position.set(
                  this.getLocation(e).toArray()
                )
                pt.layers.enable(2)

                this.obj3d.add(pt)
                this.updatePointsBuffer(this.obj3d.children.length - 1)
                this.scene.render()
              });
              break;
            case 'dimension':
              this.drawDimension();
              break;
            case 'coincident':
              setCoincident.call(this).then(
                () => this.scene.store.dispatch({ type: 'set-mode', mode: "" })
              );
              break;
            case 'vertical':
              setOrdinate.call(this, 0).then(
                () => this.scene.store.dispatch({ type: 'set-mode', mode: "" })
              );
              break;
            case 'horizontal':
              setOrdinate.call(this, 1).then(
                () => this.scene.store.dispatch({ type: 'set-mode', mode: "" })
              );
              break;
            case 'tangent':
              setTangent.call(this).then(
                () => this.scene.store.dispatch({ type: 'set-mode', mode: "" })
              );
              break;
          }
        }
        break;
    }

    if (mode !== undefined) {
      this.scene.store.dispatch({ type: 'set-mode', mode })
    }

  }

  deleteSelected() {

    this.scene.selected
      .filter(e => e.userData.type == 'dimension')
      .forEach(e => this.constraints.has(e.name) && this.deleteConstraints(e.name))

    const toDelete = this.scene.selected
      .filter(e => e.userData.type == 'line')
      .sort((a, b) => b.id - a.id)
      .map(obj => {
        return this.delete(obj)
      })

    if (toDelete.length) {
      this.updatePointsBuffer(toDelete[toDelete.length - 1])
    }

    this.updateOtherBuffers()

    // this.selected = []

    this.scene.store.dispatch({ type: 'clear-selection' })

    this.scene.render()
  }

  delete(obj) {
    if (!obj) return
    if (obj.userData.type == 'dimension') {
      this.deleteConstraints(obj.name)
      return
    }

    let link = this.linkedObjs.get(obj.userData.l_id)
    if (!link) return;
    link = link[1]

    let i = this.objIdx.get(link[0]) || this.updatePoint // hacky, see drawEvent.js for updatePoint def

    for (let j = 0; j < link.length; j++) {
      const obj = this.obj3d.children[i + j]
      // obj.geometry.dispose()
      // obj.material.dispose()

      obj.traverse((ob) => {
        if (ob.geometry) ob.geometry.dispose()
        if (ob.material) ob.material.dispose()
      })

      // collect all coincident constraints to be reconnected
      // after deleting this point
      let arr = []
      let cons
      for (let c_id of obj.userData.constraints.slice()) {
        // i hate js, slice is important because deleteContraints mutates constraints array
        cons = this.constraints.get(c_id)
        if (cons[0] == 'points_coincident') {
          arr.push(cons[2][0] == obj.name ?
            cons[2][1] : cons[2][0]
          )
        }
        this.deleteConstraints(c_id)
      }

      for (let i = 0; i < arr.length - 1; i++) {
        setCoincident.call(this, [
          this.obj3d.children[this.objIdx.get(arr[i])],
          this.obj3d.children[this.objIdx.get(arr[i + 1])]
        ])
      }

      obj.userData.constraints = []
    }

    this.obj3d.children.splice(i, link.length)

    this.linkedObjs.delete(obj.userData.l_id)

    return i
  }


  deleteConstraints(c_id) {
    for (let idx of this.constraints.get(c_id)[2]) { // clean on contraint references
      if (idx == -1) continue
      const ob = this.obj3d.children[this.objIdx.get(idx)]
      if (ob) {
        ob.userData.constraints.splice(ob.userData.constraints.indexOf(c_id), 1)
      }
    }
    this.constraints.delete(c_id)

    for (let i = 0; i < this.dimGroup.children.length; i++) {
      if (this.dimGroup.children[i].name == c_id) {
        this.dimGroup.children.splice(i, i + 2).forEach(
          e => {
            if (e.label) e.label.remove()
            e.geometry.dispose()
            e.material.dispose()
          }
        )
        break
      }
    }
  }

  updateOtherBuffers() {
    let i = 0
    for (let [key, obj] of this.constraints) {
      this.constraintsBuf.set(
        [
          this.constraintNum[obj[0]], obj[1],
          ...obj[2].map(ele => this.objIdx.get(ele) ?? 0), obj[3], obj[4]
        ],
        (i) * 8
      )
      i++
    }

    i = 0;
    for (let [key, obj] of this.linkedObjs) {
      this.linksBuf.set(
        [
          this.linkNum[obj[0]],
          ...obj[1].map(ele => this.objIdx.get(ele) ?? -1),
        ],
        (i) * 5
      )
      i++
    }

  }


  updatePointsBuffer(startingIdx = 0) {
    for (let i = startingIdx; i < this.obj3d.children.length; i++) {
      const obj = this.obj3d.children[i]
      this.objIdx.set(obj.name, i)
      if (obj.type == "Points") {
        this.ptsBuf.set(obj.geometry.attributes.position.array, 3 * i)
      }
    }
  }


  updateBoundingSpheres() {

    for (let x = this.geomStartIdx; x < this.obj3d.children.length; x++) { // geometry boundign spheres
      const obj = this.obj3d.children[x]
      obj.geometry.computeBoundingSphere()
    }

    for (let x = 0; x < this.dimGroup.children.length; x++) { // dimension bounding sphere
      const obj = this.dimGroup.children[x]
      obj.geometry.computeBoundingSphere()
    }
  }
  getLocation(e) {

    raycaster.setFromCamera(
      _vec2.set(
        (e.clientX - this.rect.left) / this.rect.width * 2 - 1,
        - (e.clientY - this.rect.top) / this.rect.height * 2 + 1
      ),
      this.camera
    );


    raycaster.ray.intersectPlane(this.plane, _vec3).applyMatrix4(this.obj3d.inverse)

    return _vec3
  }

  getScreenXY(arr) {
    return _vec3.set(...arr).applyMatrix4(this.obj3d.matrix).project(this.camera)
  }

  solve() {

    const pts_buffer =
      Module._malloc(this.ptsBuf.length * this.ptsBuf.BYTES_PER_ELEMENT)
    Module.HEAPF32.set(this.ptsBuf, pts_buffer >> 2)

    const constraints_buffer =
      Module._malloc(this.constraintsBuf.length * this.constraintsBuf.BYTES_PER_ELEMENT)
    Module.HEAPF32.set(this.constraintsBuf, constraints_buffer >> 2)

    const links_buffer =
      Module._malloc(this.linksBuf.length * this.linksBuf.BYTES_PER_ELEMENT)
    Module.HEAPF32.set(this.linksBuf, links_buffer >> 2)

    Module["_solver"](
      this.obj3d.children.length, pts_buffer,
      this.constraints.size, constraints_buffer,
      this.linkedObjs.size, links_buffer,
      this.geomStartIdx
    )

    /*
      - loop to update all the children that are points
      - we also sneak in updating lines children as well, by checking when ptsBuf[ptr] is NaN
    */

    for (let i = this.geomStartIdx, ptr = (pts_buffer >> 2) + this.geomStartIdx * 3; i < this.obj3d.children.length; i += 1, ptr += 3) {

      const pos = this.obj3d.children[i].geometry.attributes.position;
      if (isNaN(Module.HEAPF32[ptr])) {
        pos.array[0] = Module.HEAPF32[ptr - 6]
        pos.array[1] = Module.HEAPF32[ptr - 5]
        pos.array[3] = Module.HEAPF32[ptr - 3]
        pos.array[4] = Module.HEAPF32[ptr - 2]
      } else {
        pos.array[0] = Module.HEAPF32[ptr]
        pos.array[1] = Module.HEAPF32[ptr + 1]
      }

      pos.needsUpdate = true;
    }

    Module._free(pts_buffer)
    Module._free(links_buffer)
    Module._free(constraints_buffer)

    /*
      arcs were not updated in above loop, we go through all arcs linkedObjs 
      and updated based on the control pts (which were updated in loop above) 
    */

    for (let [k, obj] of this.linkedObjs) {
      if (obj[0] != 'arc') continue;
      const [p1, p2, c, arc] = obj[1].map(e => this.obj3d.children[this.objIdx.get(e)])

      let points

      points = get3PtArc(
        p1.geometry.attributes.position.array,
        p2.geometry.attributes.position.array,
        c.geometry.attributes.position.array
      );

      arc.geometry.attributes.position.set(points)
      arc.needsUpdate = true;
    }

    this.setDimLines()
  }


}

const _m1 = new THREE.Matrix4()
const _q1 = new THREE.Quaternion()
const _x = new THREE.Vector3();
const _y = new THREE.Vector3();
const _z = new THREE.Vector3();


Object.assign(Sketch.prototype,
  {
    linkNum: {
      'line': 0,
      'arc': 1
    },
    constraintNum: {
      points_coincident: 0,
      pt_pt_distance: 1,
      pt_plane_distance: 2,
      pt_line_distance: 3,
      pt_face_distance: 4,
      pt_in_plane: 5,
      pt_on_line: 6,
      pt_on_face: 7,
      equal_length_lines: 8,
      length_ratio: 9,
      eq_len_pt_line_d: 10,
      eq_pt_ln_distances: 11,
      equal_angle: 12,
      equal_line_arc_len: 13,
      symmetric: 14,
      symmetric_horiz: 15,
      symmetric_vert: 16,
      symmetric_line: 17,
      at_midpoint: 18,
      horizontal: 19,
      vertical: 20,
      diameter: 21,
      pt_on_circle: 22,
      same_orientation: 23,
      angle: 24,
      parallel: 25,
      perpendicular: 26,
      arc_line_tangent: 27,
      cubic_line_tangent: 28,
      equal_radius: 29,
      proj_pt_distance: 30,
      where_dragged: 31,
      curve_curve_tangent: 32,
      length_difference: 33,
      h_dist: 34,
      v_dist: 35,
    },


    max_pts: 1000,
    max_links: 1000,
    max_constraints: 1000,

  }
)



export { Sketch }


