import express from "express";
import cors from "cors";
import Kinematic from "./kinematic.js";
import { WebSocketServer } from "ws";

const app = express();
const kinematic = new Kinematic();
const wss = new WebSocketServer({ port: 2096 });

app.use(cors());
app.use(express.json());

const port = 3000;
const subscriptions = {};

const randomFloatFromInterval = (min, max) =>
  Math.floor((Math.random() * (max - min + 1) + min) * 100) / 100;

const radToDeg = (val) => val * (180 / Math.PI);
const degToRad = (val) => val * (Math.PI / 180);

const lerp = (start, end, amt) => {
  return (1 - amt) * start + amt * end;
};

function createNewClientState() {
  return {
    model: JSON.parse(JSON.stringify(model)),
    settings: JSON.parse(JSON.stringify(settings)),
    logs: JSON.parse(JSON.stringify(logs)),
    subscriptions: {},
    mode: "idle",
    controller: {
      stop: 0,
      lx: 0,
      ly: 0,
      rx: 0,
      ry: 0,
      h: 70,
      s: 0,
    },
  };
}

const unsubscribeClientFromCategory = (ws, category) => {
  if (!subscriptions[category]) return;
  subscriptions[category].delete(ws);
  if (subscriptions[category].size === 0) {
    delete subscriptions[category].size;
  }
};

const sendUpdateToSubscribers = (category, data) => {
  if (subscriptions[category]) {
    const message = JSON.stringify(data);
    for (const client of subscriptions[category]) {
      client.send(message);
    }
  }
};

if (!Array.prototype.last) {
  Array.prototype.last = function () {
    return this[this.length - 1];
  };
}

const model = {
  battery: {
    voltage: randomFloatFromInterval(7.6, 8.2),
    ampere: randomFloatFromInterval(0.2, 3),
    power_button: false,
  },
  servos: {
    angles: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    dir: [-1, -1, -1, 1, -1, -1, -1, -1, -1, 1, -1, -1],
    on: true,
  },
  mpu: {
    x: 0,
    y: 0,
    z: 0,
    heading: 240,
    temperature: 21,
  },
  display: [[]],
  distance_sensors: {
    left: 22,
    right: 23,
  },
  appTime: 1123321,
  connectivity: {
    ssid: "best network",
    ip: "192.168.0.118",
    mDNS: "leika.local",
    rssi: 100,
  },
  running: true,
  mode: "stand",
  rotation: [0, 0, 0],
  position: [0, 0, 0],
};

const settings = {
  useMetric: true,
  name: "Leika",
  ssid: "Rune private network",
  pass: "12345678",
  ap: "Leika",
  apPass: "12345678",
  apChannel: 1,
};

const logs = [
  "[2023-02-05 10:00:00] [verbose] Booting up",
  "[2023-02-05 10:00:10] [verbose] Starting webserver",
  "[2023-02-05 10:00:20] [verbose] Loading setting",
  "[2023-02-05 10:00:30] [verbose] Connected to Rune private network",
];

const system = {
  HeapSize: 400000,
  HeapFree: 0,
  HeapMin: 0,
  DmaHeapSize: 0,
  DmaHeapFree: 0,
  DmaHeapMin: 0,
  PsramSize: 400000,
  PsramFree: 0,
  PsramMin: 0,
  ChipModel: 0,
  ChipRevision: 0,
  ChipCores: 2,
  CpuFreqMHz: 80,
  SketchSize: 0,
  FreeSketchSpace: 10200,
  FlashChipSize: 0,
  CpuUsed: 0,
  CpuUsedCore0: 0,
  CpuUsedCore1: 0,
  arduinoVersion: "3.2.1",
};

const updateBattery = () => {
  model.battery.voltage = randomFloatFromInterval(7.6, 8.2);
  model.battery.ampere = randomFloatFromInterval(0.2, 3);
  return model.battery;
};

const updateMpu = () => {
  model.mpu.x = randomFloatFromInterval(0, 1);
  model.mpu.y = randomFloatFromInterval(0, 1);
  model.mpu.z = randomFloatFromInterval(0, 1);
  model.mpu.temperature = randomFloatFromInterval(20, 22);
  return model.mpu;
};

const updateDistances = () => {
  model.distance_sensors = {
    left: randomFloatFromInterval(10, 220),
    right: randomFloatFromInterval(10, 220),
  };
  return model.distance_sensors;
};

const updateDistance = (position) => {
  model.distance_sensors[position] = randomFloatFromInterval(10, 220);
  return model.distance_sensors[position];
};

const updateSystem = () => {
  system.CpuUsedCore0 = randomFloatFromInterval(0, 100);
  system.CpuUsedCore1 = randomFloatFromInterval(0, 100);
  system.CpuUsed =
    Math.floor((system.CpuUsedCore0 + system.CpuUsedCore1) / 0.02) / 100;
  system.HeapFree = randomFloatFromInterval(0, 20000);
  system.HeapMin = randomFloatFromInterval(0, 20000);
  return system;
};

const updateBodyState = (model, angles, position) => {
  const Lp = [
    [100, -100, 100, 1],
    [100, -100, -100, 1],
    [-100, -100, 100, 1],
    [-100, -100, -100, 1],
  ];

  model.servos.angles = kinematic
    .calcIK(
      Lp,
      angles.map((x) => degToRad(x)),
      position
    )
    .flat()
    .map((x, i) => radToDeg(x * model.servos.dir[i]));
  return model.servos.angles;
};

const updateAngle = (id, angle) => {
  model.servos.angles[id] = angle;
  return model.servos.angles;
};

const updateAngles = (angles) => {
  model.servos.angles = angles;
  return model.servos.angles;
};

const bufferToController = (buffer) => {
  return {
    stop: buffer[0],
    lx: buffer[1],
    ly: buffer[2],
    rx: buffer[3],
    ry: buffer[4],
    h: buffer[5],
    s: buffer[6],
  };
};

const unpackMessageBuffer = (data) => {
  return {
    angles: [0, data.rx / 4, data.ry / 4],
    position: [data.ly / 2, 70, data.lx / 2],
  };
};

const rest_stance = {
  rotation: [0, 0, 0],
  position: [0, 10, 0],
};

const idle = () => {};

const rest = (client) => {
  for (let i = 0; i < 3; i++) {
    client.clientState.model.position[i] = lerp(
      client.clientState.model.position[i],
      rest_stance.position[i],
      0.01
    );
    client.clientState.model.rotation[i] = lerp(
      client.clientState.model.rotation[i],
      rest_stance.rotation[i],
      0.01
    );
  }
  client.send(
    JSON.stringify({
      type: "angles",
      data: updateBodyState(
        client.clientState.model,
        client.clientState.model.rotation,
        client.clientState.model.position
      ),
    })
  );
};

const stand = (client) => {
  if (!client.clientState.model.running) return;
  const data = unpackMessageBuffer(client.clientState.controller);
  client.send(
    JSON.stringify({
      type: "angles",
      data: updateBodyState(
        client.clientState.model,
        data.angles,
        data.position
      ),
    })
  );
};

// https://www.hindawi.com/journals/cin/2016/9853070/

const step = (model, controller, tick) => {
  const y1 = -100 * Math.sin(-0.05 * tick) - 150;
  const y2 = -100 * Math.sin(-0.05 * tick + Math.PI) - 150;
  const x1 = Math.abs((tick % 120) - 60) - 60;
  const Lp = [
    // -50  is minimum
    [100, y1, 100, 1],
    [100, y2, -100, 1],
    [-100, y2, 100, 1],
    [-100, y1, -100, 1],
  ];

  model.servos.angles = kinematic
    .calcIK(
      Lp,
      model.rotation.map((x) => degToRad(x)),
      model.position
    )
    .flat()
    .map((x, i) => radToDeg(x * model.servos.dir[i]));
  return model.servos.angles;
};

const walk = (client) => {
  const angles = step(
    client.clientState.model,
    client.clientState.controller,
    client.tick
  );
  client.send(JSON.stringify({ type: "angles", data: angles }));
};

const start_dynamics = (client) => {
  client.tick = 0;
  client.clientState.mode = "idle";
  client.clientState.next_mode = "walk";
  const modes = { rest, idle, stand, walk };
  client.id = setInterval(() => {
    client.tick += 1;

    if (client.clientState.mode !== client.clientState.next_mode) {
      // Transition
      client.clientState.mode = client.clientState.next_mode;
    } else {
      modes[client.clientState.mode](client);
    }
  }, 10);
};

const handelController = (ws, buffer) => {
  const controllerData = bufferToController(new Int8Array(buffer));
  ws.clientState.controller = controllerData;
  if (controllerData.stop) {
    ws.clientState.model.running = false;
    ws.clientState.logs.push("[2024-02-05 19:10:00] [Warning] STOPPING SERVOS");
    ws.send(JSON.stringify({ type: "log", data: ws.clientState.logs.last() }));
    return;
  }
};

const handleBufferMessage = (ws, buffer) => {
  if (buffer.length === 6) {
    handelController(ws, buffer);
  }
};

const handleJsonMessage = (ws, data) => {
  switch (data.type) {
    case "mode":
      ws.clientState.next_mode = data.data;
      //   ws.send({ type: "battery", data: JSON.stringify(updateBattery()) });
      break;
    case "sensor/battery":
      ws.send({ type: "battery", data: JSON.stringify(updateBattery()) });
      break;
    case "sensor/mpu":
      ws.send({ type: "battery", data: JSON.stringify(updateMpu()) });
      break;
    case "sensor/distances":
      ws.send(JSON.stringify(updateDistances()));
      break;
    case "sensor/distance":
      ws.send(JSON.stringify({ distance: updateDistance(data.position) }));
      break;
    case "kinematic/angle":
      if (data.angle && data.id) {
        ws.clientState.model.servos.angles[data.id] = data.angle;
        ws.send(
          JSON.stringify({
            type: "angles",
            data: ws.clientState.model.servos.angles,
          })
        );
      } else {
        ws.send(JSON.stringify(updateAngle(data.id, data.angle)));
      }
      break;
    case "kinematic/angles":
      if (data.angles) {
        ws.clientState.model.servos.angles = data.angles;
        ws.send(
          JSON.stringify({
            type: "angles",
            data: ws.clientState.model.servos.angles,
          })
        );
      } else {
        ws.send(JSON.stringify(updateAngles(data.angles)));
      }
      break;
    case "kinematic/bodystate":
      if (data.angles) {
        ws.send(
          JSON.stringify({
            type: "angles",
            data: updateBodyState(
              ws.clientState.model,
              data.angles,
              data.position
            ),
          })
        );
      } else {
        ws.send(JSON.stringify({ angles: model.servos.angles }));
      }
      break;
    case "system/logs":
      ws.send(JSON.stringify({ type: "logs", data: ws.clientState.logs }));
      break;
    case "system/info":
      ws.send(JSON.stringify({ type: "info", data: updateSystem() }));
      break;
    case "system/settings":
      if (data.settings) {
        Object.entries(data.settings).forEach(
          ([key, value]) => (ws.clientState.settings[key] = value)
        );
        ws.send(JSON.stringify(ws.clientState.settings));
      } else {
        ws.send(
          JSON.stringify({
            type: "settings",
            settings: ws.clientState.settings,
          })
        );
      }
      break;
    case "system/stop":
      ws.clientState.model.running = false;
      ws.clientState.logs.push(
        "[2024-02-05 19:10:00] [Warning] STOPPING SERVOS"
      );
      ws.send(
        JSON.stringify({ type: "log", data: ws.clientState.logs.last() })
      );
      break;
    default:
      ws.send(JSON.stringify({ error: "Unknown request type" }));
  }
};

wss.on("connection", (ws) => {
  const clientState = createNewClientState();
  ws.clientState = clientState;
  start_dynamics(ws);
  ws.on("error", console.error);

  ws.on("message", (message) => {
    let data = message;
    try {
      data = JSON.parse(message);
      handleJsonMessage(ws, data);
    } catch (error) {
      handleBufferMessage(ws, message);
    }
  });

  ws.on("close", () => {
    for (const category in subscriptions) {
      unsubscribeClientFromCategory(ws, category);
    }
  });
});

app.get("/sensor/battery", (req, res) => res.send(updateBattery()));
app.get("/sensor/mpu", (req, res) => res.send(updateMpu()));
app.get("/sensor/distances", (req, res) => res.send(updateDistances()));
app.get("/sensor/distance/:position", (req, res) =>
  res.send({ distance: updateDistance(req.params.position) })
);

// ----------------------------------------------------------- //

app.post("/kinematic/angle/:id", (req, res) =>
  res.send(updateAngle(req.params.id, req.body.angle))
);
app.post("/kinematic/angles/", (req, res) =>
  res.send(updateAngles(req.body.angles))
);
app.get("/kinematic/bodystate", (req, res) => res.send(model.servos.angles));
app.post("/kinematic/bodystate", (req, res) => {
  sendUpdateToSubscribers("angles", model.servos.angles);
  res.send(updateBodyState(model, req.body.angles, req.body.position));
});

// ----------------------------------------------------------- //

app.get("/system/log", (req, res) => res.send(logs));
app.get("/system/info", (req, res) => res.send(updateSystem()));
app.get("/system/settings", (req, res) => res.send(settings));
app.post("/system/settings", (req, res) => {
  Object.entries(req.body).forEach((x) => (settings[x[0]] = x[1]));
  res.send(settings);
});
app.post("/system/stop", (req, res) => {
  model.running = false;
  model.res.send(settings);
});
app.listen(port, () => console.log(`Open at http://localhost:${port}`));
