"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const lodash_1 = require("lodash");
const binary_type_1 = require("./type/binary.type");
const null_type_1 = require("./type/null.type");
const undefined_type_1 = require("./type/undefined.type");
const BUFFER_TYPES = [
    'Buffer',
    'File',
    'Blob',
    'ArrayBuffer',
    'DataView',
    'Int8Array',
    'Uint8Array',
    'Uint8ClampedArray',
    'Int16Array',
    'Uint16Array',
    'Int32Array',
    'Uint32Array',
    'Float32Array',
    'Float64Array',
];
/**
 * Detects the dynamodb type to which an collection value should be mapped. Empty collections will be mapped to L(ist).
 * Collections of type array where all the values are either String | Number | Binary will be mapped to the corresponding S(et)
 * type. If the item types are heterogeneous or it is a non supported set type the returned type will be L(ist).
 * The logic for collection fo type Set is the same.
 *
 * @param {any[] | Set<any>} collection
 * @returns {AttributeCollectionType}
 */
function detectCollectionType(collection) {
    if (Array.isArray(collection)) {
        if (collection.length) {
            if (collection.every(lodash_1.isString)) {
                return 'SS';
            }
            if (collection.every(lodash_1.isNumber)) {
                return 'NS';
            }
            if (collection.every(isBinary)) {
                return 'BS';
            }
        }
        return 'L';
    }
    else if (isSet(collection)) {
        const firstValueType = collection.size ? detectType(collection.values().next().value) : null;
        let heterogeneous = false;
        for (const item of collection) {
            const type = detectType(item);
            if (type !== firstValueType) {
                heterogeneous = true;
                break;
            }
        }
        if (heterogeneous) {
            return 'L';
        }
        else {
            switch (firstValueType) {
                case 'S':
                    return 'SS';
                case 'N':
                    return 'NS';
                case 'B':
                    return 'BS';
                default:
                    return 'L';
            }
        }
    }
    else {
        throw new Error('given collection was no array or Set -> type could not be detected');
    }
}
exports.detectCollectionType = detectCollectionType;
function isCollection(value) {
    return value && (Array.isArray(value) || isSet(value));
}
exports.isCollection = isCollection;
function isSet(value) {
    return ((value !== null && value !== undefined && value.hasOwnProperty('name') && value.name === 'Set') ||
        value instanceof Set);
}
exports.isSet = isSet;
function detectType(value) {
    if (isCollection(value)) {
        return detectCollectionType(value);
    }
    else {
        if (lodash_1.isString(value)) {
            return 'S';
        }
        if (lodash_1.isNumber(value)) {
            return 'N';
        }
        if (isBinary(value)) {
            return 'B';
        }
        if (value === null) {
            return 'NULL';
        }
        if (typeof value === 'boolean') {
            return 'BOOL';
        }
        if (typeof value === 'object') {
            return 'M';
        }
    }
    throw new Error(`the type for value ${value} could not be detected`);
}
exports.detectType = detectType;
/**
 * Will resolve the type based on given data.
 *
 * @param data
 * @returns {AttributeModelTypeName}
 */
function typeOf(data) {
    if (data === null) {
        return null_type_1.NullType;
    }
    else {
        if (Array.isArray(data)) {
            return Array;
        }
        else if (data instanceof Set) {
            return Set;
        }
        else if (data instanceof Map) {
            return Map;
        }
        else if (isBinary(data)) {
            return binary_type_1.Binary;
        }
        else {
            switch (typeof data) {
                case 'string':
                    return String;
                case 'number':
                    return Number;
                case 'boolean':
                    return Boolean;
                case 'undefined':
                    return undefined_type_1.UndefinedType;
                case 'object':
                    return Object;
            }
        }
    }
    throw new Error(`typeof data ${data} could not be detected`);
}
exports.typeOf = typeOf;
/*
 * copied from https://github.com/aws/aws-sdk-js/blob/0c974a7ff6749a541594de584b43a040978d4b72/lib/dynamodb/types.js
 * should we work with string match
 */
function typeOfFromDb(attributeValue) {
    if (attributeValue) {
        const dynamoType = Object.keys(attributeValue)[0];
        switch (dynamoType) {
            case 'S':
                return String;
            case 'N':
                return Number;
            case 'B':
                return binary_type_1.Binary;
            case 'BOOL':
                return Boolean;
            case 'SS':
            case 'NS':
            case 'BS':
                return Set;
            case 'L':
                return Array;
            case 'M':
                return Object;
            case 'NULL':
                return null_type_1.NullType;
        }
    }
    throw new Error(`could not resolve the dynamo db type for attribute value ${attributeValue}`);
}
exports.typeOfFromDb = typeOfFromDb;
function isBinary(data) {
    if (isNode()) {
        // TODO LOW:BINARY should add || data instanceof Stream
        return Buffer.isBuffer(data);
    }
    else {
        return BUFFER_TYPES.some(type => data !== undefined && data.constructor && (isType(data, type) || typeName(data.constructor) === type));
    }
}
exports.isBinary = isBinary;
function isBufferType(type) {
    return BUFFER_TYPES.includes(type);
}
exports.isBufferType = isBufferType;
/*
 * copied from https://github.com/aws/aws-sdk-js/blob/0c974a7ff6749a541594de584b43a040978d4b72/lib/js
 */
function isType(obj, type) {
    // handle cross-"frame" objects
    if (typeof type === 'function') {
        type = typeName(type);
    }
    return Object.prototype.toString.call(obj) === '[object ' + type + ']';
}
exports.isType = isType;
function isBrowser() {
    return process && process.browser;
}
exports.isBrowser = isBrowser;
function isNode() {
    return !isBrowser();
}
exports.isNode = isNode;
/**
 * Returns the name of the given Type. null and undefined are special cases were we return 'Null' vs. 'Undefined'
 * @param type
 * @returns {string}
 */
function typeName(type) {
    if (type !== null && type !== undefined) {
        if (Object.prototype.hasOwnProperty.call(type, 'name')) {
            return type.name;
        }
        else {
            const str = type.toString();
            const match = str.match(/^\s*function (.+)\(/);
            return match ? match[1] : str;
        }
    }
    else {
        if (type === null) {
            return 'Null';
        }
        else if (type === undefined) {
            return 'Undefined';
        }
    }
    throw new Error(`was not able to resolve type name for type ${type}`);
}
exports.typeName = typeName;
//# sourceMappingURL=util.js.map