"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 date_mapper_1 = require("./for-type/date.mapper");
const enum_mapper_1 = require("./for-type/enum.mapper");
const moment_mapper_1 = require("./for-type/moment.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 moment_type_1 = require("./type/moment.type");
const null_type_1 = require("./type/null.type");
const undefined_type_1 = require("./type/undefined.type");
const util_1 = require("./util");
// import debug from 'debug';
/**
 *
 */
class Mapper {
    // static logger = debug('Mapper');
    static toDb(item, modelConstructor) {
        const mapped = {};
        if (modelConstructor) {
            const metadata = metadata_helper_1.MetadataHelper.get(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.Util.uuidv4());
                    }
                });
            }
        }
        const propertyNames = Object.getOwnPropertyNames(item) || [];
        propertyNames.forEach(propertyKey => {
            /*
             * 1) get the value of the property
             */
            const propertyValue = Mapper.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.MetadataHelper.forProperty(modelConstructor, propertyKey);
                }
                if (propertyMetadata) {
                    if (propertyMetadata.transient) {
                        // skip transient property
                        // Mapper.logger.log('transient property -> skip')
                    }
                    else {
                        /*
                         * 3a) property metadata is defined
                         */
                        attributeValue = Mapper.toDbOne(propertyValue, propertyMetadata);
                    }
                }
                else {
                    /*
                     * 3b) no metadata found
                     */
                    // let typeByConvention = Util.typeByConvention(propertyKey);
                    // if (typeByConvention) {
                    /*
                       * 4a) matches a convention
                       */
                    // attributeValue = Mapper.mapperForConvention(typeByConvention).toDb(propertyValue);
                    // } else {
                    //   /*
                    //    * 4b) no naming convention matches
                    //    */
                    attributeValue = Mapper.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;
    }
    static toDbOne(propertyValue, propertyMetadata) {
        const explicitType = propertyMetadata && propertyMetadata.typeInfo && propertyMetadata.typeInfo.isCustom
            ? propertyMetadata.typeInfo.type
            : null;
        const type = explicitType || util_1.Util.typeOf(propertyValue);
        // some basic validation
        if (propertyMetadata &&
            propertyMetadata.key &&
            propertyMetadata.key.type === 'HASH' &&
            !propertyMetadata.mapper &&
            type !== String &&
            type !== Number &&
            type !== binary_type_1.Binary) {
            throw new Error(`make sure to define a custom mapper which returns a string or number value for partition key, type ${type} cannot be used as partition key, value = ${JSON.stringify(propertyValue)}`);
        }
        if (propertyMetadata && propertyMetadata.mapper) {
            // custom mapper
            if (explicitType) {
                return new propertyMetadata.mapper().toDb(propertyValue, propertyMetadata);
            }
            else {
                return new propertyMetadata.mapper().toDb(propertyValue);
            }
        }
        else {
            // mapper by type
            if (explicitType) {
                return Mapper.forType(type).toDb(propertyValue, propertyMetadata);
            }
            else {
                return Mapper.forType(type).toDb(propertyValue);
            }
        }
    }
    /**
     * 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
     */
    static createToKeyFn(modelConstructor) {
        const metadata = metadata_helper_1.MetadataHelper.get(modelConstructor);
        const properties = metadata.modelOptions.properties;
        if (!properties) {
            throw new Error('metadata properties is not defined');
        }
        const keyProps = properties.filter(p => !!p.key);
        return (item) => keyProps.reduce((key, propMeta) => {
            const propertyValue = Mapper.getPropertyValue(item, propMeta.name);
            if (propertyValue === null || propertyValue === undefined) {
                throw new Error(`there is no value for property ${propMeta.name} but is the ${propMeta.key.type} key`);
            }
            const attributeValue = Mapper.toDbOne(propertyValue, propMeta);
            return Object.assign({}, key, { [propMeta.name]: attributeValue });
        }, {});
    }
    static toKey(item, modelConstructor) {
        return Mapper.createToKeyFn(modelConstructor)(item);
    }
    static 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.MetadataHelper.forProperty(modelClass, attributeName);
            }
            if (propertyMetadata) {
                if (propertyMetadata.transient) {
                    // skip transient property
                    // Mapper.logger.log('transient property -> skip')
                }
                else {
                    /*
                     * 3a) property metadata is defined
                     */
                    if (propertyMetadata && propertyMetadata.mapper) {
                        // custom mapper
                        modelValue = new propertyMetadata.mapper().fromDb(attributeValue, propertyMetadata);
                    }
                    else {
                        modelValue = Mapper.fromDbOne(attributeValue, propertyMetadata);
                    }
                }
            }
            else {
                /*
                 * 3b) no metadata found
                 */
                // let typeByConvention = Util.typeByConvention(propertyKey);
                // if (typeByConvention) {
                //   /*
                //    * 4a) matches a convention
                //    */
                //   modelValue = Mapper.mapperForConvention(typeByConvention).fromDb(attributeValue);
                // } else {
                /*
                   * 4b) no naming convention matches
                   */
                modelValue = Mapper.fromDbOne(attributeValue);
                // }
            }
            Reflect.set(model, propertyMetadata ? propertyMetadata.name : attributeName, modelValue);
            // throw new Error('don\'t know how to map without model class');
        });
        return model;
    }
    static fromDbOne(attributeValue, propertyMetadata) {
        const explicitType = propertyMetadata && propertyMetadata.typeInfo && propertyMetadata.typeInfo.isCustom
            ? propertyMetadata.typeInfo.type
            : null;
        const type = explicitType || util_1.Util.typeOfFromDb(attributeValue);
        // Mapper.logger.log(`mapFromDbOne for type ${type}`)
        if (explicitType) {
            return Mapper.forType(type).fromDb(attributeValue, propertyMetadata);
        }
        else {
            return Mapper.forType(type).fromDb(attributeValue);
        }
    }
    static forType(type) {
        // FIXME HIGH review this, we now use toString to compare because we had issues with ng client for moment when
        // using a GSI on creationDate (MomentType) was a different MomentType than for lastUpdatedDate
        if (!Mapper.mapperForType.has(type.toString())) {
            let mapperForType;
            switch (type.toString()) {
                case String.toString():
                    mapperForType = new string_mapper_1.StringMapper();
                    break;
                case Number.toString():
                    mapperForType = new number_mapper_1.NumberMapper();
                    break;
                case Boolean.toString():
                    mapperForType = new boolean_mapper_1.BooleanMapper();
                    break;
                case moment_type_1.MomentType.toString():
                    mapperForType = new moment_mapper_1.MomentMapper();
                    break;
                case Date.toString():
                    mapperForType = new date_mapper_1.DateMapper();
                    break;
                case enum_type_1.EnumType.toString():
                    mapperForType = new enum_mapper_1.EnumMapper();
                    break;
                case Map.toString():
                    // 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.toString():
                    mapperForType = new collection_mapper_1.CollectionMapper();
                    break;
                case Set.toString():
                    mapperForType = new collection_mapper_1.CollectionMapper();
                    break;
                case Object.toString():
                    mapperForType = new object_mapper_1.ObjectMapper();
                    break;
                case null_type_1.NullType.toString():
                    mapperForType = new null_mapper_1.NullMapper();
                    break;
                case binary_type_1.Binary.toString():
                    // TODO LOW:BINARY add binary mapper
                    throw new Error('no mapper for binary type implemented yet');
                case undefined_type_1.UndefinedType.toString():
                    mapperForType = new object_mapper_1.ObjectMapper();
                    break;
                default:
                    throw new Error('no mapper defined for type ' + JSON.stringify(type));
                // mapperForType = new ObjectMapper()
            }
            this.mapperForType.set(type.toString(), mapperForType);
        }
        return this.mapperForType.get(type.toString());
    }
    static 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}`);
        }
    }
}
Mapper.mapperForType = new Map();
exports.Mapper = Mapper;
//# sourceMappingURL=mapper.js.map