import { typeConversion } from './conversion';
import { BigDecimal, BigInt } from './numbers';
import { Value } from './value';

/**
 * Byte array
 */
export class ByteArray extends Uint8Array {
  /**
   * Returns bytes in little-endian order.
   */
  static fromI32(x: i32): ByteArray {
    const self = new ByteArray(4);
    self[0] = x as u8;
    self[1] = (x >> 8) as u8;
    self[2] = (x >> 16) as u8;
    self[3] = (x >> 24) as u8;
    return self;
  }

  /**
   * Returns bytes in little-endian order.
   */
  static fromU32(x: u32): ByteArray {
    const self = new ByteArray(4);
    self[0] = x as u8;
    self[1] = (x >> 8) as u8;
    self[2] = (x >> 16) as u8;
    self[3] = (x >> 24) as u8;
    return self;
  }

  /**
   * Returns bytes in little-endian order.
   */
  static fromI64(x: i64): ByteArray {
    const self = new ByteArray(8);
    self[0] = x as u8;
    self[1] = (x >> 8) as u8;
    self[2] = (x >> 16) as u8;
    self[3] = (x >> 24) as u8;
    self[4] = (x >> 32) as u8;
    self[5] = (x >> 40) as u8;
    self[6] = (x >> 48) as u8;
    self[7] = (x >> 56) as u8;
    return self;
  }

  /**
   * Returns bytes in little-endian order.
   */
  static fromU64(x: u64): ByteArray {
    const self = new ByteArray(8);
    self[0] = x as u8;
    self[1] = (x >> 8) as u8;
    self[2] = (x >> 16) as u8;
    self[3] = (x >> 24) as u8;
    self[4] = (x >> 32) as u8;
    self[5] = (x >> 40) as u8;
    self[6] = (x >> 48) as u8;
    self[7] = (x >> 56) as u8;
    return self;
  }

  static empty(): ByteArray {
    return ByteArray.fromI32(0);
  }

  /**
   * Convert the string `hex` which must consist of an even number of
   * hexadecimal digits to a `ByteArray`. The string `hex` can optionally
   * start with '0x'
   */
  static fromHexString(hex: string): ByteArray {
    assert(hex.length % 2 == 0, 'input ' + hex + ' has odd length');
    // Skip possible `0x` prefix.
    if (hex.length >= 2 && hex.charAt(0) == '0' && hex.charAt(1) == 'x') {
      hex = hex.substr(2);
    }
    const output = new Bytes(hex.length / 2);
    for (let i = 0; i < hex.length; i += 2) {
      output[i / 2] = I8.parseInt(hex.substr(i, 2), 16);
    }
    return output;
  }

  static fromUTF8(str: string): ByteArray {
    const utf8 = String.UTF8.encode(str);
    return changetype<ByteArray>(ByteArray.wrap(utf8));
  }

  static fromBigInt(bigInt: BigInt): ByteArray {
    return changetype<ByteArray>(bigInt);
  }

  toHex(): string {
    return typeConversion.bytesToHex(this);
  }

  toHexString(): string {
    return typeConversion.bytesToHex(this);
  }

  toString(): string {
    return typeConversion.bytesToString(this);
  }

  toBase58(): string {
    return typeConversion.bytesToBase58(this);
  }

  /**
   * Interprets the byte array as a little-endian U32.
   * Throws in case of overflow.
   */

  toU32(): u32 {
    for (let i = 4; i < this.length; i++) {
      if (this[i] != 0) {
        assert(false, 'overflow converting ' + this.toHexString() + ' to u32');
      }
    }
    const paddedBytes = new Bytes(4);
    paddedBytes[0] = 0;
    paddedBytes[1] = 0;
    paddedBytes[2] = 0;
    paddedBytes[3] = 0;
    const minLen = paddedBytes.length < this.length ? paddedBytes.length : this.length;
    for (let i = 0; i < minLen; i++) {
      paddedBytes[i] = this[i];
    }
    let x: u32 = 0;
    x = (x | paddedBytes[3]) << 8;
    x = (x | paddedBytes[2]) << 8;
    x = (x | paddedBytes[1]) << 8;
    x = x | paddedBytes[0];
    return x;
  }

  /**
   * Interprets the byte array as a little-endian I32.
   * Throws in case of overflow.
   */

  toI32(): i32 {
    const isNeg = this.length > 0 && this[this.length - 1] >> 7 == 1;
    const padding = isNeg ? 255 : 0;
    for (let i = 4; i < this.length; i++) {
      if (this[i] != padding) {
        assert(false, 'overflow converting ' + this.toHexString() + ' to i32');
      }
    }
    const paddedBytes = new Bytes(4);
    paddedBytes[0] = padding;
    paddedBytes[1] = padding;
    paddedBytes[2] = padding;
    paddedBytes[3] = padding;
    const minLen = paddedBytes.length < this.length ? paddedBytes.length : this.length;
    for (let i = 0; i < minLen; i++) {
      paddedBytes[i] = this[i];
    }
    let x: i32 = 0;
    x = (x | paddedBytes[3]) << 8;
    x = (x | paddedBytes[2]) << 8;
    x = (x | paddedBytes[1]) << 8;
    x = x | paddedBytes[0];
    return x;
  }

  /** Create a new `ByteArray` that consist of `this` directly followed by
   * the bytes from `other` */
  concat(other: ByteArray): ByteArray {
    const newArray = new ByteArray(this.length + other.length);
    newArray.set(this, 0);
    newArray.set(other, this.length);
    return newArray;
  }

  /** Create a new `ByteArray` that consists of `this` directly followed by
   * the representation of `other` as bytes */
  concatI32(other: i32): ByteArray {
    return this.concat(ByteArray.fromI32(other));
  }

  /**
   * Interprets the byte array as a little-endian I64.
   * Throws in case of overflow.
   */

  toI64(): i64 {
    const isNeg = this.length > 0 && this[this.length - 1] >> 7 == 1;
    const padding = isNeg ? 255 : 0;
    for (let i = 8; i < this.length; i++) {
      if (this[i] != padding) {
        assert(false, 'overflow converting ' + this.toHexString() + ' to i64');
      }
    }
    const paddedBytes = new Bytes(8);
    paddedBytes[0] = padding;
    paddedBytes[1] = padding;
    paddedBytes[2] = padding;
    paddedBytes[3] = padding;
    paddedBytes[4] = padding;
    paddedBytes[5] = padding;
    paddedBytes[6] = padding;
    paddedBytes[7] = padding;
    const minLen = paddedBytes.length < this.length ? paddedBytes.length : this.length;
    for (let i = 0; i < minLen; i++) {
      paddedBytes[i] = this[i];
    }
    let x: i64 = 0;
    x = (x | paddedBytes[7]) << 8;
    x = (x | paddedBytes[6]) << 8;
    x = (x | paddedBytes[5]) << 8;
    x = (x | paddedBytes[4]) << 8;
    x = (x | paddedBytes[3]) << 8;
    x = (x | paddedBytes[2]) << 8;
    x = (x | paddedBytes[1]) << 8;
    x = x | paddedBytes[0];
    return x;
  }

  /**
   * Interprets the byte array as a little-endian U64.
   * Throws in case of overflow.
   */

  toU64(): u64 {
    for (let i = 8; i < this.length; i++) {
      if (this[i] != 0) {
        assert(false, 'overflow converting ' + this.toHexString() + ' to u64');
      }
    }
    const paddedBytes = new Bytes(8);
    paddedBytes[0] = 0;
    paddedBytes[1] = 0;
    paddedBytes[2] = 0;
    paddedBytes[3] = 0;
    paddedBytes[4] = 0;
    paddedBytes[5] = 0;
    paddedBytes[6] = 0;
    paddedBytes[7] = 0;
    const minLen = paddedBytes.length < this.length ? paddedBytes.length : this.length;
    for (let i = 0; i < minLen; i++) {
      paddedBytes[i] = this[i];
    }
    let x: u64 = 0;
    x = (x | paddedBytes[7]) << 8;
    x = (x | paddedBytes[6]) << 8;
    x = (x | paddedBytes[5]) << 8;
    x = (x | paddedBytes[4]) << 8;
    x = (x | paddedBytes[3]) << 8;
    x = (x | paddedBytes[2]) << 8;
    x = (x | paddedBytes[1]) << 8;
    x = x | paddedBytes[0];
    return x;
  }

  @operator('==')
  equals(other: ByteArray): boolean {
    if (this.length != other.length) {
      return false;
    }
    for (let i = 0; i < this.length; i++) {
      if (this[i] != other[i]) {
        return false;
      }
    }
    return true;
  }

  @operator('!=')
  notEqual(other: ByteArray): boolean {
    return !(this == other);
  }
}

/** A dynamically-sized byte array. */
export class Bytes extends ByteArray {
  static fromByteArray(byteArray: ByteArray): Bytes {
    return changetype<Bytes>(byteArray);
  }

  static fromUint8Array(uint8Array: Uint8Array): Bytes {
    return changetype<Bytes>(uint8Array);
  }

  /**
   * Convert the string `hex` which must consist of an even number of
   * hexadecimal digits to a `ByteArray`. The string `hex` can optionally
   * start with '0x'
   */
  static fromHexString(str: string): Bytes {
    return changetype<Bytes>(ByteArray.fromHexString(str));
  }

  static fromUTF8(str: string): Bytes {
    return Bytes.fromByteArray(ByteArray.fromUTF8(str));
  }

  static fromI32(i: i32): Bytes {
    return changetype<Bytes>(ByteArray.fromI32(i));
  }

  static empty(): Bytes {
    return changetype<Bytes>(ByteArray.empty());
  }

  concat(other: Bytes): Bytes {
    return changetype<Bytes>(super.concat(other));
  }

  concatI32(other: i32): Bytes {
    return changetype<Bytes>(super.concat(ByteArray.fromI32(other)));
  }
}

/**
 * TypedMap entry.
 */
export class TypedMapEntry<K, V> {
  key: K;
  value: V;

  constructor(key: K, value: V) {
    this.key = key;
    this.value = value;
  }
}

/** Typed map */
export class TypedMap<K, V> {
  entries: Array<TypedMapEntry<K, V>>;

  constructor() {
    this.entries = new Array<TypedMapEntry<K, V>>(0);
    // this.entries = []
  }

  set(key: K, value: V): void {
    const entry = this.getEntry(key);
    if (entry === null) {
      const entry = new TypedMapEntry<K, V>(key, value);
      this.entries.push(entry);
    } else {
      entry.value = value;
    }
  }

  getEntry(key: K): TypedMapEntry<K, V> | null {
    for (let i: i32 = 0; i < this.entries.length; i++) {
      if (this.entries[i].key == key) {
        return this.entries[i];
      }
    }
    return null;
  }

  mustGetEntry(key: K): TypedMapEntry<K, V> {
    const entry = this.getEntry(key);
    assert(entry != null, `Entry for key ${key} does not exist in TypedMap`);
    return entry!;
  }

  get(key: K): V | null {
    for (let i: i32 = 0; i < this.entries.length; i++) {
      if (this.entries[i].key == key) {
        return this.entries[i].value;
      }
    }
    return null;
  }

  mustGet(key: K): V {
    const value = this.get(key);
    assert(value != null, `Value for key ${key} does not exist in TypedMap`);
    return value!;
  }

  isSet(key: K): bool {
    for (let i: i32 = 0; i < this.entries.length; i++) {
      if (this.entries[i].key == key) {
        return true;
      }
    }
    return false;
  }
}

/**
 * Common representation for entity data, storing entity attributes
 * as `string` keys and the attribute values as dynamically-typed
 * `Value` objects.
 */
export class Entity extends TypedMap<string, Value> {
  unset(key: string): void {
    this.set(key, Value.fromNull());
  }

  /** Assigns properties from sources to this Entity in right-to-left order */
  merge(sources: Array<Entity>): Entity {
    const target = this;
    for (let i = 0; i < sources.length; i++) {
      const entries = sources[i].entries;
      for (let j = 0; j < entries.length; j++) {
        target.set(entries[j].key, entries[j].value);
      }
    }
    return target;
  }

  setString(key: string, value: string): void {
    this.set(key, Value.fromString(value));
  }

  setI32(key: string, value: i32): void {
    this.set(key, Value.fromI32(value));
  }

  setBigInt(key: string, value: BigInt): void {
    this.set(key, Value.fromBigInt(value));
  }

  setBytes(key: string, value: Bytes): void {
    this.set(key, Value.fromBytes(value));
  }

  setBoolean(key: string, value: bool): void {
    this.set(key, Value.fromBoolean(value));
  }

  setBigDecimal(key: string, value: BigDecimal): void {
    this.set(key, Value.fromBigDecimal(value));
  }

  getString(key: string): string {
    return this.get(key)!.toString();
  }

  getI32(key: string): i32 {
    return this.get(key)!.toI32();
  }

  getBigInt(key: string): BigInt {
    return this.get(key)!.toBigInt();
  }

  getBytes(key: string): Bytes {
    return this.get(key)!.toBytes();
  }

  getBoolean(key: string): boolean {
    return this.get(key)!.toBoolean();
  }

  getBigDecimal(key: string): BigDecimal {
    return this.get(key)!.toBigDecimal();
  }
}

/**
 * The result of an operation, with a corresponding value and error type.
 */
export class Result<V, E> {
  _value: Wrapped<V> | null;
  _error: Wrapped<E> | null;

  get isOk(): boolean {
    return this._value !== null;
  }

  get isError(): boolean {
    return this._error !== null;
  }

  get value(): V {
    assert(this._value != null, 'Trying to get a value from an error result');
    return changetype<Wrapped<V>>(this._value).inner;
  }

  get error(): E {
    assert(this._error != null, 'Trying to get an error from a successful result');
    return changetype<Wrapped<E>>(this._error).inner;
  }
}

// This is used to wrap a generic so that it can be unioned with `null`, working around limitations
// with primitives.
export class Wrapped<T> {
  inner: T;

  constructor(inner: T) {
    this.inner = inner;
  }
}
