import { cloneDeep } from './utils';
import {
  ClarityValue,
  uintCV,
  intCV,
  contractPrincipalCV,
  standardPrincipalCV,
  noneCV,
  bufferCV,
  falseCV,
  trueCV,
  ClarityType,
  getCVTypeString,
  bufferCVFromString,
} from './clarity';
import { ContractCallPayload } from './payload';
import { NotImplementedError } from './errors';
import { stringAsciiCV, stringUtf8CV } from './clarity/types/stringCV';
import { utf8ToBytes } from '@stacks/common';

// From https://github.com/blockstack/stacks-blockchain-sidecar/blob/master/src/event-stream/contract-abi.ts

export type ClarityAbiTypeBuffer = { buffer: { length: number } };
export type ClarityAbiTypeStringAscii = { 'string-ascii': { length: number } };
export type ClarityAbiTypeStringUtf8 = { 'string-utf8': { length: number } };
export type ClarityAbiTypeResponse = { response: { ok: ClarityAbiType; error: ClarityAbiType } };
export type ClarityAbiTypeOptional = { optional: ClarityAbiType };
export type ClarityAbiTypeTuple = { tuple: { name: string; type: ClarityAbiType }[] };
export type ClarityAbiTypeList = { list: { type: ClarityAbiType; length: number } };

export type ClarityAbiTypeUInt128 = 'uint128';
export type ClarityAbiTypeInt128 = 'int128';
export type ClarityAbiTypeBool = 'bool';
export type ClarityAbiTypePrincipal = 'principal';
export type ClarityAbiTypeTraitReference = 'trait_reference';
export type ClarityAbiTypeNone = 'none';

export type ClarityAbiTypePrimitive =
  | ClarityAbiTypeUInt128
  | ClarityAbiTypeInt128
  | ClarityAbiTypeBool
  | ClarityAbiTypePrincipal
  | ClarityAbiTypeTraitReference
  | ClarityAbiTypeNone;

export type ClarityAbiType =
  | ClarityAbiTypePrimitive
  | ClarityAbiTypeBuffer
  | ClarityAbiTypeResponse
  | ClarityAbiTypeOptional
  | ClarityAbiTypeTuple
  | ClarityAbiTypeList
  | ClarityAbiTypeStringAscii
  | ClarityAbiTypeStringUtf8
  | ClarityAbiTypeTraitReference;

export enum ClarityAbiTypeId {
  ClarityAbiTypeUInt128 = 1,
  ClarityAbiTypeInt128 = 2,
  ClarityAbiTypeBool = 3,
  ClarityAbiTypePrincipal = 4,
  ClarityAbiTypeNone = 5,
  ClarityAbiTypeBuffer = 6,
  ClarityAbiTypeResponse = 7,
  ClarityAbiTypeOptional = 8,
  ClarityAbiTypeTuple = 9,
  ClarityAbiTypeList = 10,
  ClarityAbiTypeStringAscii = 11,
  ClarityAbiTypeStringUtf8 = 12,
  ClarityAbiTypeTraitReference = 13,
}

export const isClarityAbiPrimitive = (val: ClarityAbiType): val is ClarityAbiTypePrimitive =>
  typeof val === 'string';
export const isClarityAbiBuffer = (val: ClarityAbiType): val is ClarityAbiTypeBuffer =>
  (val as ClarityAbiTypeBuffer).buffer !== undefined;
export const isClarityAbiStringAscii = (val: ClarityAbiType): val is ClarityAbiTypeStringAscii =>
  (val as ClarityAbiTypeStringAscii)['string-ascii'] !== undefined;
export const isClarityAbiStringUtf8 = (val: ClarityAbiType): val is ClarityAbiTypeStringUtf8 =>
  (val as ClarityAbiTypeStringUtf8)['string-utf8'] !== undefined;
export const isClarityAbiResponse = (val: ClarityAbiType): val is ClarityAbiTypeResponse =>
  (val as ClarityAbiTypeResponse).response !== undefined;
export const isClarityAbiOptional = (val: ClarityAbiType): val is ClarityAbiTypeOptional =>
  (val as ClarityAbiTypeOptional).optional !== undefined;
export const isClarityAbiTuple = (val: ClarityAbiType): val is ClarityAbiTypeTuple =>
  (val as ClarityAbiTypeTuple).tuple !== undefined;
export const isClarityAbiList = (val: ClarityAbiType): val is ClarityAbiTypeList =>
  (val as ClarityAbiTypeList).list !== undefined;

export type ClarityAbiTypeUnion =
  | { id: ClarityAbiTypeId.ClarityAbiTypeUInt128; type: ClarityAbiTypeUInt128 }
  | { id: ClarityAbiTypeId.ClarityAbiTypeInt128; type: ClarityAbiTypeInt128 }
  | { id: ClarityAbiTypeId.ClarityAbiTypeBool; type: ClarityAbiTypeBool }
  | { id: ClarityAbiTypeId.ClarityAbiTypePrincipal; type: ClarityAbiTypePrincipal }
  | { id: ClarityAbiTypeId.ClarityAbiTypeTraitReference; type: ClarityAbiTypeTraitReference }
  | { id: ClarityAbiTypeId.ClarityAbiTypeNone; type: ClarityAbiTypeNone }
  | { id: ClarityAbiTypeId.ClarityAbiTypeBuffer; type: ClarityAbiTypeBuffer }
  | { id: ClarityAbiTypeId.ClarityAbiTypeResponse; type: ClarityAbiTypeResponse }
  | { id: ClarityAbiTypeId.ClarityAbiTypeOptional; type: ClarityAbiTypeOptional }
  | { id: ClarityAbiTypeId.ClarityAbiTypeTuple; type: ClarityAbiTypeTuple }
  | { id: ClarityAbiTypeId.ClarityAbiTypeList; type: ClarityAbiTypeList }
  | { id: ClarityAbiTypeId.ClarityAbiTypeStringAscii; type: ClarityAbiTypeStringAscii }
  | { id: ClarityAbiTypeId.ClarityAbiTypeStringUtf8; type: ClarityAbiTypeStringUtf8 };

export function getTypeUnion(val: ClarityAbiType): ClarityAbiTypeUnion {
  if (isClarityAbiPrimitive(val)) {
    if (val === 'uint128') {
      return { id: ClarityAbiTypeId.ClarityAbiTypeUInt128, type: val };
    } else if (val === 'int128') {
      return { id: ClarityAbiTypeId.ClarityAbiTypeInt128, type: val };
    } else if (val === 'bool') {
      return { id: ClarityAbiTypeId.ClarityAbiTypeBool, type: val };
    } else if (val === 'principal') {
      return { id: ClarityAbiTypeId.ClarityAbiTypePrincipal, type: val };
    } else if (val === 'trait_reference') {
      return { id: ClarityAbiTypeId.ClarityAbiTypeTraitReference, type: val };
    } else if (val === 'none') {
      return { id: ClarityAbiTypeId.ClarityAbiTypeNone, type: val };
    } else {
      throw new Error(`Unexpected Clarity ABI type primitive: ${JSON.stringify(val)}`);
    }
  } else if (isClarityAbiBuffer(val)) {
    return { id: ClarityAbiTypeId.ClarityAbiTypeBuffer, type: val };
  } else if (isClarityAbiResponse(val)) {
    return { id: ClarityAbiTypeId.ClarityAbiTypeResponse, type: val };
  } else if (isClarityAbiOptional(val)) {
    return { id: ClarityAbiTypeId.ClarityAbiTypeOptional, type: val };
  } else if (isClarityAbiTuple(val)) {
    return { id: ClarityAbiTypeId.ClarityAbiTypeTuple, type: val };
  } else if (isClarityAbiList(val)) {
    return { id: ClarityAbiTypeId.ClarityAbiTypeList, type: val };
  } else if (isClarityAbiStringAscii(val)) {
    return { id: ClarityAbiTypeId.ClarityAbiTypeStringAscii, type: val };
  } else if (isClarityAbiStringUtf8(val)) {
    return { id: ClarityAbiTypeId.ClarityAbiTypeStringUtf8, type: val };
  } else {
    throw new Error(`Unexpected Clarity ABI type: ${JSON.stringify(val)}`);
  }
}

function encodeClarityValue(type: ClarityAbiType, val: string): ClarityValue;
function encodeClarityValue(type: ClarityAbiTypeUnion, val: string): ClarityValue;
function encodeClarityValue(
  input: ClarityAbiTypeUnion | ClarityAbiType,
  val: string
): ClarityValue {
  let union: ClarityAbiTypeUnion;
  if ((input as ClarityAbiTypeUnion).id !== undefined) {
    union = input as ClarityAbiTypeUnion;
  } else {
    union = getTypeUnion(input as ClarityAbiType);
  }
  switch (union.id) {
    case ClarityAbiTypeId.ClarityAbiTypeUInt128:
      return uintCV(val);
    case ClarityAbiTypeId.ClarityAbiTypeInt128:
      return intCV(val);
    case ClarityAbiTypeId.ClarityAbiTypeBool:
      if (val === 'false' || val === '0') return falseCV();
      else if (val === 'true' || val === '1') return trueCV();
      else throw new Error(`Unexpected Clarity bool value: ${JSON.stringify(val)}`);
    case ClarityAbiTypeId.ClarityAbiTypePrincipal:
      if (val.includes('.')) {
        const [addr, name] = val.split('.');
        return contractPrincipalCV(addr, name);
      } else {
        return standardPrincipalCV(val);
      }
    case ClarityAbiTypeId.ClarityAbiTypeTraitReference:
      const [addr, name] = val.split('.');
      return contractPrincipalCV(addr, name);
    case ClarityAbiTypeId.ClarityAbiTypeNone:
      return noneCV();
    case ClarityAbiTypeId.ClarityAbiTypeBuffer:
      return bufferCV(utf8ToBytes(val));
    case ClarityAbiTypeId.ClarityAbiTypeStringAscii:
      return stringAsciiCV(val);
    case ClarityAbiTypeId.ClarityAbiTypeStringUtf8:
      return stringUtf8CV(val);
    case ClarityAbiTypeId.ClarityAbiTypeResponse:
      throw new NotImplementedError(`Unsupported encoding for Clarity type: ${union.id}`);
    case ClarityAbiTypeId.ClarityAbiTypeOptional:
      throw new NotImplementedError(`Unsupported encoding for Clarity type: ${union.id}`);
    case ClarityAbiTypeId.ClarityAbiTypeTuple:
      throw new NotImplementedError(`Unsupported encoding for Clarity type: ${union.id}`);
    case ClarityAbiTypeId.ClarityAbiTypeList:
      throw new NotImplementedError(`Unsupported encoding for Clarity type: ${union.id}`);
    default:
      throw new Error(`Unexpected Clarity type ID: ${JSON.stringify(union)}`);
  }
}
export { encodeClarityValue };

export function getTypeString(val: ClarityAbiType): string {
  if (isClarityAbiPrimitive(val)) {
    if (val === 'int128') {
      return 'int';
    } else if (val === 'uint128') {
      return 'uint';
    }
    return val;
  } else if (isClarityAbiBuffer(val)) {
    return `(buff ${val.buffer.length})`;
  } else if (isClarityAbiStringAscii(val)) {
    return `(string-ascii ${val['string-ascii'].length})`;
  } else if (isClarityAbiStringUtf8(val)) {
    return `(string-utf8 ${val['string-utf8'].length})`;
  } else if (isClarityAbiResponse(val)) {
    return `(response ${getTypeString(val.response.ok)} ${getTypeString(val.response.error)})`;
  } else if (isClarityAbiOptional(val)) {
    return `(optional ${getTypeString(val.optional)})`;
  } else if (isClarityAbiTuple(val)) {
    return `(tuple ${val.tuple.map(t => `(${t.name} ${getTypeString(t.type)})`).join(' ')})`;
  } else if (isClarityAbiList(val)) {
    return `(list ${val.list.length} ${getTypeString(val.list.type)})`;
  } else {
    throw new Error(`Type string unsupported for Clarity type: ${JSON.stringify(val)}`);
  }
}

export interface ClarityAbiFunction {
  name: string;
  access: 'private' | 'public' | 'read_only';
  args: {
    name: string;
    type: ClarityAbiType;
  }[];
  outputs: {
    type: ClarityAbiType;
  };
}

export function abiFunctionToString(func: ClarityAbiFunction): string {
  const access = func.access === 'read_only' ? 'read-only' : func.access;
  return `(define-${access} (${func.name} ${func.args
    .map(arg => `(${arg.name} ${getTypeString(arg.type)})`)
    .join(' ')}))`;
}

export interface ClarityAbiVariable {
  name: string;
  access: 'variable' | 'constant';
  type: ClarityAbiType;
}

export interface ClarityAbiMap {
  name: string;
  key: {
    name: string;
    type: ClarityAbiType;
  }[];
  value: {
    name: string;
    type: ClarityAbiType;
  }[];
}

export interface ClarityAbiTypeFungibleToken {
  name: string;
}

export interface ClarityAbiTypeNonFungibleToken {
  name: string;
  type: ClarityAbiType;
}

export interface ClarityAbi {
  functions: ClarityAbiFunction[];
  variables: ClarityAbiVariable[];
  maps: ClarityAbiMap[];
  fungible_tokens: ClarityAbiTypeFungibleToken[];
  non_fungible_tokens: ClarityAbiTypeNonFungibleToken[];
}

function matchType(cv: ClarityValue, abiType: ClarityAbiType): boolean {
  const union = getTypeUnion(abiType);

  switch (cv.type) {
    case ClarityType.BoolTrue:
    case ClarityType.BoolFalse:
      return union.id === ClarityAbiTypeId.ClarityAbiTypeBool;
    case ClarityType.Int:
      return union.id === ClarityAbiTypeId.ClarityAbiTypeInt128;
    case ClarityType.UInt:
      return union.id === ClarityAbiTypeId.ClarityAbiTypeUInt128;
    case ClarityType.Buffer:
      return (
        union.id === ClarityAbiTypeId.ClarityAbiTypeBuffer &&
        union.type.buffer.length >= cv.buffer.length
      );
    case ClarityType.StringASCII:
      return (
        union.id === ClarityAbiTypeId.ClarityAbiTypeStringAscii &&
        union.type['string-ascii'].length >= cv.data.length
      );
    case ClarityType.StringUTF8:
      return (
        union.id === ClarityAbiTypeId.ClarityAbiTypeStringUtf8 &&
        union.type['string-utf8'].length >= cv.data.length
      );
    case ClarityType.OptionalNone:
      return (
        union.id === ClarityAbiTypeId.ClarityAbiTypeNone ||
        union.id === ClarityAbiTypeId.ClarityAbiTypeOptional
      );
    case ClarityType.OptionalSome:
      return (
        union.id === ClarityAbiTypeId.ClarityAbiTypeOptional &&
        matchType(cv.value, union.type.optional)
      );
    case ClarityType.ResponseErr:
      return (
        union.id === ClarityAbiTypeId.ClarityAbiTypeResponse &&
        matchType(cv.value, union.type.response.error)
      );
    case ClarityType.ResponseOk:
      return (
        union.id === ClarityAbiTypeId.ClarityAbiTypeResponse &&
        matchType(cv.value, union.type.response.ok)
      );
    case ClarityType.PrincipalContract:
      return (
        union.id === ClarityAbiTypeId.ClarityAbiTypePrincipal ||
        union.id === ClarityAbiTypeId.ClarityAbiTypeTraitReference
      );
    case ClarityType.PrincipalStandard:
      return union.id === ClarityAbiTypeId.ClarityAbiTypePrincipal;
    case ClarityType.List:
      return (
        union.id == ClarityAbiTypeId.ClarityAbiTypeList &&
        union.type.list.length >= cv.list.length &&
        cv.list.every(val => matchType(val, union.type.list.type))
      );
    case ClarityType.Tuple:
      if (union.id == ClarityAbiTypeId.ClarityAbiTypeTuple) {
        const tuple = cloneDeep(cv.data);
        for (let i = 0; i < union.type.tuple.length; i++) {
          const abiTupleEntry = union.type.tuple[i];
          const key = abiTupleEntry.name;
          const val = tuple[key];

          // if key exists in cv tuple, check if its type matches the abi
          // return false if key doesn't exist
          if (val) {
            if (!matchType(val, abiTupleEntry.type)) {
              return false;
            }
            delete tuple[key];
          } else {
            return false;
          }
        }
        return true;
      } else {
        return false;
      }
    default:
      return false;
  }
}

/**
 * Validates a contract-call payload with a contract ABI
 *
 * @param {ContractCallPayload} payload - a contract-call payload
 * @param {ClarityAbi} abi - a contract ABI
 *
 * @returns {boolean} true if the payloads functionArgs type check against those in the ABI
 */
export function validateContractCall(payload: ContractCallPayload, abi: ClarityAbi): boolean {
  const filtered = abi.functions.filter(fn => fn.name === payload.functionName.content);
  if (filtered.length === 1) {
    const abiFunc = filtered[0];
    const abiArgs = abiFunc.args;

    if (payload.functionArgs.length !== abiArgs.length) {
      throw new Error(
        `Clarity function expects ${abiArgs.length} argument(s) but received ${payload.functionArgs.length}`
      );
    }

    for (let i = 0; i < payload.functionArgs.length; i++) {
      const payloadArg = payload.functionArgs[i];
      const abiArg = abiArgs[i];

      if (!matchType(payloadArg, abiArg.type)) {
        const argNum = i + 1;
        throw new Error(
          `Clarity function \`${
            payload.functionName.content
          }\` expects argument ${argNum} to be of type ${getTypeString(
            abiArg.type
          )}, not ${getCVTypeString(payloadArg)}`
        );
      }
    }

    return true;
  } else if (filtered.length === 0) {
    throw new Error(`ABI doesn't contain a function with the name ${payload.functionName.content}`);
  } else {
    throw new Error(
      `Malformed ABI. Contains multiple functions with the name ${payload.functionName.content}`
    );
  }
}

/**
 * Convert string input to Clarity value based on contract ABI data. Only handles Clarity
 * primitives and buffers. Responses, optionals, tuples and lists are not supported.
 *
 * @param {string} input - string to be parsed into Clarity value
 * @param {ClarityAbiType} type - the contract function argument object
 *
 * @returns {ClarityValue} returns a Clarity value
 */
export function parseToCV(input: string, type: ClarityAbiType): ClarityValue {
  const typeString = getTypeString(type);
  if (isClarityAbiPrimitive(type)) {
    if (type === 'uint128') {
      return uintCV(input);
    } else if (type === 'int128') {
      return intCV(input);
    } else if (type === 'bool') {
      if (input.toLowerCase() === 'true') {
        return trueCV();
      } else if (input.toLowerCase() === 'false') {
        return falseCV();
      } else {
        throw new Error(`Invalid bool value: ${input}`);
      }
    } else if (type === 'principal') {
      if (input.includes('.')) {
        const [address, contractName] = input.split('.');
        return contractPrincipalCV(address, contractName);
      } else {
        return standardPrincipalCV(input);
      }
    } else {
      throw new Error(`Contract function contains unsupported Clarity ABI type: ${typeString}`);
    }
  } else if (isClarityAbiBuffer(type)) {
    const inputLength = utf8ToBytes(input).byteLength;
    if (inputLength > type.buffer.length) {
      throw new Error(`Input exceeds specified buffer length limit of ${type.buffer.length}`);
    }
    return bufferCVFromString(input);
  } else if (isClarityAbiResponse(type)) {
    throw new Error(`Contract function contains unsupported Clarity ABI type: ${typeString}`);
  } else if (isClarityAbiOptional(type)) {
    throw new Error(`Contract function contains unsupported Clarity ABI type: ${typeString}`);
  } else if (isClarityAbiTuple(type)) {
    throw new Error(`Contract function contains unsupported Clarity ABI type: ${typeString}`);
  } else if (isClarityAbiList(type)) {
    throw new Error(`Contract function contains unsupported Clarity ABI type: ${typeString}`);
  } else {
    throw new Error(`Contract function contains unsupported Clarity ABI type: ${typeString}`);
  }
}
