import {Capture, Chip, Chip_Radio, Device as ProtoDevice,} from './netsim/model.js';

// URL for netsim
const DEVICES_URL = './v1/devices';
const CAPTURES_URL = './v1/captures';

/**
 * Interface for a method in notifying the subscribed observers.
 * Subscribed observers must implement this interface.
 */
export interface Notifiable {
  onNotify(data: {}): void;
}

/**
 * Modularization of Device.
 * Contains getters and setters for properties in Device interface.
 */
export class Device {
  device: ProtoDevice;

  constructor(device: ProtoDevice) {
    this.device = device;
  }

  get name(): string {
    return this.device.name;
  }

  set name(value: string) {
    this.device.name = value;
  }

  get position(): {x: number; y: number; z: number} {
    const result = {x: 0, y: 0, z: 0};
    if ('position' in this.device && this.device.position &&
        typeof this.device.position === 'object') {
      if ('x' in this.device.position &&
          typeof this.device.position.x === 'number') {
        result.x = this.device.position.x;
      }
      if ('y' in this.device.position &&
          typeof this.device.position.y === 'number') {
        result.y = this.device.position.y;
      }
      if ('z' in this.device.position &&
          typeof this.device.position.z === 'number') {
        result.z = this.device.position.z;
      }
    }
    return result;
  }

  set position(pos: {x: number; y: number; z: number}) {
    this.device.position = pos;
  }

  get orientation(): {yaw: number; pitch: number; roll: number} {
    const result = {yaw: 0, pitch: 0, roll: 0};
    if ('orientation' in this.device && this.device.orientation &&
        typeof this.device.orientation === 'object') {
      if ('yaw' in this.device.orientation &&
          typeof this.device.orientation.yaw === 'number') {
        result.yaw = this.device.orientation.yaw;
      }
      if ('pitch' in this.device.orientation &&
          typeof this.device.orientation.pitch === 'number') {
        result.pitch = this.device.orientation.pitch;
      }
      if ('roll' in this.device.orientation &&
          typeof this.device.orientation.roll === 'number') {
        result.roll = this.device.orientation.roll;
      }
    }
    return result;
  }

  set orientation(ori: {yaw: number; pitch: number; roll: number}) {
    this.device.orientation = ori;
  }

  // TODO modularize getters and setters for Chip Interface
  get chips(): Chip[] {
    return this.device.chips ?? [];
  }

  // TODO modularize getters and setters for Chip Interface
  set chips(value: Chip[]) {
    this.device.chips = value;
  }

  get visible(): boolean {
    return Boolean(this.device.visible);
  }

  set visible(value: boolean) {
    this.device.visible = value;
  }

  toggleChipState(radio: Chip_Radio) {
    radio.state = !radio.state;
  }

  toggleCapture(device: Device, chip: Chip) {
    if ('capture' in chip && chip.capture) {
      chip.capture = !chip.capture;
      simulationState.patchDevice({
        device: {
          name: device.name,
          chips: device.chips,
        },
      });
    }
  }
}

/**
 * The most recent state of the simulation.
 * Subscribed observers must refer to this info and patch accordingly.
 */
export interface SimulationInfo {
  devices: Device[];
  captures: Capture[];
  selectedId: string;
  dimension: {x: number; y: number; z: number};
  lastModified: string;
}

interface Observable {
  registerObserver(elem: Notifiable): void;
  removeObserver(elem: Notifiable): void;
}

class SimulationState implements Observable {
  private observers: Notifiable[] = [];

  private simulationInfo: SimulationInfo = {
    devices: [],
    captures: [],
    selectedId: '',
    dimension: {x: 10, y: 10, z: 0},
    lastModified: '',
  };

  constructor() {
    // initial GET
    this.invokeGetDevice();
    this.invokeListCaptures();
  }

  async invokeGetDevice() {
    await fetch(DEVICES_URL, {
      method: 'GET',
    })
        .then(response => response.json())
        .then(data => {
          this.fetchDevice(data.devices);
          this.updateLastModified(data.lastModified);
        })
        .catch(error => {
          // eslint-disable-next-line
          console.log('Cannot connect to netsim web server', error);
        });
  }

  async invokeListCaptures() {
    await fetch(CAPTURES_URL, {
      method: 'GET',
    })
        .then(response => response.json())
        .then(data => {
          this.simulationInfo.captures = data.captures;
          this.notifyObservers();
        })
        .catch(error => {
          console.log('Cannot connect to netsim web server', error);
        });
  }

  fetchDevice(devices?: ProtoDevice[]) {
    this.simulationInfo.devices = [];
    if (devices) {
      this.simulationInfo.devices = devices.map(device => new Device(device));
    }
    this.notifyObservers();
  }

  getLastModified() {
    return this.simulationInfo.lastModified;
  }

  updateLastModified(timestamp: string) {
    this.simulationInfo.lastModified = timestamp;
  }

  patchSelected(id: string) {
    this.simulationInfo.selectedId = id;
    this.notifyObservers();
  }

  handleDrop(id: string, x: number, y: number) {
    this.simulationInfo.selectedId = id;
    for (const device of this.simulationInfo.devices) {
      if (id === device.name) {
        device.position = {x, y, z: device.position.z};
        this.patchDevice({
          device: {
            name: device.name,
            position: device.position,
          },
        });
        break;
      }
    }
  }

  patchCapture(id: string, state: string) {
    fetch(CAPTURES_URL + '/' + id, {
      method: 'PATCH',
      headers: {
        'Content-Type': 'text/plain',
        'Content-Length': state.length.toString(),
      },
      body: state,
    });
    this.notifyObservers();
  }

  patchDevice(obj: object) {
    const jsonBody = JSON.stringify(obj);
    fetch(DEVICES_URL, {
      method: 'PATCH',
      headers: {
        'Content-Type': 'application/json',
        'Content-Length': jsonBody.length.toString(),
      },
      body: jsonBody,
    })
        .then(response => response.json())
        .catch(error => {
          // eslint-disable-next-line
          console.error('Error:', error);
        });
    this.notifyObservers();
  }

  registerObserver(elem: Notifiable) {
    this.observers.push(elem);
    elem.onNotify(this.simulationInfo);
  }

  removeObserver(elem: Notifiable) {
    const index = this.observers.indexOf(elem);
    this.observers.splice(index, 1);
  }

  notifyObservers() {
    for (const observer of this.observers) {
      observer.onNotify(this.simulationInfo);
    }
  }

  getDeviceList() {
    return this.simulationInfo.devices;
  }
}

/** Subscribed observers must register itself to the simulationState */
export const simulationState = new SimulationState();

async function subscribeCaptures() {
  const delay = (ms: number) => new Promise(res => setTimeout(res, ms));
  while (true) {
    await simulationState.invokeListCaptures();
    await simulationState.invokeGetDevice();
    await delay(1000);
  }
}

async function subscribeDevices() {
  await simulationState.invokeGetDevice();
  while (true) {
    const jsonBody = JSON.stringify({
      lastModified: simulationState.getLastModified(),
    });
    await fetch(DEVICES_URL, {
      method: 'SUBSCRIBE',
      headers: {
        'Content-Type': 'application/json',
        'Content-Length': jsonBody.length.toString(),
      },
      body: jsonBody,
    })
        .then(response => response.json())
        .then(data => {
          simulationState.fetchDevice(data.devices);
          simulationState.updateLastModified(data.lastModified);
        })
        .catch(error => {
          // eslint-disable-next-line
          console.log('Cannot connect to netsim web server', error);
        });
  }
}

subscribeCaptures();
subscribeDevices();
