import { ConnectionInfo, NiimbotAbstractClient } from ".";
import { ConnectResult, NiimbotPacket } from "../packets";
import { Utils } from "../utils";
import { ConnectEvent, DisconnectEvent, RawPacketSentEvent } from "./events";

/** WIP. Uses serial communication, Works worse than NiimbotBluetoothClient at the moment, events are not firing */
export class NiimbotSerialClient extends NiimbotAbstractClient {
  private port?: SerialPort = undefined;
  private writer?: WritableStreamDefaultWriter<Uint8Array> = undefined;

  public async connect(): Promise<ConnectionInfo> {
    this.disconnect();

    const _port: SerialPort = await navigator.serial.requestPort();

    _port.addEventListener("disconnect", () => {
      this.port = undefined;
      this.dispatchTypedEvent("disconnect", new DisconnectEvent());
    });

    await _port.open({ baudRate: 115200 });

    if (_port.writable === null) {
      throw new Error("Port is not writable");
    }

    this.port = _port;
    this.writer = _port.writable.getWriter();
    const info = _port.getInfo();

    try {
      await this.initialNegotiate();
      await this.fetchPrinterInfo();
    } catch (e) {
      console.error(e);
    }

    const result: ConnectionInfo = {
      deviceName: `Serial (VID:${info.usbVendorId?.toString(16)} PID:${info.usbProductId?.toString(16)})`,
      result: this.info.connectResult ?? ConnectResult.FirmwareErrors
    };

    this.dispatchTypedEvent("connect", new ConnectEvent(result));
    return result;
  }

  public async disconnect() {
    if (this.writer !== undefined) {
      this.writer.releaseLock();
    }

    if (this.port !== undefined) {
      this.port.close();
      this.dispatchTypedEvent("disconnect", new DisconnectEvent());
    }

    this.port = undefined;
    this.writer = undefined;
  }

  public isConnected(): boolean {
    throw this.port !== undefined && this.writer !== undefined;
  }

  public async sendPacketWaitResponse(packet: NiimbotPacket, timeoutMs: number = 1000): Promise<NiimbotPacket> {
    if (this.port === undefined) {
      throw new Error("Port is closed");
    }

    this.sendPacket(packet);

    if (this.port.readable === null) {
      throw new Error("Port is not readable");
    }

    let data = new Uint8Array();
    let p: NiimbotPacket | undefined = undefined;

    const reader = this.port.readable.getReader();

    // todo: rewrite, no timeout!
    try {
      while (true) {
        const { value, done } = await reader.read();
        if (done) {
          console.log("done");
          break;
        }
        const newArr = new Uint8Array(data.length + value.length);
        newArr.set(data);
        newArr.set(value, data.length);
        data = newArr;

        try {
          const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
          p = NiimbotPacket.fromBytes(dv);
          break;
        } catch (e) {
          console.log("skipping");
        }
        // Do something with |value|...
      }
    } catch (error) {
      console.error(error);
    } finally {
      reader.releaseLock();
    }

    console.log("end");

    if (p === undefined) {
      throw new Error("err");
    }
    // const reader: ReadableStreamDefaultReader<Uint8Array> = this.port.readable.getReader();

    // const timer: NodeJS.Timeout = setTimeout(() => {
    //   reader.releaseLock();
    // }, timeoutMs);

    // const result: ReadableStreamReadResult<Uint8Array> = await reader.read();

    // clearTimeout(timer);
    // reader.releaseLock();

    // const arr: Uint8Array = result.value!;
    // console.log(Utils.bufToHex(arr));
    // const dv = new DataView(arr.buffer, arr.byteOffset, arr.byteLength);
    // const inPacket = NiimbotPacket.fromBytes(dv);

    // this.port.readable.pipeTo(new WritableStream({
    //   write(chunk) {
    //     console.log("Chunk received", chunk);
    //   },
    //   close() {
    //     console.log("All data successfully read!");
    //   },
    //   abort(e) {
    //     console.error("Something went wrong!", e);
    //   }
    // }));

    return p;
  }

  public async sendRaw(data: Uint8Array, force?: boolean) {
    if (this.writer === undefined) {
      throw new Error("Port is not writable");
    }

    // this.writer.releaseLock();

    this.dispatchTypedEvent("rawpacketsent", new RawPacketSentEvent(data));
    await Utils.sleep(2); // fixme maybe
  }
}