import { SchemaHash } from './schemaHash';
import { ElemBytes } from './elemBytes';
import { Constants } from './constants';
import { Id } from './id';
import {
  checkBigIntArrayInField,
  checkBigIntInField,
  getDateFromUnixTimestamp,
  getUint32,
  getUint64,
  getUnixTimestamp,
  putUint32,
  putUint64 as getBytesFromUint64
} from './utils';
import { Hex, poseidon } from '@iden3/js-crypto';

/*
Claim structure

Index:
 i_0: [ 128  bits ] claim schema
      [ 32 bits ] option flags
          [3] Subject:
            000: A.1 Self
            001: invalid
            010: A.2.i OtherIden Index
            011: A.2.v OtherIden Value
            100: B.i Object Index
            101: B.v Object Value
          [1] Expiration: bool
          [1] Updatable: bool
          [3] Merklized: data is merklized root is stored in the:
            000: none
            001: C.i Root Index (root located in i_2)
            010: C.v Root Value (root located in v_2)
          [24] 0
      [ 32 bits ] version (optional?)
      [ 61 bits ] 0 - reserved for future use
 i_1: [ 248 bits] identity (case b) (optional)
      [  5 bits ] 0
 i_2: [ 253 bits] 0
 i_3: [ 253 bits] 0
Value:
 v_0: [ 64 bits ]  revocation nonce
      [ 64 bits ]  expiration date (optional)
      [ 125 bits] 0 - reserved
 v_1: [ 248 bits] identity (case c) (optional)
      [  5 bits ] 0
 v_2: [ 253 bits] 0
 v_3: [ 253 bits] 0
*/

export enum SlotName {
  IndexA = 'IndexA',
  IndexB = 'IndexB',
  ValueA = 'ValueA',
  ValueB = 'ValueB'
}

// ErrSlotOverflow means some ElemBytes overflows Q Field. And wraps the name
// of overflowed slot.
export class ErrSlotOverflow extends Error {
  constructor(msg: string) {
    super(`Slot ${msg} not in field (too large)`);
    Object.setPrototypeOf(this, ErrSlotOverflow.prototype);
  }
}

// subjectFlag for the time being describes the location of Id (in index or value
// slots or nowhere at all).
//
// Values subjectFlagInvalid presents for backward compatibility and for now means nothing.

export enum SubjectFlag {
  Self = 0b0,
  Invalid = 0b1,
  OtherIdenIndex = 0b10,
  OtherIdenValue = 0b11
}

export enum IdPosition {
  None = 0,
  Index = 1,
  Value = 2
}

// merklizedFlag for the time being describes the location of root (in index or value
// slots or nowhere at all).
//
// Values merklizedFlagIndex indicates that root is located in index[2] slots.
// Values merklizedFlagValue indicates that root is located in value[2] slots.
export enum MerklizedFlag {
  None = 0b00000000,
  Index = 0b00100000,
  Value = 0b01000000,
  Invalid = 0b10000000
}

export enum MerklizedRootPosition {
  None = 0,
  Index = 1,
  Value = 2
}

export enum Flags {
  ByteIdx = 16,
  ExpirationBitIdx = 3,
  UpdatableBitIdx = 4
}

export class Claim {
  private _index: ElemBytes[] = [];
  private _value: ElemBytes[] = [];

  constructor() {
    for (let i = 0; i < Constants.ELEM_BYTES_LENGTH; i++) {
      this._index[i] = new ElemBytes();
      this._value[i] = new ElemBytes();
    }
  }

  // NewClaim creates new Claim with specified SchemaHash and any number of
  // options. Using options you can specify any field in claim.
  static newClaim(sh: SchemaHash, ...args: ClaimOption[]): Claim {
    const c = new Claim();
    c.setSchemaHash(sh);
    for (let i = 0; i < args.length; i++) {
      const fn = args[i];
      fn(c);
    }
    return c;
  }

  // GetSchemaHash return copy of claim's schema hash.
  getSchemaHash(): SchemaHash {
    return new SchemaHash(this._index[0].bytes.slice(0, Constants.SCHEMA.HASH_LENGTH));
  }

  get value(): ElemBytes[] {
    return this._value;
  }

  set value(value: ElemBytes[]) {
    this._value = value;
  }

  get index(): ElemBytes[] {
    return this._index;
  }

  set index(value: ElemBytes[]) {
    this._index = value;
  }

  // SetSchemaHash updates claim's schema hash.
  setSchemaHash(sh: SchemaHash) {
    this._index[0] = new ElemBytes(
      Uint8Array.from([...sh.bytes, ...new Array(Constants.SCHEMA.HASH_LENGTH).fill(0)])
    );
  }

  setSubject(s: SubjectFlag) {
    // clean first 3 bits
    this._index[0].bytes[Flags.ByteIdx] &= 0b11111000;
    this._index[0].bytes[Flags.ByteIdx] |= s;
  }

  private getSubject(): SubjectFlag {
    let sbj = this._index[0].bytes[Flags.ByteIdx];
    // clean all except first 3 bits
    sbj &= 0b00000111;
    return sbj as SubjectFlag;
  }

  private setFlagExpiration(val: boolean) {
    if (val) {
      this._index[0].bytes[Flags.ByteIdx] |= 0b1 << Flags.ExpirationBitIdx;
    } else {
      this._index[0].bytes[Flags.ByteIdx] &= ~(0b1 << Flags.ExpirationBitIdx);
    }
  }

  private getFlagExpiration(): boolean {
    const mask = 0b1 << Flags.ExpirationBitIdx;
    return (this._index[0].bytes[Flags.ByteIdx] & mask) > 0;
  }

  // GetIDPosition returns the position at which the Id is stored.
  getIdPosition(): IdPosition {
    switch (this.getSubject()) {
      case SubjectFlag.Self:
        return IdPosition.None;
      case SubjectFlag.OtherIdenIndex:
        return IdPosition.Index;
      case SubjectFlag.OtherIdenValue:
        return IdPosition.Value;
      default:
        throw Constants.ERRORS.INVALID_SUBJECT_POSITION;
    }
  }

  // SetValueDataInts sets data to value slots A & B.
  // Returns ErrSlotOverflow if slotA or slotB value are too big.
  setValueDataInts(slotA: bigint | null, slotB: bigint | null): void {
    this._value[2] = this.setSlotInt(slotA, SlotName.ValueA);
    this._value[3] = this.setSlotInt(slotB, SlotName.ValueB);
  }
  // SetValueDataBytes sets data to value slots A & B.
  // Returns ErrSlotOverflow if slotA or slotB value are too big.
  setValueDataBytes(slotA: Uint8Array, slotB: Uint8Array): void {
    this._value[2] = this.setSlotBytes(slotA, SlotName.ValueA);
    this._value[3] = this.setSlotBytes(slotB, SlotName.ValueB);
  }
  // SetValueData sets data to value slots A & B.
  // Returns ErrSlotOverflow if slotA or slotB value are too big.
  setValueData(slotA: ElemBytes, slotB: ElemBytes): void {
    const slotsAsInts: bigint[] = [slotA.toBigInt(), slotB.toBigInt()];
    if (!checkBigIntArrayInField(slotsAsInts)) {
      throw Constants.ERRORS.DATA_OVERFLOW;
    }
    this._value[2] = slotA;
    this._value[3] = slotB;
  }
  // SetIndexDataInts sets data to index slots A & B.
  // Returns ErrSlotOverflow if slotA or slotB value are too big.
  setIndexDataInts(slotA: bigint | null, slotB: bigint | null): void {
    this._index[2] = this.setSlotInt(slotA, SlotName.IndexA);
    this._index[3] = this.setSlotInt(slotB, SlotName.IndexB);
  }
  // SetIndexDataBytes sets data to index slots A & B.
  // Returns ErrSlotOverflow if slotA or slotB value are too big.
  setIndexDataBytes(slotA: Uint8Array | null, slotB: Uint8Array | null): void {
    this._index[2] = this.setSlotBytes(slotA, SlotName.IndexA);
    this._index[3] = this.setSlotBytes(slotB, SlotName.IndexB);
  }

  private setSlotBytes(value: Uint8Array | null, slotName: SlotName): ElemBytes {
    const slot = new ElemBytes(value);
    if (!checkBigIntInField(slot.toBigInt())) {
      throw new ErrSlotOverflow(slotName);
    }
    return slot;
  }

  setFlagMerklized(s: MerklizedRootPosition): void {
    let f: number;
    switch (s) {
      case MerklizedRootPosition.Index:
        f = MerklizedFlag.Index;
        break;
      case MerklizedRootPosition.Value:
        f = MerklizedFlag.Value;
        break;
      default:
        f = MerklizedFlag.None;
    }
    // clean last 3 bits
    this.index[0].bytes[Flags.ByteIdx] &= 0b00011111;
    this.index[0].bytes[Flags.ByteIdx] |= f;
  }

  private getMerklized(): MerklizedFlag {
    let mt = this.index[0].bytes[Flags.ByteIdx];
    // clean all except last 3 bits
    mt &= 0b11100000;
    return mt as MerklizedFlag;
  }

  // GetMerklizedPosition returns the position at which the Merklized flag is stored.
  getMerklizedPosition(): MerklizedRootPosition {
    switch (this.getMerklized()) {
      case MerklizedFlag.None:
        return MerklizedRootPosition.None;
      case MerklizedFlag.Index:
        return MerklizedRootPosition.Index;
      case MerklizedFlag.Value:
        return MerklizedRootPosition.Value;
      default:
        throw Constants.ERRORS.INCORRECT_MERKLIZED_POSITION;
    }
  }

  public setSlotInt(value: bigint | null, slotName: SlotName): ElemBytes {
    if (!value) {
      value = BigInt(0);
    }
    if (!checkBigIntInField(value)) {
      throw new ErrSlotOverflow(slotName);
    }
    return new ElemBytes().setBigInt(value);
  }
  // SetIndexData sets data to index slots A & B.
  // Returns ErrSlotOverflow if slotA or slotB value are too big.
  setIndexData(slotA: ElemBytes, slotB: ElemBytes) {
    const slotsAsInts: bigint[] = [slotA.toBigInt(), slotB.toBigInt()];
    if (!checkBigIntArrayInField(slotsAsInts)) {
      throw Constants.ERRORS.DATA_OVERFLOW;
    }
    this._index[2] = slotA;
    this._index[3] = slotB;
  }

  resetExpirationDate(): void {
    this.setFlagExpiration(false);
    const bytes = Array.from({ length: Constants.NONCE_BYTES_LENGTH }, () => 0);
    const arr = Array.from(this._value[0].bytes);
    arr.splice(Constants.NONCE_BYTES_LENGTH, Constants.NONCE_BYTES_LENGTH, ...bytes);
    this._value[0] = new ElemBytes(Uint8Array.from(arr));
  }

  // GetExpirationDate returns expiration date and flag. Flag is true if
  // expiration date is present, false if null.
  getExpirationDate(): Date | null {
    if (this.getFlagExpiration()) {
      const unixTimestamp = getUint64(this._value[0].bytes.slice(8, 16));
      return getDateFromUnixTimestamp(Number(unixTimestamp));
    }
    return null;
  }

  // SetExpirationDate sets expiration date to dt
  setExpirationDate(dt: Date) {
    this.setFlagExpiration(true);
    const bytes = getBytesFromUint64(BigInt(getUnixTimestamp(dt)));
    const arr = Array.from(this._value[0].bytes);
    arr.splice(Constants.NONCE_BYTES_LENGTH, Constants.NONCE_BYTES_LENGTH, ...bytes);
    this._value[0] = new ElemBytes(Uint8Array.from(arr));
  }

  // GetRevocationNonce returns revocation nonce
  getRevocationNonce(): bigint {
    return getUint64(this._value[0].bytes.slice(0, 8));
  }
  // SetRevocationNonce sets claim's revocation nonce
  setRevocationNonce(nonce: bigint): void {
    const bytes = getBytesFromUint64(nonce);
    if (bytes.length > Constants.NONCE_BYTES_LENGTH) {
      throw new Error('Nonce length is not valid');
    }
    const arr = Array.from(this._value[0].bytes);
    arr.splice(0, Constants.NONCE_BYTES_LENGTH, ...bytes);
    this._value[0] = new ElemBytes(Uint8Array.from(arr));
  }

  getValueId(): Id {
    return Id.fromBytes(this._value[1].bytes.slice(0, -1));
  }

  // SetValueId sets id to value. Removes id from index if any.
  setValueId(id: Id): void {
    this.resetIndexId();
    this.setSubject(SubjectFlag.OtherIdenValue);
    const arr = Array.from(this._index[1].bytes);
    arr.splice(0, id.bytes.length, ...id.bytes);
    this._value[1] = new ElemBytes(Uint8Array.from(arr));
  }

  private resetIndexId() {
    this._index[1] = new ElemBytes(new Uint8Array(Constants.BYTES_LENGTH).fill(0));
  }

  private resetValueId(): void {
    this._value[1] = new ElemBytes(new Uint8Array(Constants.BYTES_LENGTH).fill(0));
  }

  getIndexId(): Id {
    return Id.fromBytes(this._index[1].bytes.slice(0, -1));
  }

  // SetIndexId sets id to index. Removes id from value if any.
  setIndexId(id: Id): void {
    this.resetValueId();
    this.setSubject(SubjectFlag.OtherIdenIndex);
    const arr = Array.from(this._index[1].bytes);
    arr.splice(0, id.bytes.length, ...id.bytes);
    this._index[1] = new ElemBytes(Uint8Array.from(arr));
  }
  // SetVersion sets claim's version
  setVersion(ver: number) {
    const bytes = putUint32(ver);
    this._index[0].bytes[20] = bytes[0];
    this._index[0].bytes[21] = bytes[1];
    this._index[0].bytes[22] = bytes[2];
    this._index[0].bytes[23] = bytes[3];
  }
  // GetVersion returns claim's version
  getVersion(): number {
    return getUint32(this._index[0].bytes.slice(20, 24));
  }
  // SetFlagUpdatable sets claim's flag `updatable`
  setFlagUpdatable(val: boolean) {
    if (val) {
      this._index[0].bytes[Flags.ByteIdx] |= 0b1 << Flags.UpdatableBitIdx;
    } else {
      this._index[0].bytes[Flags.ByteIdx] &= ~(0b1 << Flags.UpdatableBitIdx);
    }
  }

  // HIndex calculates the hash of the Index of the Claim
  hIndex(): bigint {
    return poseidon.hash(ElemBytes.elemBytesToInts(this._index));
  }

  // GetFlagUpdatable returns claim's flag `updatable`
  getFlagUpdatable(): boolean {
    const mask = 0b1 << Flags.UpdatableBitIdx;
    return (this._index[0].bytes[Flags.ByteIdx] & mask) > 0;
  }

  // HValue calculates the hash of the Value of the Claim
  hValue(): bigint {
    return poseidon.hash(ElemBytes.elemBytesToInts(this._value));
  }

  // HiHv returns the HIndex and HValue of the Claim
  hiHv(): { hi: bigint; hv: bigint } {
    return { hi: this.hIndex(), hv: this.hValue() };
  }

  // SetIndexMerklizedRoot sets merklized root to index. Removes root from value[2] if any.
  setIndexMerklizedRoot(r: bigint): void {
    this.resetValueMerklizedRoot();
    this.setFlagMerklized(MerklizedRootPosition.Index);
    this.index[2] = this.setSlotInt(r, SlotName.IndexA);
  }

  resetIndexMerklizedRoot() {
    this._index[2] = new ElemBytes(new Uint8Array(Constants.BYTES_LENGTH).fill(0));
  }

  // SetValueMerklizedRoot sets merklized root to value. Removes root from index[2] if any.
  setValueMerklizedRoot(r: bigint): void {
    this.resetIndexMerklizedRoot();
    this.setFlagMerklized(MerklizedRootPosition.Value);
    this.value[2] = this.setSlotInt(r, SlotName.ValueA);
  }
  resetValueMerklizedRoot() {
    this._value[2] = new ElemBytes(new Uint8Array(Constants.BYTES_LENGTH).fill(0));
  }

  // GetMerklizedRoot returns merklized root from claim's index of value.
  // Returns error ErrNoMerklizedRoot if MerklizedRoot is not set.
  getMerklizedRoot(): bigint {
    switch (this.getMerklized()) {
      case MerklizedFlag.Index:
        return this.index[2].toBigInt();
      case MerklizedFlag.Value:
        return this.value[2].toBigInt();
      default:
        throw Constants.ERRORS.NO_MERKLIZED_ROOT;
    }
  }

  // resetId deletes Id from index and from value.
  resetId(): void {
    this.resetIndexId();
    this.resetValueId();
    this.setSubject(SubjectFlag.Self);
  }
  // GetId returns Id from claim's index of value.
  // Returns error ErrNoId if Id is not set.
  getId(): Id {
    switch (this.getSubject()) {
      case SubjectFlag.OtherIdenIndex:
        return this.getIndexId();
      case SubjectFlag.OtherIdenValue:
        return this.getValueId();
      default:
        throw Constants.ERRORS.NO_ID;
    }
  }
  // RawSlots returns raw bytes of claim's index and value
  rawSlots(): { index: ElemBytes[]; value: ElemBytes[] } {
    return {
      index: this._index,
      value: this._value
    };
  }
  // RawSlotsAsInts returns slots as []bigint
  rawSlotsAsInts(): bigint[] {
    return [...ElemBytes.elemBytesToInts(this._index), ...ElemBytes.elemBytesToInts(this._value)];
  }

  clone(): Claim {
    return JSON.parse(JSON.stringify(this));
  }

  marshalJson(): string[] {
    return this.rawSlotsAsInts().map((b) => b.toString());
  }

  unMarshalJson(b: string): Claim {
    const ints: bigint[] = JSON.parse(b).map((s: string) => BigInt(s));

    if (ints.length !== this._index.length + this._value.length) {
      throw new Error("invalid number of claim's slots");
    }
    this._index = [];
    this._value = [];
    for (let i = 0, j = Constants.ELEM_BYTES_LENGTH; i < ints.length / 2; i++, j++) {
      this._index[i] = new ElemBytes();
      this._index[i].setBigInt(ints[i]);
      this._value[i] = new ElemBytes();
      this._value[i].setBigInt(ints[j]);
    }
    return this;
  }

  marshalBinary(): Uint8Array {
    const getBytes = (src: ElemBytes[]) =>
      src.reduce((acc: number[], cur: ElemBytes) => {
        return [...acc, ...cur.bytes];
      }, []);
    return Uint8Array.from(getBytes(this._index).concat(getBytes(this._value)));
  }

  // Hex returns hex representation of binary claim
  hex(): string {
    const b = this.marshalBinary();
    return Hex.encodeString(b);
  }

  fromHex(hex: string): Claim {
    const b = Hex.decodeString(hex);
    this.unMarshalBinary(b);
    return this;
  }

  unMarshalBinary(data: Uint8Array): void {
    const wantLen = 2 * Constants.ELEM_BYTES_LENGTH * Constants.BYTES_LENGTH;
    if (data.length !== wantLen) {
      throw new Error('unexpected length of input data');
    }
    this._index = [];
    this._value = [];
    for (let i = 0, j = Constants.ELEM_BYTES_LENGTH; i < Constants.ELEM_BYTES_LENGTH; i++, j++) {
      this._index[i] = new ElemBytes(
        data.slice(i * Constants.BYTES_LENGTH, (i + 1) * Constants.BYTES_LENGTH)
      );
      this._value[i] = new ElemBytes(
        data.slice(j * Constants.BYTES_LENGTH, (j + 1) * Constants.BYTES_LENGTH)
      );
    }
  }
}

// Option provides the ability to set different Claim's fields on construction
export type ClaimOption = (c: Claim) => void;
export class ClaimOptions {
  // WithFlagUpdatable sets claim's flag `updatable`
  static withFlagUpdatable(val: boolean): ClaimOption {
    return (c: Claim) => c.setFlagUpdatable(val);
  }

  // WithVersion sets claim's version
  static withVersion(ver: number): ClaimOption {
    return (c: Claim) => c.setVersion(ver);
  }

  // WithIndexId sets Id to claim's index
  static withIndexId(id: Id): ClaimOption {
    return (c: Claim) => c.setIndexId(id);
  }

  // WithValueId sets Id to claim's value
  static withValueId(id: Id): ClaimOption {
    return (c: Claim) => c.setValueId(id);
  }

  // WithFlagMerklized sets claim's flag `merklized`
  static withFlagMerklized(p: MerklizedRootPosition): ClaimOption {
    return (c: Claim) => c.setFlagMerklized(p);
  }

  // WithId sets Id to claim's index or value depending on `pos`.
  static withId(id: Id, pos: IdPosition): ClaimOption {
    return (c: Claim) => {
      switch (pos) {
        case IdPosition.Index:
          c.setIndexId(id);
          break;
        case IdPosition.Value:
          c.setValueId(id);
          break;
        default:
          throw Constants.ERRORS.INCORRECT_ID_POSITION;
      }
    };
  }

  // WithRevocationNonce sets claim's revocation nonce.
  static withRevocationNonce(nonce: bigint): ClaimOption {
    return (c: Claim) => c.setRevocationNonce(nonce);
  }

  // WithExpirationDate sets claim's expiration date to `dt`.
  static withExpirationDate(dt: Date): ClaimOption {
    return (c: Claim) => c.setExpirationDate(dt);
  }

  // WithIndexData sets data to index slots A & B.
  // Returns ErrSlotOverflow if slotA or slotB value are too big.
  static withIndexData(slotA: ElemBytes, slotB: ElemBytes): ClaimOption {
    return (c: Claim) => c.setIndexData(slotA, slotB);
  }

  // WithIndexDataBytes sets data to index slots A & B.
  // Returns ErrSlotOverflow if slotA or slotB value are too big.
  static withIndexDataBytes(slotA: Uint8Array | null, slotB: Uint8Array | null): ClaimOption {
    return (c: Claim) => c.setIndexDataBytes(slotA, slotB);
  }

  // WithIndexDataInts sets data to index slots A & B.
  // Returns ErrSlotOverflow if slotA or slotB value are too big.
  static withIndexDataInts(slotA: bigint | null, slotB: bigint | null): ClaimOption {
    return (c: Claim) => c.setIndexDataInts(slotA, slotB);
  }

  // WithValueData sets data to value slots A & B.
  // Returns ErrSlotOverflow if slotA or slotB value are too big.
  static withValueData(slotA: ElemBytes, slotB: ElemBytes): ClaimOption {
    return (c: Claim) => c.setValueData(slotA, slotB);
  }

  // WithValueDataBytes sets data to value slots A & B.
  // Returns ErrSlotOverflow if slotA or slotB value are too big.
  static withValueDataBytes(slotA: Uint8Array, slotB: Uint8Array): ClaimOption {
    return (c: Claim) => c.setValueDataBytes(slotA, slotB);
  }

  // WithValueDataInts sets data to value slots A & B.
  // Returns ErrSlotOverflow if slotA or slotB value are too big.
  static withValueDataInts(slotA: bigint | null, slotB: bigint | null): ClaimOption {
    return (c: Claim) => c.setValueDataInts(slotA, slotB);
  }

  // WithIndexMerklizedRoot sets root to index i_2
  // Returns ErrSlotOverflow if root value are too big.
  static withIndexMerklizedRoot(r: bigint): ClaimOption {
    return (c: Claim) => {
      c.setFlagMerklized(MerklizedRootPosition.Index);
      c.index[2] = c.setSlotInt(r, SlotName.IndexA);
    };
  }

  // WithValueMerklizedRoot sets root to value v_2
  // Returns ErrSlotOverflow if root value are too big.
  static withValueMerklizedRoot(r: bigint): ClaimOption {
    return (c: Claim) => {
      c.setFlagMerklized(MerklizedRootPosition.Value);
      c.value[2] = c.setSlotInt(r, SlotName.ValueA);
    };
  }

  // WithMerklizedRoot sets root to value v_2 or index i_2
  // Returns ErrSlotOverflow if root value are too big.
  static withMerklizedRoot(r: bigint, pos: MerklizedRootPosition): ClaimOption {
    return (c: Claim) => {
      switch (pos) {
        case MerklizedRootPosition.Index:
          c.setFlagMerklized(MerklizedRootPosition.Index);
          c.index[2] = c.setSlotInt(r, SlotName.IndexA);
          break;
        case MerklizedRootPosition.Value:
          c.setFlagMerklized(MerklizedRootPosition.Value);
          c.value[2] = c.setSlotInt(r, SlotName.ValueA);
          break;
        default:
          throw Constants.ERRORS.INCORRECT_MERKLIZED_POSITION;
      }
    };
  }
}
