const demoCode = `# Highlighting Demo
# This is a comment

# Hover a define id anywhere to see it's definition
define a_def 10

# Hover HASH("String")'s to see computed crc32
#     hover here    vvvvvvvvvvvvvvvv
define a_hash HASH("This is a String")

# hover over an alias anywhere in the code
# to see it's definition
alias a_var r0
alias a_device d0

# instructions have Auto Completion,
# numeric logic types are identified on hover
s db 12 0
#    ^^
# hover here

# Enums and their values are Known, Hover them!
#        vvvvvvvvvvvvvvvvvv
move r2 LogicType.Temperature
push r2

# same with constants
#       vvvv
move r3 pinf

# Labels are known
main:
l r1 dr15 RatioWater
move r2 100000.001
push r2

# Hover Hash Strings of Known prefab names
# to get their documentation
#             vvvvvvvvvvvvvvv
move r0 HASH("AccessCardBlack")
push r0
beqzal r1 test

# -2045627372 is the crc32 hash of a SolarPanel,
# hover it to see the documentation!
#        vvvvvvvvvv
move r1 -2045627372
jal test
move r1 $FF
push r1
beqzal 0 test
move r1 %1000
push r1
yield
j main

test:
add r15 r15 1
j ra

`;

import type { ICError } from "ic10emu_wasm";

export class Session extends EventTarget {
  _programs: Map<number, string>;
  _errors: Map<number, ICError[]>;
  _activeIC: number;
  _activeLines: Map<number, number>;
  _activeLine: number;
  _save_timeout?: ReturnType<typeof setTimeout>;
  constructor() {
    super();
    this._programs = new Map();
    this._errors = new Map();
    this._save_timeout = undefined;
    this._activeIC = 1;
    this._activeLines = new Map();
    this.loadFromFragment();

    const that = this;
    window.addEventListener("hashchange", (_event) => {
      that.loadFromFragment();
    });
  }

  get programs() {
    return this._programs;
  }

  set programs(programs) {
    this._programs = new Map([...programs]);
    this._fireOnLoad();
  }

  get activeIC() {
    return this._activeIC;
  }

  set activeIC(val: number) {
    this._activeIC = val;
    this.dispatchEvent(
      new CustomEvent("session-active-ic", { detail: this.activeIC }),
    );
  }

  onActiveIc(callback: EventListenerOrEventListenerObject) {
    this.addEventListener("session-active-ic", callback);
  }

  get errors() {
    return this._errors;
  }

  getActiveLine(id: number) {
    return this._activeLines.get(id);
  }

  setActiveLine(id: number, line: number) {
    const last = this._activeLines.get(id);
    if (last !== line) {
      this._activeLines.set(id, line);
      this._fireOnActiveLine(id);
    }
  }

  set activeLine(line: number) {
    this._activeLine = line;
  }

  setProgramCode(id: number, code: string) {
    this._programs.set(id, code);
    this.save();
  }

  setProgramErrors(id: number, errors: ICError[]) {
    this._errors.set(id, errors);
    this._fireOnErrors([id]);
  }

  _fireOnErrors(ids: number[]) {
    this.dispatchEvent(
      new CustomEvent("session-errors", {
        detail: ids,
      }),
    );
  }

  onErrors(callback: EventListenerOrEventListenerObject) {
    this.addEventListener("session-errors", callback);
  }

  onLoad(callback: EventListenerOrEventListenerObject) {
    this.addEventListener("session-load", callback);
  }

  _fireOnLoad() {
    this.dispatchEvent(
      new CustomEvent("session-load", {
        detail: this,
      }),
    );
  }

  onActiveLine(callback: EventListenerOrEventListenerObject) {
    this.addEventListener("active-line", callback);
  }

  _fireOnActiveLine(id: number) {
    this.dispatchEvent(
      new CustomEvent("active-line", {
        detail: id,
      }),
    );
  }

  save() {
    if (this._save_timeout) clearTimeout(this._save_timeout);
    this._save_timeout = setTimeout(() => {
      this.saveToFragment();
      if (window.App!.vm) {
        window.App!.vm.updateCode();
      }
      this._save_timeout = undefined;
    }, 1000);
  }

  async saveToFragment() {
    const toSave = { programs: Array.from(this._programs) };
    const bytes = new TextEncoder().encode(JSON.stringify(toSave));
    try {
      const c_bytes = await compress(bytes);
      const fragment = base64url_encode(c_bytes);
      window.history.replaceState(null, "", `#${fragment}`);
    } catch (e) {
      console.log("Error compressing content fragment:", e);
      return;
    }
  }

  async loadFromFragment() {
    const fragment = window.location.hash.slice(1);
    if (fragment === "demo") {
      this._programs = new Map([[1, demoCode]]);
      this._fireOnLoad();
      return;
    }
    if (fragment.length > 0) {
      const c_bytes = base64url_decode(fragment);
      const bytes = await decompressFragment(c_bytes);
      if (bytes !== null) {
        const txt = new TextDecoder().decode(bytes);
        const data = getJson(txt);
        if (data === null) {
          // backwards compatible
          this._programs = new Map([[1, txt]]);
          this, this._fireOnLoad();
          return;
        }
        try {
          this._programs = new Map(data.programs);
          this._fireOnLoad();
          return;
        } catch (e) {
          console.log("Bad session data:", e);
        }
      }
    }
  }
}
async function decompressFragment(c_bytes: ArrayBuffer) {
  try {
    const bytes = await decompress(c_bytes);
    return bytes;
  } catch (e) {
    console.log("Error decompressing content fragment:", e);
    return null;
  }
}

function getJson(value: any) {
  try {
    return JSON.parse(value);
  } catch (_) {
    return null;
  }
}

async function* streamAsyncIterator(stream: ReadableStream) {
  // Get a lock on the stream
  const reader = stream.getReader();

  try {
    while (true) {
      // Read from the stream
      const { done, value } = await reader.read();
      if (done) return;
      yield value;
    }
  } finally {
    reader.releaseLock();
  }
}

function base64url_encode(buffer: ArrayBuffer) {
  return btoa(
    Array.from(new Uint8Array(buffer), (b) => String.fromCharCode(b)).join(""),
  )
    .replace(/\+/g, "-")
    .replace(/\//g, "_")
    .replace(/=+$/, "");
}

function base64url_decode(value: string): ArrayBuffer {
  const m = value.length % 4;
  return Uint8Array.from(
    atob(
      value
        .replace(/-/g, "+")
        .replace(/_/g, "/")
        .padEnd(value.length + (m === 0 ? 0 : 4 - m), "="),
    ),
    (c) => c.charCodeAt(0),
  ).buffer;
}

async function concatUintArrays(arrays: Uint8Array[]) {
  const blob = new Blob(arrays);
  const buffer = await blob.arrayBuffer();
  return new Uint8Array(buffer);
}

async function compress(bytes: ArrayBuffer) {
  const s = new Blob([bytes]).stream();
  const cs = s.pipeThrough(new CompressionStream("deflate-raw"));
  const chunks: Uint8Array[] = [];
  for await (const chunk of streamAsyncIterator(cs)) {
    chunks.push(chunk);
  }
  return await concatUintArrays(chunks);
}

async function decompress(bytes: ArrayBuffer) {
  const s = new Blob([bytes]).stream();
  const ds = s.pipeThrough(new DecompressionStream("deflate-raw"));
  const chunks: Uint8Array[] = [];
  for await (const chunk of streamAsyncIterator(ds)) {
    chunks.push(chunk);
  }
  return await concatUintArrays(chunks);
}
