"use strict";
// Copyright 2017-2020 @polkadot/types authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const util_1 = require("@polkadot/util");
const Null_1 = __importDefault(require("../primitive/Null"));
const utils_1 = require("./utils");
const Base_1 = __importDefault(require("./Base"));
const Struct_1 = __importDefault(require("./Struct"));
function extractDef(registry, _def) {
    if (!Array.isArray(_def)) {
        const def = utils_1.mapToTypeMap(registry, _def);
        const isBasic = !Object.values(def).some((type) => type !== Null_1.default);
        return {
            def,
            isBasic
        };
    }
    return {
        def: _def.reduce((def, key) => {
            def[key] = Null_1.default;
            return def;
        }, {}),
        isBasic: true
    };
}
function createFromValue(registry, def, index = 0, value) {
    const Clazz = Object.values(def)[index];
    util_1.assert(!util_1.isUndefined(Clazz), `Unable to create Enum via index ${index}, in ${Object.keys(def).join(', ')}`);
    return {
        index,
        value: new Clazz(registry, value)
    };
}
function decodeFromJSON(registry, def, key, value) {
    // JSON comes in the form of { "<type (lowercased)>": "<value for type>" }, here we
    // additionally force to lower to ensure forward compat
    const keys = Object.keys(def).map((k) => k.toLowerCase());
    const keyLower = key.toLowerCase();
    const index = keys.indexOf(keyLower);
    util_1.assert(index !== -1, `Cannot map Enum JSON, unable to find '${key}' in ${keys.join(', ')}`);
    return createFromValue(registry, def, index, value);
}
function decodeFromString(registry, def, value) {
    return util_1.isHex(value)
        // eslint-disable-next-line @typescript-eslint/no-use-before-define
        ? decodeFromValue(registry, def, util_1.hexToU8a(value))
        : decodeFromJSON(registry, def, value);
}
function decodeFromValue(registry, def, value) {
    if (util_1.isU8a(value)) {
        return createFromValue(registry, def, value[0], value.subarray(1));
    }
    else if (util_1.isNumber(value)) {
        return createFromValue(registry, def, value);
    }
    else if (util_1.isString(value)) {
        return decodeFromString(registry, def, value.toString());
    }
    else if (util_1.isObject(value)) {
        const key = Object.keys(value)[0];
        return decodeFromJSON(registry, def, key, value[key]);
    }
    // Worst-case scenario, return the first with default
    return createFromValue(registry, def, 0);
}
/**
 * @name Enum
 * @description
 * This implements an enum, that based on the value wraps a different type. It is effectively
 * an extension to enum where the value type is determined by the actual index.
 */
// TODO:
//   - As per Enum, actually use TS enum
//   - It should rather probably extend Enum instead of copying code
class Enum extends Base_1.default {
    constructor(registry, def, value, index) {
        const defInfo = extractDef(registry, def);
        const decoded = Enum.decodeEnum(registry, defInfo.def, value, index);
        super(registry, decoded.value);
        this._def = defInfo.def;
        this._isBasic = defInfo.isBasic;
        this._indexes = Object.keys(defInfo.def).map((_, index) => index);
        this._index = this._indexes.indexOf(decoded.index) || 0;
    }
    /** @internal */
    static decodeEnum(registry, def, value, index) {
        // NOTE We check the index path first, before looking at values - this allows treating
        // the optional indexes before anything else, more-specific > less-specific
        if (util_1.isNumber(index)) {
            return createFromValue(registry, def, index, value);
        }
        else if (value instanceof Enum) {
            return createFromValue(registry, def, value._index, value.raw);
        }
        // Or else, we just look at `value`
        return decodeFromValue(registry, def, value);
    }
    static with(Types) {
        return class extends Enum {
            constructor(registry, value, index) {
                super(registry, Types, value, index);
                Object.keys(this._def).forEach((_key) => {
                    const name = util_1.stringUpperFirst(util_1.stringCamelCase(_key.replace(' ', '_')));
                    const askey = `as${name}`;
                    const iskey = `is${name}`;
                    // do not clobber existing properties on the object
                    if (util_1.isUndefined(this[iskey])) {
                        Object.defineProperty(this, iskey, {
                            enumerable: true,
                            get: () => this.type === _key
                        });
                    }
                    if (util_1.isUndefined(this[askey])) {
                        Object.defineProperty(this, askey, {
                            enumerable: true,
                            get: () => {
                                util_1.assert(this[iskey], `Cannot convert '${this.type}' via ${askey}`);
                                return this.value;
                            }
                        });
                    }
                });
            }
        };
    }
    /**
     * @description The length of the value when encoded as a Uint8Array
     */
    get encodedLength() {
        return 1 + this.raw.encodedLength;
    }
    /**
     * @description The index of the metadata value
     */
    get index() {
        return this._index;
    }
    /**
     * @description true if this is a basic enum (no values)
     */
    get isBasic() {
        return this._isBasic;
    }
    /**
     * @description Checks if the Enum points to a [[Null]] type
     */
    get isNone() {
        return this.isNull;
    }
    /**
     * @description Checks if the Enum points to a [[Null]] type (deprecated, use isNone)
     */
    get isNull() {
        return this.raw instanceof Null_1.default;
    }
    /**
     * @description The available keys for this enum
     */
    get defEntries() {
        return Object.keys(this._def);
    }
    /**
     * @description The available keys for this enum
     */
    get defKeys() {
        return Object.keys(this._def);
    }
    /**
     * @description The name of the type this enum value represents
     */
    get type() {
        return this.defKeys[this._index];
    }
    /**
     * @description The value of the enum
     */
    get value() {
        return this.raw;
    }
    /**
     * @description Compares the value of the input to see if there is a match
     */
    eq(other) {
        // cater for the case where we only pass the enum index
        if (util_1.isNumber(other)) {
            return this.toNumber() === other;
        }
        else if (this._isBasic && util_1.isString(other)) {
            return this.type === other;
        }
        else if (util_1.isU8a(other)) {
            return !this.toU8a().some((entry, index) => entry !== other[index]);
        }
        else if (util_1.isHex(other)) {
            return this.toHex() === other;
        }
        else if (other instanceof Enum) {
            return this.index === other.index && this.value.eq(other.value);
        }
        else if (util_1.isObject(other)) {
            return this.value.eq(other[this.type]);
        }
        // compare the actual wrapper value
        return this.value.eq(other);
    }
    /**
     * @description Returns a hex string representation of the value
     */
    toHex() {
        return util_1.u8aToHex(this.toU8a());
    }
    /**
     * @description Converts the Object to JSON, typically used for RPC transfers
     */
    toJSON() {
        return this._isBasic
            ? this.type
            : { [this.type]: this.raw.toJSON() };
    }
    /**
     * @description Returns the number representation for the value
     */
    toNumber() {
        return this._index;
    }
    /**
     * @description Returns a raw struct representation of the enum types
     */
    toRawStruct() {
        return this._isBasic
            ? this.defKeys
            : Struct_1.default.typesToMap(this.registry, this._def);
    }
    /**
     * @description Returns the base runtime type name for this instance
     */
    toRawType() {
        return JSON.stringify({ _enum: this.toRawStruct() });
    }
    /**
     * @description Returns the string representation of the value
     */
    toString() {
        return this.isNull
            ? this.type
            : JSON.stringify(this.toJSON());
    }
    /**
     * @description Encodes the value as a Uint8Array as per the SCALE specifications
     * @param isBare true when the value has none of the type-specific prefixes (internal)
     */
    toU8a(isBare) {
        const index = this._indexes[this._index];
        return util_1.u8aConcat(new Uint8Array([index]), this.raw.toU8a(isBare));
    }
}
exports.default = Enum;
