"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const metadata_helper_1 = require("../decorator/metadata/metadata-helper");
const boolean_mapper_1 = require("./for-type/boolean.mapper");
const collection_mapper_1 = require("./for-type/collection.mapper");
const enum_mapper_1 = require("./for-type/enum.mapper");
const null_mapper_1 = require("./for-type/null.mapper");
const number_mapper_1 = require("./for-type/number.mapper");
const object_mapper_1 = require("./for-type/object.mapper");
const string_mapper_1 = require("./for-type/string.mapper");
const binary_type_1 = require("./type/binary.type");
const enum_type_1 = require("./type/enum.type");
const null_type_1 = require("./type/null.type");
const undefined_type_1 = require("./type/undefined.type");
const util_1 = require("./util");
const mapperForType = new Map();
function toDb(item, modelConstructor) {
    const mapped = {};
    if (modelConstructor) {
        const metadata = metadata_helper_1.metadataForClass(modelConstructor);
        /*
         * initialize possible properties with auto generated uuid
         */
        if (metadata) {
            metadata.getKeysWithUUID().forEach(propertyMetadata => {
                if (!Reflect.get(item, propertyMetadata.name)) {
                    Reflect.set(item, propertyMetadata.name, util_1.uuidv4());
                }
            });
        }
    }
    const propertyNames = Object.getOwnPropertyNames(item) || [];
    propertyNames.forEach(propertyKey => {
        /*
         * 1) get the value of the property
         */
        const propertyValue = getPropertyValue(item, propertyKey);
        let attributeValue;
        // TODO concept maybe make this configurable how to map undefined & null values
        if (propertyValue === undefined || propertyValue === null) {
            // noop ignore because we can't map it
        }
        else {
            /*
             * 2) decide how to map the property depending on type or value
             */
            let propertyMetadata;
            if (modelConstructor) {
                propertyMetadata = metadata_helper_1.metadataForProperty(modelConstructor, propertyKey);
            }
            if (propertyMetadata) {
                if (propertyMetadata.transient) {
                    // skip transient property
                    // logger.log('transient property -> skip')
                }
                else {
                    /*
                     * 3a) property metadata is defined
                     */
                    attributeValue = toDbOne(propertyValue, propertyMetadata);
                }
            }
            else {
                /*
                 * 3b) no metadata found
                 */
                // let typeByConvention = typeByConvention(propertyKey);
                // if (typeByConvention) {
                /*
                 * 4a) matches a convention
                 */
                // attributeValue = mapperForConvention(typeByConvention).toDb(propertyValue);
                // } else {
                //   /*
                //    * 4b) no naming convention matches
                //    */
                attributeValue = toDbOne(propertyValue);
                // }
            }
            if (attributeValue === undefined) {
                // no-op transient field, just ignore it
            }
            else if (attributeValue === null) {
                // empty values (string, set, list) will be ignored too
            }
            else {
                ;
                mapped[propertyMetadata ? propertyMetadata.nameDb : propertyKey] = attributeValue;
            }
        }
    });
    return mapped;
}
exports.toDb = toDb;
function toDbOne(propertyValue, propertyMetadata) {
    const explicitType = propertyMetadata && propertyMetadata.typeInfo && propertyMetadata.typeInfo.isCustom
        ? propertyMetadata.typeInfo.type
        : null;
    const type = explicitType || util_1.typeOf(propertyValue);
    const mapper = propertyMetadata && propertyMetadata.mapper ? propertyMetadata.mapper() : forType(type);
    const attrValue = explicitType
        ? mapper.toDb(propertyValue, propertyMetadata)
        : mapper.toDb(propertyValue);
    // some basic validation
    if (propertyMetadata && propertyMetadata.key) {
        if (attrValue === null) {
            throw new Error(`${propertyMetadata.name.toString()} is null but is a ${propertyMetadata.key.type} key`);
        }
        if (!('S' in attrValue) && !('N' in attrValue) && !('B' in attrValue)) {
            throw new Error(`\
DynamoDb only allows string, number or binary type for RANGE and HASH key. \
Make sure to define a custom mapper for '${propertyMetadata.name.toString()}' which returns a string, number or binary value for partition key, \
type ${type} cannot be used as partition key, value = ${JSON.stringify(propertyValue)}`);
        }
    }
    return attrValue;
}
exports.toDbOne = toDbOne;
function testForKey(p) {
    return !!p.key;
}
/**
 * returns the function for the given ModelConstructor to create the AttributeMap with HASH (and RANGE) Key of a given item. used to delete items
 * @param modelConstructor
 */
function createToKeyFn(modelConstructor) {
    const metadata = metadata_helper_1.metadataForClass(modelConstructor);
    const properties = metadata.modelOptions.properties;
    if (!properties) {
        throw new Error('metadata properties is not defined');
    }
    const keyProperties = properties.filter(testForKey);
    return (item) => keyProperties.reduce((key, propMeta) => {
        const propertyValue = getPropertyValue(item, propMeta.name);
        if (propertyValue === null || propertyValue === undefined) {
            throw new Error(`there is no value for property ${propMeta.name.toString()} but is ${propMeta.key.type} key`);
        }
        // fixme: typings
        ;
        key[propMeta.name] = toDbOne(propertyValue, propMeta);
        return key;
    }, {});
}
exports.createToKeyFn = createToKeyFn;
function toKey(item, modelConstructor) {
    return createToKeyFn(modelConstructor)(item);
}
exports.toKey = toKey;
function fromDb(attributeMap, modelClass) {
    const model = {};
    Object.getOwnPropertyNames(attributeMap).forEach(attributeName => {
        /*
         * 1) get the value of the property
         */
        const attributeValue = attributeMap[attributeName];
        /*
         * 2) decide how to map the property depending on type or value
         */
        let modelValue;
        let propertyMetadata;
        if (modelClass) {
            propertyMetadata = metadata_helper_1.metadataForProperty(modelClass, attributeName);
        }
        if (propertyMetadata) {
            if (propertyMetadata.transient) {
                // skip transient property
            }
            else {
                /*
                 * 3a) property metadata is defined
                 */
                if (propertyMetadata && propertyMetadata.mapper) {
                    // custom mapper
                    modelValue = propertyMetadata.mapper().fromDb(attributeValue, propertyMetadata);
                }
                else {
                    modelValue = fromDbOne(attributeValue, propertyMetadata);
                }
            }
        }
        else {
            /*
             * 3b) no metadata found
             */
            // let typeByConvention = typeByConvention(propertyKey);
            // if (typeByConvention) {
            //   /*
            //    * 4a) matches a convention
            //    */
            //   modelValue = mapperForConvention(typeByConvention).fromDb(attributeValue);
            // } else {
            /*
             * 4b) no naming convention matches
             */
            modelValue = fromDbOne(attributeValue);
            // }
        }
        if (modelValue) {
            Reflect.set(model, propertyMetadata ? propertyMetadata.name : attributeName, modelValue);
        }
        // throw new Error('don\'t know how to map without model class');
    });
    return model;
}
exports.fromDb = fromDb;
function fromDbOne(attributeValue, propertyMetadata) {
    const explicitType = propertyMetadata && propertyMetadata.typeInfo && propertyMetadata.typeInfo.isCustom
        ? propertyMetadata.typeInfo.type
        : null;
    const type = explicitType || util_1.typeOfFromDb(attributeValue);
    if (explicitType) {
        return forType(type).fromDb(attributeValue, propertyMetadata);
    }
    else {
        return forType(type).fromDb(attributeValue);
    }
}
exports.fromDbOne = fromDbOne;
function forType(type) {
    let mapper = mapperForType.get(type);
    if (!mapper) {
        switch (type) {
            case String:
                mapper = string_mapper_1.StringMapper;
                break;
            case Number:
                mapper = number_mapper_1.NumberMapper;
                break;
            case Boolean:
                mapper = boolean_mapper_1.BooleanMapper;
                break;
            case enum_type_1.EnumType:
                mapper = enum_mapper_1.EnumMapper;
                break;
            case Map:
                // Maps support complex types as keys, we only support String & Number as Keys, otherwise a .toString() method should be implemented,
                // so we now how to save a  key
                // mapperForType = new MapMapper()
                throw new Error('Map is not supported to be mapped for now');
            case Array:
                mapper = collection_mapper_1.CollectionMapper;
                break;
            case Set:
                mapper = collection_mapper_1.CollectionMapper;
                break;
            case Object:
                mapper = object_mapper_1.ObjectMapper;
                break;
            case null_type_1.NullType:
                mapper = null_mapper_1.NullMapper;
                break;
            case binary_type_1.Binary:
                // TODO LOW:BINARY add binary mapper
                throw new Error('no mapper for binary type implemented yet');
            case undefined_type_1.UndefinedType:
                mapper = object_mapper_1.ObjectMapper;
                break;
            default:
                mapper = object_mapper_1.ObjectMapper;
            // throw new Error('no mapper defined for type ' + JSON.stringify(type))
        }
        mapperForType.set(type, mapper);
    }
    return mapper;
}
exports.forType = forType;
function getPropertyValue(item, propertyKey) {
    const propertyDescriptor = Object.getOwnPropertyDescriptor(item, propertyKey);
    // use get accessor if available otherwise use value property of descriptor
    if (propertyDescriptor) {
        if (propertyDescriptor.get) {
            return propertyDescriptor.get();
        }
        else {
            return propertyDescriptor.value;
        }
    }
    else {
        throw new Error(`there is no property descriptor for item ${JSON.stringify(item)} and property key ${propertyKey}`);
    }
}
exports.getPropertyValue = getPropertyValue;
//# sourceMappingURL=mapper.js.map