import { MainApi } from '../common/mainApi';
import { OSInfo } from '../common/preloadApi';
import { RendererApi } from '../common/rendererApi';

let project_dir: string | null = null;
export async function runAction(
  code: string,
  port: string,
  rapi: RendererApi,
  main: MainApi,
  os: OSInfo
): Promise<boolean> {
  if (project_dir == null) {
    project_dir = await main.makeTmpProjectDirectory();
    if (project_dir == null) {
      rapi.err('プロジェクトの作成に失敗');
      return false;
    }
  }

  const cppFile = `${project_dir}${os.separator}src${os.separator}main.cpp`;

  if (!(await main.saveTextFile(cppFile, code))) {
    rapi.err('ファイルの保存に失敗');
    return false;
  }

  if (!(await main.build(project_dir))) {
    rapi.err('プロジェクトのビルドに失敗');
    return false;
  }

  if (!(await main.upload(project_dir, port))) {
    rapi.err('書き込みに失敗');
    return false;
  }

  return true;
}

interface PortData {
  port: SerialPort;
  open: boolean;
  name: string;
  productId: number | undefined;
  vendorId: number | undefined;
}

let port: PortData | null = null;
/**
 * シリアルポートを開くための要求をブラウザに出します。
 * セキュリティ制限により、ユーザーイベントによるハンドラから
 * 直接起動する必要があります。
 * await に注意。
 */
export async function requestPort(
  rapi: RendererApi,
  portName: string,
  productId: number | undefined,
  vendorId: number | undefined
): Promise<boolean> {
  if (productId === undefined && vendorId === undefined) {
    rapi.err('ポートへの接続失敗');
    return false;
  }

  if (port) {
    if ((port.productId && port.productId == productId) || (port.vendorId && port.vendorId == vendorId)) return true;

    if (port.open) closePort();
  }

  try {
    const p = await navigator.serial.requestPort({
      filters: [
        {
          usbProductId: productId,
          usbVendorId: vendorId,
        },
      ],
    });
    port = {
      port: p,
      name: portName,
      productId: productId,
      vendorId: vendorId,
      open: false,
    };
    return true;
  } catch (e) {
    rapi.err('ポートへの接続失敗');
    rapi.err((e as any).toString()); //eslint-disable-line
    console.error(e);
    return false;
  }
}

export async function openPort(rapi: RendererApi): Promise<void> {
  if (port == null) {
    rapi.err('ポート要求に失敗');
    return;
  }
  if (port.open) {
    return;
  }
  const p = port;

  try {
    await p.port.open({ baudRate: 921600 });
    p.open = true;
    rapi.log('ポートへの接続成功');
    await readPort(port, rapi);
  } catch (e) {
    rapi.err('ポートへの接続失敗');
    rapi.err((e as any).toString()); //eslint-disable-line
    console.error(e);
  }
  if (p == port) port = null;
}

export async function closePort(): Promise<void> {
  const p = port;
  port = null;
  if (!p || !p.open) return;
  console.log('ポート切断');
  p.open = false;
  if (p.port.readable) {
    await p.port.readable.getReader().cancel();
  }
  await p.port.close();
}

type SerialDataType = 'IMG' | 'MSG' | 'BLK';

class SerialReader {
  private _buffer: Uint8Array;
  private _length: number;
  private _next_check_index: number;
  private _imgMode: boolean;
  private _done: boolean;
  private reader: ReadableStreamDefaultReader<Uint8Array>;

  constructor(reader: ReadableStreamDefaultReader<Uint8Array>) {
    this._buffer = new Uint8Array(256);
    this._length = 0;
    this._next_check_index = 0;
    this._imgMode = false;
    this._done = false;
    this.reader = reader;
  }

  async next(): Promise<[string, SerialDataType] | null> {
    for (;;) {
      if (this._imgMode) return await this.nextImg();

      const ret = await this.nextLine();
      if (ret == 'CONTINUE') continue;
      return ret;
    }
  }

  private async nextImg(): Promise<[string, 'IMG'] | null> {
    let s = this.readImage();
    while (s == null) {
      if (this._done) return null;
      await this.read();
      s = this.readImage();
    }
    this._imgMode = false;
    return [s, 'IMG'];
  }

  private async nextLine(): Promise<[string, 'MSG' | 'BLK'] | 'CONTINUE' | null> {
    let s = this.readLine();
    while (s == null) {
      if (this._done) return null;
      await this.read();
      s = this.readLine();
    }
    const l = s.trim();
    if (l.startsWith('IMG')) {
      this._imgMode = true;
    } else if (l.startsWith('MSG ')) {
      return [l.substr('MSG '.length), 'MSG'];
    } else if (l.startsWith('BLK ')) {
      return [l.substr('BLK '.length), 'BLK'];
    }
    return 'CONTINUE';
  }

  private async read(): Promise<void> {
    if (this._done) return;
    const { value, done } = await this.reader.read();
    if (done || value === undefined) {
      this._done = true;
      return;
    }
    const len = this._length + value.length;
    if (this._buffer.length < len) {
      const newlen = (this._buffer.length * 3) >> 1;
      this.ensureCapacity(newlen < len ? len : newlen);
    }
    this._buffer.set(value, this._length);
    this._length = len;
  }

  private remove(length: number) {
    if (this._length <= length) {
      this._length = 0;
      this._next_check_index = 0;
      return;
    }
    this._buffer.copyWithin(0, length, this._length);
    this._length -= length;
    this._next_check_index -= length;
    if (this._next_check_index < 0) this._next_check_index = 0;
  }

  private ensureCapacity(length: number) {
    if (this._buffer.length >= length) {
      return;
    }
    const newBuffer = new Uint8Array(length);
    newBuffer.set(this._buffer);
    this._buffer = newBuffer;
  }

  private subarray(from: number, end: number): Uint8Array {
    return new Uint8Array(this._buffer.buffer, from, end);
  }

  private readLine(): string | null {
    const len = this._length;
    const buf = this._buffer;
    for (let i = this._next_check_index; i < len; i++) {
      if (buf[i] == 0x0a) {
        const line = new TextDecoder().decode(this.subarray(0, i));
        this.remove(i + 1);
        return line;
      }
    }
    this._next_check_index = len;
    return null;
  }

  private readImage(): string | null {
    const len = this._length;
    const buf = this._buffer;
    let before = buf[this._next_check_index];
    for (let i = this._next_check_index + 1; i < len; i++) {
      const current = buf[i];
      if (before == 0xff && current == 0xd9) {
        const len = i + 1;
        // Base64に変換
        const data = btoa(String.fromCharCode(...this.subarray(0, len)));
        this.remove(len);
        return data;
      }
      before = current;
    }
    this._next_check_index = len;
    return null;
  }
}

async function readPort(p: PortData, rapi: RendererApi): Promise<void> {
  if (!p.port.readable) return;
  const r = p.port.readable.getReader();
  const reader = new SerialReader(r);

  let n = await reader.next();
  while (n != null) {
    switch (n[1]) {
      case 'MSG':
        rapi.writeLine(n[0].replace('\\\\', '\\@@').replace('\\n', '\n').replace('\\@@', '\\'));
        break;
      case 'IMG':
        rapi.writeImage(n[0]);
        break;
      case 'BLK':
        rapi.highlight(n[0], true);
        break;
    }
    n = await reader.next();
  }
  r.releaseLock();
}
