import { randomBytes } from 'crypto';
import {Int64BE, Uint64BE} from 'int64-buffer';
import crypto from 'crypto';
import {TextEncoder} from 'text-encoding';

import {
  be_ui8_array_to_i16,
  be_ui8_array_to_i32,
  be_ui8_array_to_ui16,
  be_ui8_array_to_ui32,
  i16_to_be_ui8_array,
  i32_to_be_ui8_array,
  i8_to_ui8_array,
  ui16_to_be_ui8_array,
  ui32_to_be_ui8_array,
  ui8_array_to_i8,
  ui8_array_to_ui8,
  ui8_to_ui8_array,
} from '../utils/byte_order';
import JSChaCha20 from '../utils/jschacha20';

export default class packet {
  constructor(token) {
    this.token = token;
  }

  static HEADER = new TextEncoder().encode('repertory');

  buffer = null;
  decode_offset = 0;
  token;

  append_buffer = (buffer) => {
    if (!(buffer instanceof Uint8Array)) {
      throw new Error('Buffer must be of type Uint8Array');
    }

    this.buffer = this.buffer
      ? new Uint8Array([...this.buffer, ...buffer])
      : buffer;
  };

  clear = () => {
    this.buffer = null;
    this.decode_offset = 0;
  };

  decode_buffer = (length) => {
    if (!this.buffer) {
      throw new Error('Invalid buffer');
    }

    const ret = this.buffer.slice(
      this.decode_offset,
      this.decode_offset + length
    );
    this.decode_offset += length;
    return Buffer.from(ret);
  };

  decode_stat = () => {
    const mode = this.decode_ui16();
    const nlink = this.decode_ui16();
    const uid = this.decode_ui32();
    const gid = this.decode_ui32();
    const atime = this.decode_ui64();
    const mtime = this.decode_ui64();
    const ctime = this.decode_ui64();
    const birth_time = this.decode_ui64();
    const size = this.decode_ui64();
    const blocks = this.decode_ui64();
    const blksize = this.decode_ui32();
    const flags = this.decode_ui32();
    const directory = !!this.decode_ui8();
    return {
      mode,
      nlink,
      uid,
      gid,
      atime,
      mtime,
      ctime,
      birth_time,
      size,
      blocks,
      blksize,
      flags,
      directory,
    };
  };

  decode_utf8 = () => {
    if (!this.buffer) {
      throw new Error('Invalid buffer');
    }

    const startIndex = this.decode_offset;
    const endIndex = this.buffer.indexOf(0, startIndex);
    if (endIndex >= 0) {
      let ret = '';
      for (let i = startIndex; i < endIndex; i++) {
        ret += String.fromCharCode(this.buffer[i]);
      }
      this.decode_offset = endIndex + 1;
      return ret;
    }

    throw new Error('String not found in buffer');
  };

  decode_i8 = () => {
    return ui8_array_to_i8(this.buffer, this.decode_offset++);
  };

  decode_ui8 = () => {
    return ui8_array_to_ui8(this.buffer, this.decode_offset++);
  };

  decode_i16 = () => {
    const ret = be_ui8_array_to_i16(this.buffer, this.decode_offset);
    this.decode_offset += 2;
    return ret;
  };

  decode_ui16 = () => {
    const ret = be_ui8_array_to_ui16(this.buffer, this.decode_offset);
    this.decode_offset += 2;
    return ret;
  };

  decode_i32 = () => {
    const ret = be_ui8_array_to_i32(this.buffer, this.decode_offset);
    this.decode_offset += 4;
    return ret;
  };

  decode_ui32 = () => {
    const ret = be_ui8_array_to_ui32(this.buffer, this.decode_offset);
    this.decode_offset += 4;
    return ret;
  };

  decode_i64 = () => {
    const ret = new Int64BE(
      this.buffer.slice(this.decode_offset, this.decode_offset + 8)
    );
    this.decode_offset += 8;
    return ret.toString(10);
  };

  decode_ui64 = () => {
    const ret = new Uint64BE(
      this.buffer.slice(this.decode_offset, this.decode_offset + 8)
    );
    this.decode_offset += 8;
    return ret.toString(10);
  };

  decrypt = async () => {
    try {
      let hash = crypto.createHash('sha256');
      hash = hash.update(new TextEncoder().encode(this.token));

      const key = Uint8Array.from(hash.digest());
      const nonce = this.buffer.slice(0, 12);

      this.buffer = new JSChaCha20(key, nonce, 0).decrypt(
        this.buffer.slice(12)
      );

      this.decode_offset = packet.HEADER.length;

      const header = this.buffer.slice(0, 9);
      if (header.toString() !== packet.HEADER.toString()) {
        return Promise.reject(new Error('Header does not match'));
      }

      return this.buffer;
    } catch (e) {
      return Promise.reject(e);
    }
  };

  encode_buffer = (buffer) => {
    this.append_buffer(new Uint8Array(buffer));
  };

  encode_i8 = (num) => {
    this.append_buffer(i8_to_ui8_array(num));
  };

  encode_top_i8 = (num) => {
    this.push_buffer(i8_to_ui8_array(num));
  };

  encode_u8 = (num) => {
    this.append_buffer(ui8_to_ui8_array(num));
  };

  encode_top_u8 = (num) => {
    this.push_buffer(ui8_to_ui8_array(num));
  };

  encode_i16 = (num) => {
    this.append_buffer(i16_to_be_ui8_array(num));
  };

  encode_top_i16 = (num) => {
    this.push_buffer(i16_to_be_ui8_array(num));
  };

  encode_ui16 = (num) => {
    this.append_buffer(ui16_to_be_ui8_array(num));
  };

  encode_top_ui16 = (num) => {
    this.push_buffer(ui16_to_be_ui8_array(num));
  };

  encode_i32 = (num) => {
    this.append_buffer(i32_to_be_ui8_array(num));
  };

  encode_top_i32 = (num) => {
    this.push_buffer(i32_to_be_ui8_array(num));
  };

  encode_ui32 = (num) => {
    this.append_buffer(ui32_to_be_ui8_array(num));
  };

  encode_top_ui32 = (num) => {
    this.push_buffer(ui32_to_be_ui8_array(num));
  };

  encode_i64 = (num) => {
    this.append_buffer(new Uint8Array(new Int64BE(num).toArray()));
  };

  encode_top_i64 = (num) => {
    this.push_buffer(new Uint8Array(new Int64BE(num).toArray()));
  };

  encode_ui64 = (num) => {
    this.append_buffer(new Uint8Array(new Uint64BE(num).toArray()));
  };

  encode_top_ui64 = (num) => {
    this.push_buffer(new Uint8Array(new Uint64BE(num).toArray()));
  };

  encode_utf8 = (str) => {
    if (!(typeof str === 'string' || str instanceof String)) {
      throw new Error('Value must be of type string');
    }

    const buffer = new Uint8Array([...new TextEncoder().encode(str), 0]);
    this.append_buffer(buffer);
  };

  encode_top_utf8 = (str) => {
    if (!(typeof str === 'string' || str instanceof String)) {
      throw new Error('Value must be of type string');
    }

    const buffer = new Uint8Array([...new TextEncoder().encode(str), 0]);
    this.push_buffer(buffer);
  };

  encrypt = async (nonce) => {
    try {
      this.push_buffer(packet.HEADER);
      let hash = crypto.createHash('sha256');
      hash = hash.update(new TextEncoder().encode(this.token));

      const key = Uint8Array.from(hash.digest());
      if (!nonce) {
        nonce = Uint8Array.from(randomBytes(12));
      }

      this.buffer = new JSChaCha20(key, nonce, 0).encrypt(this.buffer);
      this.push_buffer(nonce);

      return this.buffer;
    } catch (e) {
      return Promise.reject(e);
    }
  };

  push_buffer = (buffer) => {
    if (!(buffer instanceof Uint8Array)) {
      throw new Error('Buffer must be of type Uint8Array');
    }

    this.buffer = this.buffer
      ? new Uint8Array([...buffer, ...this.buffer])
      : buffer;
  };
}
