import { Format } from "@truffle/codec";
import { BN } from "bn.js";
import { Big } from "big.js";

export function safeNativizeVariables(variables: {
  [name: string]: Format.Values.Result;
}): { [name: string]: any } {
  return Object.assign(
    {},
    ...Object.entries(variables).map(([name, value]) => {
      try {
        return { [name]: safeNativize(value) };
      } catch (_) {
        return undefined; //I guess??
      }
    })
  );
}

//HACK! Avoid using!
/**
 * WARNING! Do NOT use this function in real code unless you absolutely have
 * to!  Using it in controlled tests is fine, but do NOT use it in real code if
 * you have any better option!
 *
 * This function is a giant hack.  It will throw exceptions on numbers that
 * don't fit in a Javascript number.  It loses various information.  It was
 * only ever written to support our hacked-together watch expression system,
 * and later repurposed to make testing easier.
 *
 * If you are not doing something as horrible as evaluating user-inputted
 * Javascript expressions meant to operate upon Solidity variables, then you
 * probably have a better option than using this in real code!
 *
 * (For instance, if you just want to nicely print individual values, without
 * attempting to first operate on them via Javascript expressions, we have the
 * [[ResultInspector]] class, which can be used with Node's
 * [util.inspect()](https://nodejs.org/api/util.html#util_util_inspect_object_options)
 * to do exactly that.)
 *
 * Remember, the decoder output format was made to be machine-readable.  It
 * shouldn't be too hard for you to process.  If it comes to it, copy-paste
 * this code and dehackify it for your use case, which hopefully is more
 * manageable than the one that caused us to write this.
 */
export function safeNativize(result: Format.Values.Result): any {
  return safeNativizeWithTable(result, []);
}

function safeNativizeWithTable(
  result: Format.Values.Result,
  seenSoFar: any[]
): any {
  if (result.kind === "error") {
    switch (result.error.kind) {
      case "BoolOutOfRangeError":
        return true;
      default:
        return undefined;
    }
  }
  //NOTE: for simplicity, only arrays & structs will call unsafeNativizeWithTable;
  //other containers will just call unsafeNativize because they can get away with it
  //(only things that can *be* circular need unsafeNativizeWithTable, not things that
  //can merely *contain* circularities)
  switch (result.type.typeClass) {
    case "uint":
    case "int":
      const bnVal = (<Format.Values.UintValue | Format.Values.IntValue>(
        result
      )).value.asBN;
      // change bnVal's prototype to BN
      //@ts-ignore
      bnVal.__proto__ = BN.prototype;
      try {
        return bnVal.toNumber(); //WARNING
      } catch (_) {
        return bnVal.toString();
      }
    case "bool":
      return (<Format.Values.BoolValue>result).value.asBoolean;
    case "bytes":
      return (<Format.Values.BytesValue>result).value.asHex;
    case "address":
      return (<Format.Values.AddressValue>result).value.asAddress;
    case "string": {
      let coercedResult = <Format.Values.StringValue>result;
      switch (coercedResult.value.kind) {
        case "valid":
          return coercedResult.value.asString;
        case "malformed":
          // this will turn malformed utf-8 into replacement characters (U+FFFD) (WARNING)
          // note we need to cut off the 0x prefix
          return Buffer.from(
            coercedResult.value.asHex.slice(2),
            "hex"
          ).toString();
      }
    }
    case "fixed":
    case "ufixed":
      //HACK: Big doesn't have a toNumber() method, so we convert to string and then parse with Number
      //NOTE: we don't bother setting the magic variables Big.NE or Big.PE first, as the choice of
      //notation shouldn't affect the result (can you believe I have to write this? @_@)
      const val = (<Format.Values.FixedValue | Format.Values.UfixedValue>(
        result
      )).value.asBig;
      // change val's prototype to Big
      //@ts-ignore
      val.__proto__ = Big.prototype;
      return Number(val.toString()); //WARNING
    case "array": {
      let coercedResult = <Format.Values.ArrayValue>result;
      if (coercedResult.reference === undefined) {
        //we need to do some pointer stuff here, so let's first create our new
        //object we'll be pointing to
        //[we don't want to alter the original accidentally so let's clone a bit]
        let output: any[] = [...coercedResult.value];
        //now, we can't use a map here, or we'll screw things up!
        //we want to *mutate* output, not replace it with a new object
        for (let index = 0; index < output.length; index++) {
          output[index] = safeNativizeWithTable(output[index], [
            output,
            ...seenSoFar
          ]);
        }
        return output;
      } else {
        return seenSoFar[coercedResult.reference - 1];
      }
    }
    case "userDefinedValueType": {
      return safeNativize(
        (<Format.Values.UserDefinedValueTypeValue>result).value
      );
    }
    case "mapping":
      return Object.assign(
        {},
        ...(<Format.Values.MappingValue>result).value.map(({ key, value }) => ({
          [safeNativize(key).toString()]: safeNativize(value)
        }))
      );
    case "struct": {
      let coercedResult = <Format.Values.StructValue>result;
      if (coercedResult.reference === undefined) {
        //we need to do some pointer stuff here, so let's first create our new
        //object we'll be pointing to
        let output = Object.assign(
          {},
          ...(<Format.Values.StructValue>result).value.map(
            ({ name, value }) => ({
              [name]: value //we *don't* nativize yet!
            })
          )
        );
        //now, we can't use a map here, or we'll screw things up!
        //we want to *mutate* output, not replace it with a new object
        for (let name in output) {
          output[name] = safeNativizeWithTable(output[name], [
            output,
            ...seenSoFar
          ]);
        }
        return output;
      } else {
        return seenSoFar[coercedResult.reference - 1];
      }
    }
    case "type":
      switch (result.type.type.typeClass) {
        case "contract":
          return Object.assign(
            {},
            ...(<Format.Values.TypeValueContract>result).value.map(
              ({ name, value }) => ({
                [name]: safeNativize(value)
              })
            )
          );
        case "enum":
          return Object.assign(
            {},
            ...(<Format.Values.TypeValueEnum>result).value.map(enumValue => ({
              [enumValue.value.name]: safeNativize(enumValue)
            }))
          );
      }
    case "tuple":
      return (<Format.Values.TupleValue>result).value.map(({ value }) =>
        safeNativize(value)
      );
    case "magic":
      return Object.assign(
        {},
        ...Object.entries((<Format.Values.MagicValue>result).value).map(
          ([key, value]) => ({ [key]: safeNativize(value) })
        )
      );
    case "enum":
      return enumFullName(<Format.Values.EnumValue>result);
    case "contract":
      return (<Format.Values.ContractValue>result).value.address; //we no longer include additional info
    case "function":
      switch (result.type.visibility) {
        case "external": {
          let coercedResult = <Format.Values.FunctionExternalValue>result;
          switch (coercedResult.value.kind) {
            case "known":
              return `${coercedResult.value.contract.class.typeName}(${coercedResult.value.contract.address}).${coercedResult.value.abi.name}`;
            case "invalid":
              return `${coercedResult.value.contract.class.typeName}(${coercedResult.value.contract.address}).call(${coercedResult.value.selector}...)`;
            case "unknown":
              return `${coercedResult.value.contract.address}.call(${coercedResult.value.selector}...)`;
          }
        }
        case "internal": {
          let coercedResult = <Format.Values.FunctionInternalValue>result;
          switch (coercedResult.value.kind) {
            case "function":
              if (coercedResult.value.definedIn) {
                return `${coercedResult.value.definedIn.typeName}.${coercedResult.value.name}`;
              } else {
                return coercedResult.value.name;
              }
            case "exception":
              return coercedResult.value.deployedProgramCounter === 0
                ? `<zero>`
                : `<uninitialized>`;
            case "unknown":
              return `<decoding not supported>`;
          }
        }
      }
  }
}

function enumFullName(value: Format.Values.EnumValue): string {
  switch (value.type.kind) {
    case "local":
      return `${value.type.definingContractName}.${value.type.typeName}.${value.value.name}`;
    case "global":
      return `${value.type.typeName}.${value.value.name}`;
  }
}
