import BN from "../../crypto/bn.extension";
import CommonUtils from "../../utils/common.utils";
import ValidationUtils from "../../utils/validation.utils";
import Script from "../script/script";
import type BufferReader from "../../encoding/bufferreader";
import BufferWriter from "../../encoding/bufferwriter";
import type { IOutput } from "./interfaces";
import Address from "../address/address";
import { networks } from "../network/network-manager";
import { AddressType } from "../address/address-formatter";
import GroupToken from "../grouptoken";

export enum  OutputType {
  SATOSCRIPT = 0,
  TEMPLATE = 1,
  INFER = 0x8000 // Not a real type: implies the type should be infered from the script at construction time
}

export default class Output implements IOutput {

  public type: OutputType;

  private _value!: bigint;
  private _scriptPubKey!: Script;

  constructor(value: bigint | number | string, scriptPubKey: Script | string, type = OutputType.INFER) {
    this.type = type;
    this.value = value;
    this.scriptPubKey = scriptPubKey;
  }

  public get value(): bigint {
    return this._value;
  }

  public set value(sats: bigint | number | string) {
    sats = BigInt(sats);
    ValidationUtils.validateArgument(CommonUtils.isNaturalBigInt(sats), "Output value is not a natural bigint");
    this._value = sats;
  }

  public get scriptPubKey(): Script {
    return this._scriptPubKey;
  }

  public set scriptPubKey(script: Script | string) {
    if (this._scriptPubKey != null) {
      this.type = OutputType.INFER;
    }

    if (script instanceof Script) {
      this._scriptPubKey = script;
    } else if (typeof script === 'string') {
      this._scriptPubKey = Script.fromString(script);
    } else {
      throw new TypeError('Invalid argument type: script');
    }

    // in case we replace script or type wasn't passed in c'tor, we need to define the output type
    if (this.type == OutputType.INFER) {
      this.type = this._scriptPubKey.isPublicKeyTemplateOut() || this._scriptPubKey.isScriptTemplateOut()
              ? OutputType.TEMPLATE : OutputType.SATOSCRIPT;
    }
  }

  public get address(): string | undefined {
    if (this._scriptPubKey.isPublicKeyTemplateOut()) {
      return Address.fromScriptTemplate(this._scriptPubKey.getTemplateHash(), this._scriptPubKey.getConstraintHash()).toString();
    } else if (this._scriptPubKey.isPublicKeyHashOut()) {
      return new Address(this._scriptPubKey.getPublicKeyHash(), networks.defaultNetwork, AddressType.PayToPublicKeyHash).toString();
    } else if (this._scriptPubKey.isScriptTemplateOut()) {
      return Address.fromScriptTemplate(this._scriptPubKey.getTemplateHash(), this._scriptPubKey.getConstraintHash(), this._scriptPubKey.getVisibleArgs()).toString();
    }
    return undefined;
  }

  public get groupData(): { groupId: string, groupAmount: string } | undefined {
    try {
      let groupData = this._scriptPubKey.getGroupData();
      if (groupData) {
        return {
          groupId: new Address(groupData.groupId, networks.defaultNetwork, AddressType.GroupIdAddress).toString(),
          groupAmount: GroupToken.getAmountValue(groupData.groupAmount).toString()
        };
      }
    } catch {
      return undefined;
    }
  }

  public invalidValue(): string | false {
    if (this.value > BigInt(Number.MAX_SAFE_INTEGER)) {
      return 'transaction txout value greater than max safe integer';
    }
    if (this.value < 0n) {
      return 'transaction txout negative';
    }
    return false;
  }

  public toObject(): IOutput {
    let group = this.groupData;
    return {
      type: this.type,
      value: this.value.toString(),
      scriptPubKey: this.scriptPubKey.toHex(),
      address: this.address,
      groupId: group?.groupId,
      groupAmount: group?.groupAmount
    };
  }

  public toJSON = this.toObject;
  
  public static fromObject(data: IOutput): Output {
    ValidationUtils.validateArgument(typeof data === 'object' && data !== null, "data", "Unrecognized argument for Output")
    return new Output(data.value, data.scriptPubKey, data.type);
  }

  public inspect(): string {
    return `<Output (type: ${this.type}) (${this.value.toString()} sats) ${this.scriptPubKey.inspect()}>`;
  }

  public static fromBufferReader(br: BufferReader): Output {
    let type = br.readVarintNum();
    let value = br.readUInt64LEBN();
  
    let size = br.readVarintNum();
    let scriptBuf = size !== 0 ? br.read(size) : new Uint8Array();

    return new Output(value.toBigInt(), Script.fromBuffer(scriptBuf), type);
  }

  public toBufferWriter(writer?: BufferWriter): BufferWriter {
    if (!writer) {
      writer = new BufferWriter();
    }
    writer.writeUInt8(this.type);
    writer.writeUInt64LEBN(BN.fromBigInt(this.value));
    writer.writeVarLengthBuf(this.scriptPubKey.toBuffer());
    
    return writer;
  }
}