// Copyright 2017-2025 @pezkuwi/types-create authors & contributors
// SPDX-License-Identifier: Apache-2.0

import type { AnyString } from '@pezkuwi/types-codec/types';
import type { TypeDef } from '@pezkuwi/types-create/types';

import { sanitize } from '@pezkuwi/types-codec';
import { isNumber, isString, objectSpread, stringify } from '@pezkuwi/util';

import { TypeDefInfo } from '../types/index.js';
import { typeSplit } from './typeSplit.js';

interface TypeDefOptions {
  name?: string;
  displayName?: string;
}

interface SetDetails {
  _bitLength: number;
  index: number;

  [key: string]: number;
}

interface ParsedDef {
  _alias: string;
  _enum?: string[];
  _fallback?: string;
  _set?: SetDetails;

  [key: string]: unknown;
}

const KNOWN_INTERNALS = ['_alias', '_fallback'];

function getTypeString (typeOrObj: any): string {
  return isString(typeOrObj)
    ? typeOrObj.toString()
    : stringify(typeOrObj);
}

function isRustEnum (details: Record<string, string> | Record<string, number>): details is Record<string, string> {
  const values = Object.values(details);

  if (values.some((v) => isNumber(v))) {
    if (!values.every((v) => isNumber(v) && v >= 0 && v <= 255)) {
      throw new Error('Invalid number-indexed enum definition');
    }

    return false;
  }

  return true;
}

// decode an enum of either of the following forms
//  { _enum: ['A', 'B', 'C'] }
//  { _enum: { A: AccountId, B: Balance, C: u32 } }
//  { _enum: { A: 1, B: 2 } }
function _decodeEnum (value: TypeDef, details: string[] | Record<string, string> | Record<string, number>, count: number, fallbackType?: string): TypeDef {
  value.info = TypeDefInfo.Enum;
  value.fallbackType = fallbackType;

  // not as pretty, but remain compatible with oo7 for both struct and Array types
  if (Array.isArray(details)) {
    value.sub = details.map((name, index): TypeDef => ({
      index,
      info: TypeDefInfo.Plain,
      name,
      type: 'Null'
    }));
  } else if (isRustEnum(details)) {
    value.sub = Object.entries(details).map(([name, typeOrObj], index): TypeDef =>
      objectSpread({}, getTypeDef(getTypeString(typeOrObj || 'Null'), { name }, count), { index })
    );
  } else {
    value.sub = Object.entries(details).map(([name, index]): TypeDef => ({
      index,
      info: TypeDefInfo.Plain,
      name,
      type: 'Null'
    }));
  }

  return value;
}

// decode a set of the form
//   { _set: { A: 0b0001, B: 0b0010, C: 0b0100 } }
function _decodeSet (value: TypeDef, details: SetDetails, fallbackType: string | undefined): TypeDef {
  value.info = TypeDefInfo.Set;
  value.fallbackType = fallbackType;
  value.length = details._bitLength;
  value.sub = Object
    .entries(details)
    .filter(([name]): boolean => !name.startsWith('_'))
    .map(([name, index]): TypeDef => ({
      index,
      info: TypeDefInfo.Plain,
      name,
      type: 'Null'
    }));

  return value;
}

// decode a struct, set or enum
function _decodeStruct (value: TypeDef, type: string, _: string, count: number): TypeDef {
  const parsed = JSON.parse(type) as ParsedDef;
  const keys = Object.keys(parsed);

  if (parsed._enum) {
    return _decodeEnum(value, parsed._enum, count, parsed._fallback);
  } else if (parsed._set) {
    return _decodeSet(value, parsed._set, parsed._fallback);
  }

  value.alias = parsed._alias
    ? new Map(Object.entries(parsed._alias))
    : undefined;
  value.fallbackType = parsed._fallback;
  value.sub = keys
    .filter((name) => !KNOWN_INTERNALS.includes(name))
    .map((name) =>
      getTypeDef(getTypeString(parsed[name]), { name }, count)
    );

  return value;
}

// decode a fixed vector, e.g. [u8;32]
function _decodeFixedVec (value: TypeDef, type: string, _: string, count: number): TypeDef {
  const max = type.length - 1;
  let index = -1;
  let inner = 0;

  for (let i = 1; (i < max) && (index === -1); i++) {
    switch (type[i]) {
      case ';': {
        if (inner === 0) {
          index = i;
        }

        break;
      }

      case '[':
      case '(':
      case '<':
        inner++;
        break;

      case ']':
      case ')':
      case '>':
        inner--;
        break;
    }
  }

  if (index === -1) {
    throw new Error(`${type}: Unable to extract location of ';'`);
  }

  const vecType = type.substring(1, index);
  const [strLength, displayName] = type.substring(index + 1, max).split(';');
  const length = parseInt(strLength.trim(), 10);

  if (length > 2048) {
    throw new Error(`${type}: Only support for [Type; <length>], where length <= 2048`);
  }

  value.displayName = displayName;
  value.length = length;
  value.sub = getTypeDef(vecType, {}, count);

  return value;
}

// decode a tuple
function _decodeTuple (value: TypeDef, _: string, subType: string, count: number): TypeDef {
  value.sub = subType.length === 0
    ? []
    : typeSplit(subType).map((inner) => getTypeDef(inner, {}, count));

  return value;
}

// decode a Int/UInt<bitLength[, name]>
function _decodeAnyInt (value: TypeDef, type: string, _: string, clazz: 'Int' | 'UInt'): TypeDef {
  const [strLength, displayName] = type.substring(clazz.length + 1, type.length - 1).split(',');
  const length = parseInt(strLength.trim(), 10);

  if ((length > 8192) || (length % 8)) {
    throw new Error(`${type}: Only support for ${clazz}<bitLength>, where length <= 8192 and a power of 8, found ${length}`);
  }

  value.displayName = displayName;
  value.length = length;

  return value;
}

function _decodeInt (value: TypeDef, type: string, subType: string): TypeDef {
  return _decodeAnyInt(value, type, subType, 'Int');
}

function _decodeUInt (value: TypeDef, type: string, subType: string): TypeDef {
  return _decodeAnyInt(value, type, subType, 'UInt');
}

function _decodeDoNotConstruct (value: TypeDef, type: string, _: string): TypeDef {
  const NAME_LENGTH = 'DoNotConstruct'.length;

  value.displayName = type.substring(NAME_LENGTH + 1, type.length - 1);

  return value;
}

function hasWrapper (type: string, [start, end]: [string, string, TypeDefInfo, any?]): boolean {
  return (type.startsWith(start)) && (type.slice(-1 * end.length) === end);
}

const nestedExtraction: [string, string, TypeDefInfo, (value: TypeDef, type: string, subType: string, count: number) => TypeDef][] = [
  ['[', ']', TypeDefInfo.VecFixed, _decodeFixedVec],
  ['{', '}', TypeDefInfo.Struct, _decodeStruct],
  ['(', ')', TypeDefInfo.Tuple, _decodeTuple],
  // the inner for these are the same as tuple, multiple values
  ['BTreeMap<', '>', TypeDefInfo.BTreeMap, _decodeTuple],
  ['HashMap<', '>', TypeDefInfo.HashMap, _decodeTuple],
  ['Int<', '>', TypeDefInfo.Int, _decodeInt],
  ['Result<', '>', TypeDefInfo.Result, _decodeTuple],
  ['UInt<', '>', TypeDefInfo.UInt, _decodeUInt],
  ['DoNotConstruct<', '>', TypeDefInfo.DoNotConstruct, _decodeDoNotConstruct]
];

const wrappedExtraction: [string, string, TypeDefInfo][] = [
  ['BTreeSet<', '>', TypeDefInfo.BTreeSet],
  ['Compact<', '>', TypeDefInfo.Compact],
  ['Linkage<', '>', TypeDefInfo.Linkage],
  ['Opaque<', '>', TypeDefInfo.WrapperOpaque],
  ['Option<', '>', TypeDefInfo.Option],
  ['Range<', '>', TypeDefInfo.Range],
  ['RangeInclusive<', '>', TypeDefInfo.RangeInclusive],
  ['Vec<', '>', TypeDefInfo.Vec],
  ['WrapperKeepOpaque<', '>', TypeDefInfo.WrapperKeepOpaque],
  ['WrapperOpaque<', '>', TypeDefInfo.WrapperOpaque]
];

function extractSubType (type: string, [start, end]: [string, string, TypeDefInfo, any?]): string {
  return type.substring(start.length, type.length - end.length);
}

export function getTypeDef (_type: AnyString, { displayName, name }: TypeDefOptions = {}, count = 0): TypeDef {
  // create the type via Type, allowing types to be sanitized
  const type = sanitize(_type);
  const value: TypeDef = { displayName, info: TypeDefInfo.Plain, name, type };

  if (++count > 64) {
    throw new Error('getTypeDef: Maximum nested limit reached');
  }

  const nested = nestedExtraction.find((nested) => hasWrapper(type, nested));

  if (nested) {
    value.info = nested[2];

    return nested[3](value, type, extractSubType(type, nested), count);
  }

  const wrapped = wrappedExtraction.find((wrapped) => hasWrapper(type, wrapped));

  if (wrapped) {
    value.info = wrapped[2];
    value.sub = getTypeDef(extractSubType(type, wrapped), {}, count);
  }

  return value;
}
