import { ZipFile } from "./zipfile";

class Buffer extends DataView {
  static alloc(n: number) {
    return new Buffer(new Uint8Array(n).buffer);
  }
  indexOf(n: number) {
    return new Uint8Array(this.buffer).indexOf(n);
  }
  get length() {
    return this.byteLength;
  }
  slice(start: number, end: number) {
    return new Buffer(this.buffer.slice(start, end));
  }
  readUInt8(i: number) {
    return this.getUint8(i);
  }
  writeUInt8(val: number, i: number) {
    this.setUint8(i, val);
  }
  write(value: string, start: number, len: number, enc: string) {
    // console.log("write", value, start, len, enc);
    let buf = new TextEncoder().encode(value);
    let ss = 0;
    let se = Math.min(len, buf.length);
    let ts = start;
    for (; ss < se; ss++, ts++) this.setInt8(ts, buf[ss]);
    shim.process.__lasterr.errno = 0;
    return se;
  }
  readDoubleLE(i: number) {
    return this.getFloat64(i, true);
  }
  readInt32LE(i: number) {
    return this.getInt32(i, true);
  }
  writeInt32LE(val: number, i: number) {
    return this.setInt32(i, val, true);
  }
  copy(target: Buffer, ts: number, ss: number, se: number) {
    for (; ss < se; ss++, ts++) target.setInt8(ts, this.getInt8(ss));
  }
  static concat(bufs: Buffer[]) {
    let size = bufs.reduce((a, b) => (a += b.byteLength), 0);
    let rval = Buffer.alloc(size);
    let off = 0;
    for (let buf of bufs) {
      const view = new Int8Array(rval.buffer);
      view.set(new Uint8Array(buf.buffer), off);
      off += buf.byteLength;
    }
    return rval;
  }
  toString() {
    return new TextDecoder().decode(this);
  }
}

export interface Handle {
  name: string;
  mode: string;
  pos: number;
  buf: Uint8Array;
}

interface Process {
  platform: string;
  stdout: {
    write(s: string): void;
  };
  argv: string[];
  exit(_: number): void;
  cwd(): string;
  env: Record<string, string>;
  __lasterr: { errno: number };
}
export interface NodeShim {
  stdout: string;
  archive?: ZipFile;
  process: Process;
  files: Record<string, Uint8Array>;
  fds: Handle[];
  tty: {
    isatty(): number;
  };
  os: {
    platform(): string;
  };
  fs: any;
}
export let shim: NodeShim = {
  // these three and process are poked at externally
  archive: undefined,
  stdout: "",
  files: {},
  fds: [],
  tty: {
    isatty() {
      return 0;
    },
  },
  os: {
    platform() {
      return "linux";
    },
  },
  fs: {
    // TODO - Idris is doing readdir, we should implement that
    opendirSync(name: string) {
      let fd = shim.fds.findIndex((x) => !x);
      if (fd < 0) fd = shim.fds.length;
      console.log("openDir", name);
      shim.process.__lasterr.errno = 0;
      return fd;
    },
    mkdirSync(name: string) {
      console.log("mkdir", name);
      shim.process.__lasterr.errno = 0;
      return 0;
    },
    openSync(name: string, mode: string) {
      console.log("open", name, mode);
      if (name.startsWith('./')) name = name.slice(2)
      let te = new TextEncoder();

      let fd = shim.fds.findIndex((x) => !x);
      if (fd < 0) fd = shim.fds.length;
      let buf: Uint8Array;
      let pos = 0;
      if (mode == "w") {
        buf = new Uint8Array(0);
      } else {
        // TODO, we need to involve localStorage when the window does multiple files and persists
        if (shim.files[name]) {
          buf = shim.files[name];
        } else if (shim.archive?.entries[name]) {
          // keep a copy of the uncompressed version for speed
          buf = shim.files[name] = shim.archive.getData(name)!;
        } else {
          shim.process.__lasterr.errno = 1;
          throw new Error(`${name} not found`);
        }
      }
      shim.process.__lasterr.errno = 0;
      shim.fds[fd] = { buf, pos, mode, name };
      // we'll mutate the pointer as stuff is read
      return fd;
    },
    writeSync(fd: number, line: string | Buffer) {
      try {
        let handle = shim.fds[fd];
        if (!handle) throw new Error(`bad fd ${fd}`);

        let buf2: ArrayBuffer;
        if (typeof line === "string") {
          buf2 = new TextEncoder().encode(line);
          let newbuf = new Uint8Array(handle.buf.byteLength + buf2.byteLength);
          newbuf.set(new Uint8Array(handle.buf));
          newbuf.set(new Uint8Array(buf2), handle.buf.byteLength);
          handle.buf = newbuf;
          shim.process.__lasterr.errno = 0;
        } else if (line instanceof Buffer) {
          let start = arguments[2];
          let len = arguments[3];
          buf2 = line.buffer.slice(start, start + len);
          let newbuf = new Uint8Array(handle.buf.byteLength + buf2.byteLength);
          newbuf.set(new Uint8Array(handle.buf));
          newbuf.set(new Uint8Array(buf2), handle.buf.byteLength);
          handle.buf = newbuf;
          shim.process.__lasterr.errno = 0;
          return len;
        } else {
          debugger;
          throw new Error(`write ${typeof line} not implemented`);
        }
      } catch (e) {
        debugger;
        throw e;
      }
    },
    chmodSync(fn: string, mode: number) {},
    fstatSync(fd: number) {
      let hand = shim.fds[fd];
      return { size: hand.buf.byteLength };
    },
    readSync(fd: number, buf: Buffer, start: number, len: number) {
      let hand = shim.fds[fd];
      let avail = hand.buf.length - hand.pos;
      let rd = Math.min(avail, len);
      let src = hand.buf;
      let dest = new Uint8Array(buf.buffer);
      for (let i = 0; i < rd; i++) dest[start + i] = src[hand.pos++];
      return rd;
    },
    closeSync(fd: number) {
      let handle = shim.fds[fd];
      // console.log("close", handle.name);
      if (handle.mode == "w") {
        shim.files[handle.name] = handle.buf;
      }
      delete shim.fds[fd];
    },
  },
  process: {
    platform: "linux",
    argv: ["", ""],
    stdout: {
      write(s) {
        shim.stdout += s;
      },
    },
    exit(v: number) {
      console.log("exit", v);
    },
    cwd() {
      return "";
    },
    env: {
      NO_COLOR: "true",
      IDRIS2_CG: "javascript",
      IDRIS2_PREFIX: "",
    },
    __lasterr: {
      errno: 0,
    },
    // stdin: { fd: 0 },
  },
};

// Spy on Idris' calls to see what we need to fill in
shim.fs = new Proxy(shim.fs, {
  get(target, prop, receiver) {
    if (prop in target) {
      return (target as any)[prop];
    }
    let err = new Error(`IMPLEMENT fs.${String(prop)}`);
    // idris support eats the exception
    console.error(err);
    throw err;
  },
});

// we intercept require to return our fake node modules
declare global {
  interface Window {
    require: (x: string) => any;
    process: Process;
  }
}
const requireStub: any = (x: string) => (shim as any)[x];
self.require = requireStub;
self.process = shim.process;
