"use strict";
// Copyright 2017-2020 @polkadot/metadata 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 codec_1 = require("../../../../types/codec");
const util_1 = require("@polkadot/util");
const util_crypto_1 = require("@polkadot/util-crypto");
const getHasher_1 = __importDefault(require("./getHasher"));
const EMPTY_U8A = new Uint8Array([]);
const NULL_HASHER = (value) => value;
// with the prefix, method & options, create both the string & raw keys
/** @internal */
function expandKey({ method, prefix }, options) {
    return options.key
        ? options.key
        : `${prefix} ${method}`;
}
// get the hashers, the base (and  in the case of DoubleMap), the second key
/** @internal */
function getHashers({ meta: { type } }) {
    if (type.isDoubleMap) {
        return [
            getHasher_1.default(type.asDoubleMap.hasher),
            getHasher_1.default(type.asDoubleMap.key2Hasher)
        ];
    }
    else if (type.isMap) {
        return [getHasher_1.default(type.asMap.hasher)];
    }
    // the default
    return [getHasher_1.default()];
}
// create a base prefixed key
/** @internal */
function createPrefixedKey({ method, prefix }) {
    return util_1.u8aConcat(util_crypto_1.xxhashAsU8a(prefix, 128), util_crypto_1.xxhashAsU8a(method, 128));
}
// create a key for a DoubleMap type
/** @internal */
function createKeyDoubleMap(registry, itemFn, stringKey, args, [hasher1, hasher2], metaVersion) {
    const { meta: { name, type } } = itemFn;
    // since we are passing an almost-unknown through, trust, but verify
    util_1.assert(Array.isArray(args) && !util_1.isUndefined(args[0]) && !util_1.isNull(args[0]) && !util_1.isUndefined(args[1]) && !util_1.isNull(args[1]), `${name} is a DoubleMap and requires two arguments`);
    // if this fails, we have bigger issues
    util_1.assert(!util_1.isUndefined(hasher2), '2 hashing functions should be defined for DoubleMaps');
    const [key1, key2] = args;
    const map = type.asDoubleMap;
    const val1 = codec_1.createTypeUnsafe(registry, map.key1.toString(), [key1]).toU8a();
    const val2 = codec_1.createTypeUnsafe(registry, map.key2.toString(), [key2]).toU8a();
    // as per createKey, always add the length prefix (underlying it is Bytes)
    return codec_1.Compact.addLengthPrefix(metaVersion <= 8
        ? util_1.u8aConcat(hasher1(util_1.u8aConcat(util_1.stringToU8a(stringKey), val1)), hasher2(val2))
        : util_1.u8aConcat(createPrefixedKey(itemFn), hasher1(val1), hasher2(val2)));
}
// create a key for either a map or a plain value
/** @internal */
function createKey(registry, itemFn, stringKey, arg, hasher, metaVersion) {
    const { meta: { name, type } } = itemFn;
    let param = EMPTY_U8A;
    if (type.isMap) {
        const map = type.asMap;
        util_1.assert(!util_1.isUndefined(arg) && !util_1.isNull(arg), `${name} is a Map and requires one argument`);
        param = codec_1.createTypeUnsafe(registry, map.key.toString(), [arg]).toU8a();
    }
    // StorageKey is a Bytes, so is length-prefixed
    return codec_1.Compact.addLengthPrefix(metaVersion <= 8
        ? hasher(util_1.u8aConcat(util_1.stringToU8a(stringKey), param))
        : util_1.u8aConcat(createPrefixedKey(itemFn), param.length ? hasher(param) : EMPTY_U8A));
}
// attach the metadata to expand to a StorageFunction
/** @internal */
function expandWithMeta({ meta, method, prefix, section }, storageFn) {
    storageFn.meta = meta;
    storageFn.method = util_1.stringLowerFirst(method);
    storageFn.prefix = prefix;
    storageFn.section = section;
    // explicitly add the actual method in the toJSON, this gets used to determine caching and without it
    // instances (e.g. collective) will not work since it is only matched on param meta
    storageFn.toJSON = () => (Object.assign(Object.assign({}, meta.toJSON()), { storage: { method, prefix, section } }));
    return storageFn;
}
/** @internal */
function extendHeadMeta(registry, { meta: { documentation, name, type }, section }, { method }, iterFn) {
    const outputType = type.isMap
        ? type.asMap.key.toString()
        : type.asDoubleMap.key1.toString();
    // metadata with a fallback value using the type of the key, the normal
    // meta fallback only applies to actual entry values, create one for head
    iterFn.meta = codec_1.createType(registry, 'StorageEntryMetadataLatest', {
        name,
        modifier: codec_1.createType(registry, 'StorageEntryModifierLatest', 1),
        type: codec_1.createType(registry, 'StorageEntryTypeLatest', codec_1.createType(registry, 'PlainTypeLatest', type.isMap ? type.asMap.key : type.asDoubleMap.key1), 0),
        fallback: codec_1.createType(registry, 'Bytes', codec_1.createTypeUnsafe(registry, outputType).toHex()),
        documentation
    });
    return codec_1.createType(registry, 'StorageKey', iterFn, { method, section });
}
// attach the head key hashing for linked maps
/** @internal */
function extendLinkedMap(registry, itemFn, storageFn, stringKey, hasher, metaVersion) {
    const key = metaVersion <= 8
        ? hasher(`head of ${stringKey}`)
        : util_1.u8aConcat(util_crypto_1.xxhashAsU8a(itemFn.prefix, 128), util_crypto_1.xxhashAsU8a(`HeadOf${itemFn.method}`, 128));
    storageFn.iterKey = extendHeadMeta(registry, itemFn, storageFn, () => new codec_1.Raw(registry, key));
    return storageFn;
}
// attach the full list hashing for prefixed maps
/** @internal */
function extendPrefixedMap(registry, itemFn, storageFn) {
    storageFn.iterKey = extendHeadMeta(registry, itemFn, storageFn, () => new codec_1.Raw(registry, createPrefixedKey(itemFn)));
    return storageFn;
}
// attach the full list hashing for double maps
/** @internal */
function extendDoubleMap(registry, itemFn, storageFn) {
    return extendPrefixedMap(registry, itemFn, storageFn);
}
/** @internal */
function createFunction(registry, itemFn, options) {
    const { meta: { type } } = itemFn;
    const stringKey = expandKey(itemFn, options);
    const [hasher, key2Hasher] = getHashers(itemFn);
    // Can only have zero or one argument:
    //   - storage.balances.freeBalance(address)
    //   - storage.timestamp.blockPeriod()
    // For doublemap queries the params is passed in as an tuple, [key1, key2]
    const _storageFn = (arg) => type.isDoubleMap
        ? createKeyDoubleMap(registry, itemFn, stringKey, arg, [hasher, key2Hasher], options.metaVersion)
        : createKey(registry, itemFn, stringKey, arg, options.skipHashing ? NULL_HASHER : hasher, options.metaVersion);
    const storageFn = expandWithMeta(itemFn, _storageFn);
    if (type.isMap) {
        const map = type.asMap;
        if (map.linked.isTrue) {
            extendLinkedMap(registry, itemFn, storageFn, stringKey, hasher, options.metaVersion);
        }
        else {
            extendPrefixedMap(registry, itemFn, storageFn);
        }
    }
    else if (type.isDoubleMap) {
        extendDoubleMap(registry, itemFn, storageFn);
    }
    storageFn.keyPrefix = storageFn.iterKey || util_1.compactStripLength(storageFn())[1];
    return storageFn;
}
exports.default = createFunction;
