/*jslint nomen: true, indent: 2, maxerr: 3, maxlen: 80 */
/*global console, getAltitude, getAltitudeRel, getInitialAltitude, gpsIsOk,
  getLatitude, getLongitude, getYaw, execUserScript, initPubsub, loiter,
  setAirSpeed, setMessage, setTargetCoordinates, std, triggerParachute,
  updateLogAndProjection, Drone, Worker*/
import {
  Drone,
  triggerParachute,
  getAirspeed,
  getAltitude,
  getClimbRate,
  getInitialAltitude,
  gpsIsOk,
  getPosition,
  getYaw,
  initPubsub,
  isLanding,
  loiter,
  setAirSpeed,
  setMessage,
  setTargetCoordinates,
  updateLogAndProjection
} from {{ json_module.dumps(qjs_wrapper) }};
import {
  SIGTERM,
  WNOHANG,
  Worker,
  close,
  exec,
  kill,
  pipe,
  setReadHandler,
  waitpid
} from "os";
import { evalScript, fdopen, loadFile, open } from "std";

(function (Drone, SIGTERM, WNOHANG, Worker, close, console, evalScript, exec,
           fdopen, getAltitude, getInitialAltitude, gpsIsOk, getPosition,
           getYaw, initPubsub, kill, isLanding, loadFile, loiter, open, pipe,
           setAirSpeed, setMessage, setReadHandler, setTargetCoordinates,
           triggerParachute, updateLogAndProjection, waitpid) {
  // Every script is evaluated per drone
  "use strict";

  var CONF_PATH = {{ json_module.dumps(configuration) }},
    conf_file = open(CONF_PATH, "r"),
    configuration = JSON.parse(conf_file.readAsString()),
    clientId,
    drone_dict = {},
    gwsocket_pid,
    gwsocket_r_pipe_fd,
    gwsocket_w_pipe_fd,
    handleWebSocketMessage,
    last_message_timestamp = 0,
    last_log_timestamp = 0,
    parent = Worker.parent,
    peer_dict = {},
    user_me = {
      //required to fly
      triggerParachute: triggerParachute,
      exit: exitWorker,
      getDroneDict: function () { return drone_dict; },
      getAltitudeAbs: getAltitude,
      getCurrentPosition: getPosition,
      getInitialAltitude: getInitialAltitude,
      gpsIsOk: gpsIsOk,
      getYaw: getYaw,
      getSpeed: getAirspeed,
      getClimbRate: getClimbRate,
      id: configuration.id,
      isLanding: isLanding,
      loiter: loiter,
      sendMsg: function (msg, id) {
        if (id === undefined) { id = -1; }
        setMessage(JSON.stringify({
          content: msg,
          timestamp: Date.now(),
          dest_id: id
        }));
      },
      setAirSpeed: setAirSpeed,
      setTargetCoordinates: setTargetCoordinates
    };
  conf_file.close();

  function exitWorker(exit_code) {
    if (user_me.hasOwnProperty("onWebSocketMessage")) {
      stopGwsocket();
    }
    parent.postMessage({type: "exited", exit: exit_code});
    parent.onmessage = null;
  }

  function readMessage(rd) {
    function read4() {
      var b1, b2, b3, b4;
      b1 = rd.getByte();
      b2 = rd.getByte();
      b3 = rd.getByte();
      b4 = rd.getByte();
      return (b1 << 24) | (b2 << 16) | (b3 << 8) | b4;
    }
    clientId = read4();
    var type = read4();
    var len = read4();
    var data = new ArrayBuffer(len);
    rd.read(data, 0, len);
    return {
      client: clientId,
      type:   type,
      data:   String.fromCharCode.apply(null, new Uint8Array(data)).trim()
    };
  }

  function writeMessage(wr, m) {
    function write4(v) {
      wr.putByte((v >> 24) & 0xFF);
      wr.putByte((v >> 16) & 0xFF);
      wr.putByte((v >> 8) & 0xFF);
      wr.putByte(v & 0xFF);
    }
    write4(m.client);
    write4(m.type);
    write4(m.data.byteLength);
    wr.write(m.data, 0, m.data.byteLength);
    wr.flush();
  }

  function runGwsocket(onMessage) {
    var gwsocket_w_pipe = pipe(),
      gwsocket_r_pipe = pipe();

    gwsocket_pid = exec([
        "gwsocket",
        "--port=" + configuration.websocketPort,
        "--addr=" + configuration.websocketIp,
        "--std",
        "--strict"
      ], {
        block:   false,
        usePath: false,
        file:    {{ json_module.dumps(gwsocket_bin) }},
        stdin:   gwsocket_w_pipe[0],
        stdout:  gwsocket_r_pipe[1]
      });

    gwsocket_w_pipe_fd = fdopen(gwsocket_w_pipe[1], "w");
    gwsocket_r_pipe_fd = fdopen(gwsocket_r_pipe[0], "r");

    handleWebSocketMessage = function () {
      var message = readMessage(gwsocket_r_pipe_fd).data;
      if (message.includes(configuration.websocketIp)) {
        return;
      }
      onMessage(message);
    };
    user_me.writeWebsocketMessage = function (message) {
      var buf = new ArrayBuffer(message.length);
      var bufView = new Uint8Array(buf);
      for (var i=0; i<message.length; i++) {
        bufView[i] = message.charCodeAt(i);
      }
      writeMessage(gwsocket_w_pipe_fd, {client: clientId, type: 1, data: buf});
    }
    setReadHandler(gwsocket_r_pipe[0], handleWebSocketMessage);
  }

  function stopGwsocket() {
    handleWebSocketMessage = null;
    close(gwsocket_w_pipe_fd);
    close(gwsocket_r_pipe_fd);
    kill(gwsocket_pid, SIGTERM);
    waitpid(gwsocket_pid, WNOHANG);
  }

  function loadUserScript(path) {
    var script_content = loadFile(path);
    if (script_content === null) {
      console.log("Failed to load user script " + path);
      exitWorker(1);
    }
    try {
      evalScript(
        "function execUserScript(from, me) {" + script_content + "};"
      );
    } catch (e) {
      console.log("Failed to evaluate user script", e);
      exitWorker(1);
    }
    execUserScript(null, user_me);

    if (user_me.hasOwnProperty("onWebSocketMessage")) {
      runGwsocket(user_me.onWebSocketMessage);
    }

    // Call the drone onStart function
    if (user_me.hasOwnProperty("onStart")) {
      user_me.onStart();
    }
  }

  function handleMainMessage(evt) {
    var type = evt.data.type, message, peer_id;

    switch (type) {

    case "initPubsub":
      initPubsub(configuration.numberOfDrone, configuration.numberOfSubscriber);
      for (peer_id = 0; peer_id < configuration.numberOfDrone + configuration.numberOfSubscriber; peer_id++) {
        peer_dict[peer_id] = new Drone(peer_id);
        peer_dict[peer_id].init(peer_id);
        if (peer_id < configuration.numberOfDrone) {
          drone_dict[peer_id] = peer_dict[peer_id];
        }
      }
      parent.postMessage({type: "initialized"});
      break;

    case "load":
      loadUserScript(evt.data.path);
      parent.postMessage({type: "loaded"});
      break;

    case "update":
      Object.entries(peer_dict).forEach(function ([id, peer]) {
        message = peer.message;
        if (user_me.id !== Number(id) && message.length > 0) {
          message = JSON.parse(message);
          if (message.timestamp != last_message_timestamp &&
              user_me.hasOwnProperty("onGetMsg") &&
              [-1, user_me.id].includes(message.dest_id)) {
            last_message_timestamp = message.timestamp;
            user_me.onGetMsg(message.content);
          }
        }
      });
      // Call the drone onStart function
      if (user_me.hasOwnProperty("onUpdate")) {
        user_me.onUpdate(evt.data.timestamp);
      }

      if (evt.data.timestamp - last_log_timestamp >= 1000) {
        updateLogAndProjection();
        last_log_timestamp = evt.data.timestamp; 
      }

      parent.postMessage({type: "updated"});
      break;

    case "exit":
      exitWorker(evt.data.code);
      break;

    default:
      throw new Error("Unsupported message type", type);
    };
  }

  parent.onmessage = function (evt) {
    try {
      handleMainMessage(evt);
    } catch (error) {
      // Catch all potential bug to exit the main process
      // if it occurs
      console.log(error);
      exitWorker(1);
    }
  };
}(Drone, SIGTERM, WNOHANG, Worker, close, console, evalScript, exec,
  fdopen, getAltitude, getInitialAltitude, gpsIsOk, getPosition, getYaw,
  initPubsub, isLanding, kill, loadFile, loiter, open, pipe, setAirSpeed,
  setMessage, setReadHandler, setTargetCoordinates, triggerParachute,
  updateLogAndProjection, waitpid));
