module.exports = exports = {};

const $ = require('jquery');
const _ = require('lodash');
const Backbone = require('backbone');
const sha256 = require('sha256');
const THREE = require('three');
const {MeshLine, MeshLineMaterial} = require( 'three.meshline' );

const MicropedeAsync = require('@micropede/client/src/async.js');
const {MicropedeClient, DumpStack} = require('@micropede/client/src/client.js');

const THREEx = {}; require('threex-domevents')(THREE, THREEx);

const Arrow = require('./arrow')(THREE);
const ElectrodeControls = require('./electrode-controls');

const FindAllNeighbours = ElectrodeControls.FindAllNeighbours;
const MAX_DISTANCE = ElectrodeControls.MAX_DISTANCE;

const APPNAME = 'scicad';

const DEFAULT_HOST = 'localhost';

let mouseDown = false;

const RouteSchema = {
  type: "object",
  properties: {
    start: {set_with: "selected-routes", type: "string"},
    path:  {set_with: "selected-routes", type: "array", hidden: true},
    "trail-length": {set_with: "selected-routes", type: "integer", minimum: 1, default: 1},
    "repeat-duration-seconds": {set_with: "selected-routes", type: "number", minium: 0, default: 1},
    "transition-duration-seconds": {set_with: "selected-routes", type: "number", minimum: 0.1, default: 1},
    "route-repeats": {set_with: "selected-routes", type: "integer", minimum: 1, default: 1}
  },
  required: ['start']
}

const RouteControlSchema = {
  type: "object",
  properties: {
    "selected-routes": {
      type: "array",
      default: [],
      "items": RouteSchema,
    }
  },
};

class RouteControls extends MicropedeClient {
  constructor(scene, camera, electrodeControls, port=undefined) {
    let options = {resubscribe: false};
    if (window) options.storageUrl = window.location.origin;
    super(APPNAME, DEFAULT_HOST, port, undefined, undefined, options);

    if (window) {
      // Listen for mouse down only on electrodes:
      electrodeControls.on("mousedown", async (e) => {
        mouseDown = true;
        this.drawRoute(e);
      });

      // Listen for mouseup anywhere on the page window:
      window.addEventListener("mouseup", (e) => {
        mouseDown = false;
        this.trigger("mouseup", e);
      });

    };

    electrodeControls.on("mouseover", (e) => this.trigger("mouseover", e));
    this.electrodeControls = electrodeControls;
    this.lines = {};
    this.scene = scene;
    this.model = new Backbone.Model({routes: []});
    this.model.on("change:routes", this.renderRoutes.bind(this));
    this.port = port;
    this.selectedRoutes = [];
    this.highlightedRoutes = [];
    this.schema = RouteControlSchema;
  }

  async putSelectedRoutes(payload, params) {
    const LABEL = "RouteControls::putSelectedRoutes";
    try {
      // Get prev routes:
      let routes = await this.getState('routes', 'routes-model');
      const updatedRoutes = payload['selected-routes'];

      // Update prev routes based on changes:
      _.each(updatedRoutes, (r) => {
        let route = _.find(routes, {uuid: r.uuid});
        _.extend(route, r);
      });

      // Call put request to routes-model:
      const scicad = new MicropedeAsync('scicad', undefined, this.port);
      await scicad.putPlugin('routes-model', 'routes', routes);

      return this.notifySender(payload, 'updated', 'selected-routes');
    } catch (e) {
      return this.notifySender(payload, DumpStack(LABEL, e),
        'selected-routes', 'failed');
    }
  }

  listen() {
    this.onStateMsg("routes-model", "routes", this.renderRoutes.bind(this));
    this.bindPutMsg("routes-model", "route", "put-route");
    this.onPutMsg("selected-routes", this.putSelectedRoutes.bind(this));
  }

  get routes() {
    return _.cloneDeep(this.model.get("routes"));
  }
  async renderRoutes(routes) {
    const LABEL = "<RouteControls::renderRoutes>";

    let selectedRoutes = await this.getState('selected-routes');

    // Remove selected routes if now longer presetn
    if (_.intersection(_.map(routes, 'uuid'), _.map(selectedRoutes, 'uuid')).length <= 0){
      await this.setState('selected-routes', []);
    }

    const group = this.electrodeControls.svgGroup;
    const scicad = new MicropedeAsync(APPNAME, DEFAULT_HOST, this.port);
    const electrodes = (await scicad.triggerPlugin('device-model',
      'electrodes-from-routes', {routes}, 1002)).response;

    const removeLine = (line) => {
      if (line.arrow) this.scene.remove(line.arrow);
      this.scene.remove(line);
      delete this.lines[line.uuid];
    }

    // Reset all lines to not visited
    _.each(this.lines, (l)=>{l.visited = false});

    // Iterate through all routes
    for (const [i, sequence] of electrodes.entries()) {
      const uuid = sequence.uuid;
      const ids = sequence.ids;

      // Generate a hash based on the electrode ids;
      const hash = sha256(JSON.stringify(ids));

      // If line already exists for route,
      if (this.lines[uuid]) {
        // check if its path has changed (via a change in the id hash)
        if (this.lines.hash == hash) {
          this.lines[uuid].visited = true;
          continue;
        } else {
          removeLine(this.lines[uuid]);
        }
      }

      // Otherwise get the electrodeIds from the route, and draw a new line
      const line = GenerateLinesFromIds(ids, group);
      this.scene.add(line);
      line.arrow = Arrow(ids, group);
      this.scene.add( line.arrow );

      line.visited = true;
      line.uuid = uuid;
      line.hash = hash;
      this.lines[uuid] = line;
    }

    // Remove all lines not visited from scene (as they must have been removed)
    for (const [uuid, line] of Object.entries(this.lines)){
      if (line.visited == true) continue;
      removeLine(line);
    }

  }

  addRoute(localRoute) {
    const route = _.cloneDeep(localRoute);
    if (RouteIsValid(localRoute, this.electrodeControls)) {
      const routes = _.clone(this.model.get("routes"));
      routes.push(localRoute);
      this.model.set("routes", routes);
    }
  }
  createLocalRoute(path, maxDistance=MAX_DISTANCE) {
    const localRoute = new Object();
    localRoute.start = path[0];
    localRoute.path = [];
    for (var i=0;i<path.length;i++){
      if (i == 0) continue;
      const prev = path[i-1];
      const next = path[i];
      const neighbours = FindAllNeighbours(this.electrodeControls.svgGroup, prev, maxDistance);
      if (_.invert(neighbours)[next]) {
        localRoute.path.push(_.invert(neighbours)[next]);
      } else {
        // Path is invalid
        return undefined;
      }
    }
    return localRoute;
  }
  async selectRoute(id) {
    const lineWidth = 0.3;
    const scicad = new MicropedeAsync(APPNAME, DEFAULT_HOST, this.port);
    let routes = await this.getState('routes', 'routes-model');
    const absoluteRoutes = (await scicad.triggerPlugin('device-model',
        'electrodes-from-routes', {routes})).response;

    const colorRoutes = (str, routes) => {
      routes = routes || this.highlightedRoutes;
      const color = new THREE.Color(str);
      for (const [i, route] of routes.entries()){
        if (this.lines[route.uuid]) {
          this.lines[route.uuid].material = new MeshLineMaterial({color, lineWidth});
        } else {
          console.error('route with id:', route.uuid, 'no longer exists');
        }
      }
    }

    colorRoutes("rgb(99, 246, 255)", this.highlightedRoutes);
    this.highlightedRoutes = [];

    // Check which routes contain the id selected
    for (const [i, electrodes] of absoluteRoutes.entries()){
      const selected = _.includes(electrodes.ids, id);
      const uuid = electrodes.uuid;
      if (selected)
        this.highlightedRoutes.push(_.find(routes, {uuid}));
    }

    if (this.highlightedRoutes.length < 1) return;

    // Turn selected routes yellow
    colorRoutes("yellow", this.highlightedRoutes);

    // Listen for context menu action
    const selectCallback = async (e) => {
      colorRoutes("red", this.highlightedRoutes);
      await scicad.triggerPlugin('step-ui-plugin', 'change-schema', {name: 'route-controls'});
      colorRoutes("rgb(99, 246, 255)", this.selectedRoutes);
      this.selectedRoutes = _.clone(this.highlightedRoutes);
      this.highlightedRoutes = [];
      await this.setState('selected-routes', this.selectedRoutes);
      this.off("clear-route");
      this.off("execute-route");
      this.off("select-route");
    }

    const clearCallback = (e) => {
      const uuids = _.map(this.highlightedRoutes, 'uuid');
      routes = _.filter(routes, (r) => !_.includes(uuids, r.uuid));
      const scicad = new MicropedeAsync(APPNAME, DEFAULT_HOST, this.port);
      scicad.putPlugin('routes-model', 'routes', routes);
      this.off("clear-route");
      this.off("execute-route");
      this.off("select-route");
    }

    const execCallback = (e) => {
      const scicad = new MicropedeAsync(APPNAME, DEFAULT_HOST, this.port);
      if (this.highlightedRoutes.length <= 0 ) return;
      switch (e.key) {
        case "executeRoute":
          scicad.triggerPlugin('routes-model', 'execute',
            {routes: this.highlightedRoutes}, -1);
          break;
        case "executeRoutes":
          scicad.triggerPlugin('routes-model', 'execute',
            {routes: this.highlightedRoutes}, -1);
          break;
      }
      this.off("clear-route");
      this.off("execute-route");
      this.off("select-route");
    }

    this.on("clear-route", clearCallback.bind(this));
    this.on("execute-route", execCallback.bind(this));
    this.on("select-route", selectCallback.bind(this));

    // Wait for another click before returning to original color
    const mousedown = () => {
      return new Promise((resolve, reject) => {
        let listener;
        listener = (e) => {
          // remove listener once mouse down (i.e. listen once), then resolve
          document.removeEventListener("mousedown", listener);
          resolve(e);
        };
        document.addEventListener("mousedown", listener);
      });
    };

    const e = await mousedown();
    // colorRoutes("rgb(99, 246, 255)");
    // XXX: Find a better way to identify if should execute...
    if (e.target.innerText == 'Execute Route') return;
    if (e.target.innerText == 'Clear Route') return;
  }

  async drawRoute(e) {
    if (this.electrodeControls.enabled == false) return;
    if (e.origDomEvent.button == 2) {
      this.selectRoute(e.target.name);
      return;
    }

    /* Draw a route starting with electrode that triggered this event*/
    const lines = [];
    const path = [];
    const routes = _.clone(this.model.get("routes"));
    const group = this.electrodeControls.svgGroup;
    const scene = this.scene;
    let lastElectrode;

    let maxDistance;
    let scicad = new MicropedeAsync('scicad', undefined, this.port);
    try {
      maxDistance = await this.getState('max-distance', 'device-model');
    } catch (e) {
      maxDistance = MAX_DISTANCE;
    }

    // Add start electrode
    if (!mouseDown) return;
    lastElectrode = e.target.name;
    path.push(e.target.name);
    var line = AddToPath(e.target.name, path, group, maxDistance);

    const drawHandler = _.extend({}, Backbone.Events);

    const mouseup = () => {
      return new Promise((resolve, reject) => {
        drawHandler.listenTo(this, "mouseup", (e) => {
          resolve(e);
        });
      });
    };

    // Add all electrodes that are hovered over
    const mouseover = drawHandler.listenTo(this.electrodeControls, "mouseover", (e) => {
      if (!mouseDown) {
        this.trigger("mouseup");
        return;
      }
      lastElectrode = e.target.name;
      var line = AddToPath(e.target.name, path, group, maxDistance);
      if (line) {lines.push(line); scene.add(line);}
    });

    // Stop here until mouse is up:
    e = await mouseup();

    // Remove events
    drawHandler.stopListening();

    // Remove lines from scene
    for (const [i, line] of lines.entries()){
      if (line.arrow) this.scene.remove(line.arrow);
      this.scene.remove(line);
    }

    const localRoute = this.createLocalRoute(path, maxDistance);

    if (path.length > 1) {
      this.trigger("put-route", localRoute);
    } else {
      if (e.altKey) {
        console.log("SELECTING ROUTE!!");
        this.selectRoute(lastElectrode);
      }
    }
  }
}

const AddToPath = (name, path, group, maxDistance=MAX_DISTANCE) => {
  const prev = _.last(path);
  if (name == prev) return;
  let neighbours = [];
  // if (prev != undefined) {
  //   neighbours = FindAllNeighbours(group, prev, maxDistance);
  // }
  // if (!_.invert(neighbours)[name] && prev != undefined) return;
  if (path.length > 0) {
    // validate path before pushing:
    neighbours = FindAllNeighbours(group, prev, maxDistance);
    if (!_.invert(neighbours)[name]) return undefined;
    const line = GenerateLineFromElectrodeIds(prev, name, group);
    path.push(name);
    return line;
  }
  path.push(name);
  return undefined;
};

function GenerateLineFromElectrodeIds(id1, id2, group, resolution) {
  const color = new THREE.Color("rgb(99, 246, 255)");
  const lineWidth = 0.3;
  const material = new MeshLineMaterial({color, lineWidth, resolution});

  var geometry = new THREE.Geometry();
  for (const [i, id] of [id1, id2].entries()) {
    const obj = _.filter(group.children, {name: id})[0];
    const point = new THREE.Vector3();
    point.setFromMatrixPosition(obj.matrixWorld);
    point.z = 1;
    geometry.vertices.push(point);
  }

  const line = new MeshLine();
  line.setGeometry(geometry)
  return new THREE.Mesh(line.geometry, material);
}

function GenerateLinesFromIds(ids, group) {
  const LABEL = "<RouteControls::GenerateLinesFromIds>";

  /* Create line from list of electrodeIds */
  const color = new THREE.Color("rgb(99, 246, 255)");
  const lineWidth = 0.3;

  const material = new MeshLineMaterial({color, lineWidth});
  const geometry = new THREE.Geometry();

  const addPoint = (name) => {
    const obj = _.filter(group.children, {name})[0];
    const point = new THREE.Vector3();
    point.setFromMatrixPosition(obj.matrixWorld);
    point.z = 1;
    geometry.vertices.push(point);
  }

  for (const [i, id] of ids.entries()){
    addPoint(id);
  }

  const line = new MeshLine();
  line.setGeometry(geometry);

  return new THREE.Mesh(line.geometry, material);
}

function RouteIsValid(localRoute, electrodeControls) {
  const objects = electrodeControls.electrodeObjects;
  let prev = localRoute.start;
  for (const [i, dir] of localRoute.path.entries()){
    const neighbours = electrodeControls.getNeighbours(prev);
    const id = _.invert(neighbours)[dir];
    if (!id) return false;
    prev = id;
  }
  return true;
}
module.exports = {
  RouteControls: RouteControls,
  GenerateLineFromElectrodeIds: GenerateLineFromElectrodeIds,
  GenerateLinesFromIds: GenerateLinesFromIds,
  RouteIsValid: RouteIsValid
};
