import { curryRight, forEach, isPlainObject } from 'lodash';
import { toDbOne, typeOf } from '../../mapper';
import { resolveAttributeNames } from './functions/attribute-names.function';
import { isFunctionOperator } from './functions/is-function-operator.function';
import { isNoParamFunctionOperator } from './functions/is-no-param-function-operator.function';
import { operatorParameterArity } from './functions/operator-parameter-arity.function';
import { uniqAttributeValueName } from './functions/unique-attribute-value-name.function';
/**
 * see http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ConditionExpressions.html
 * https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Condition.html
 */
/**
 * Will walk the object tree recursively and removes all items which do not satisfy the filterFn
 * @param obj
 * @param {(value: any) => boolean} filterFn
 * @returns {any}
 */
export function deepFilter(obj, filterFn) {
    if (Array.isArray(obj)) {
        var returnArr_1 = [];
        obj.forEach(function (i) {
            var item = deepFilter(i, filterFn);
            if (item !== null) {
                returnArr_1.push(item);
            }
        });
        return returnArr_1.length ? returnArr_1 : null;
    }
    else if (obj instanceof Set) {
        var returnArr_2 = [];
        Array.from(obj).forEach(function (i) {
            var item = deepFilter(i, filterFn);
            if (item !== null) {
                returnArr_2.push(item);
            }
        });
        return returnArr_2.length ? new Set(returnArr_2) : null;
    }
    else if (isPlainObject(obj)) {
        var returnObj_1 = {};
        forEach(obj, function (value, key) {
            var item = deepFilter(value, filterFn);
            if (item !== null) {
                returnObj_1[key] = item;
            }
        });
        return Object.keys(returnObj_1).length ? returnObj_1 : null;
    }
    else {
        if (filterFn(obj)) {
            return obj;
        }
        else {
            return null;
        }
    }
}
/**
 * Will create a condition which can be added to a request using the param object.
 * It will create the expression statement and the attribute names and values.
 *
 * @param {string} attributePath
 * @param {ConditionOperator} operator
 * @param {any[]} values Depending on the operator the amount of values differs
 * @param {string[]} existingValueNames If provided the existing names are used to make sure we have a unique name for the current attributePath
 * @param {Metadata<any>} metadata If provided we use the metadata to define the attribute name and use it to map the given value(s) to attributeValue(s)
 * @returns {Expression}
 */
export function buildFilterExpression(attributePath, operator, values, existingValueNames, metadata) {
    // TODO LOW:INVESTIGATE is there a use case for undefined desired to be a value
    // metadata rid of undefined values
    values = deepFilter(values, function (value) { return value !== undefined; });
    // check if provided values are valid for given operator
    validateValues(operator, values);
    // load property metadata if model metadata was provided
    var propertyMetadata;
    if (metadata) {
        propertyMetadata = metadata.forProperty(attributePath);
    }
    /*
     * resolve placeholder and valuePlaceholder names (same as attributePath if it not already exists)
     * myProp -> #myProp for name placeholder and :myProp for value placeholder
     *
     * person[0] -> #person: person
     * person.list[0].age -> #person: person, #attr: attr, #age: age
     * person.age
     */
    var resolvedAttributeNames = resolveAttributeNames(attributePath, propertyMetadata);
    var valuePlaceholder = uniqAttributeValueName(attributePath, existingValueNames);
    /*
     * build the statement
     */
    var buildFilterFn;
    switch (operator) {
        case 'IN':
            buildFilterFn = curryRight(buildInConditionExpression);
            break;
        case 'BETWEEN':
            buildFilterFn = curryRight(buildBetweenConditionExpression);
            break;
        default:
            buildFilterFn = curryRight(buildDefaultConditionExpression);
            buildFilterFn = buildFilterFn(operator);
    }
    return buildFilterFn(attributePath, resolvedAttributeNames.placeholder, valuePlaceholder, resolvedAttributeNames.attributeNames, values, existingValueNames, propertyMetadata);
}
/**
 * IN expression is unlike all the others property the operand is an array of unwrapped values (not attribute values)
 *
 * @param {string} attributePath
 * @param {string[]} values
 * @param {string[]} existingValueNames
 * @param {PropertyMetadata<any>} propertyMetadata
 * @returns {Expression}
 */
function buildInConditionExpression(attributePath, namePlaceholder, valuePlaceholder, attributeNames, values, existingValueNames, propertyMetadata) {
    var attributeValues = values[0]
        .map(function (value) { return toDbOne(value, propertyMetadata); })
        .reduce(function (result, mappedValue, index) {
        if (mappedValue !== null) {
            result[valuePlaceholder + "_" + index] = mappedValue;
        }
        return result;
    }, {});
    var inStatement = values[0].map(function (value, index) { return valuePlaceholder + "_" + index; }).join(', ');
    return {
        statement: namePlaceholder + " IN (" + inStatement + ")",
        attributeNames: attributeNames,
        attributeValues: attributeValues,
    };
}
function buildBetweenConditionExpression(attributePath, namePlaceholder, valuePlaceholder, attributeNames, values, existingValueNames, propertyMetadata) {
    var attributes = {};
    var mappedValue1 = toDbOne(values[0], propertyMetadata);
    var mappedValue2 = toDbOne(values[1], propertyMetadata);
    if (mappedValue1 === null || mappedValue2 === null) {
        throw new Error('make sure to provide an actual value for te BETWEEN operator');
    }
    var value2Placeholder = uniqAttributeValueName(attributePath, [valuePlaceholder].concat(existingValueNames || []));
    var statement = namePlaceholder + " BETWEEN " + valuePlaceholder + " AND " + value2Placeholder;
    attributes[valuePlaceholder] = mappedValue1;
    attributes[value2Placeholder] = mappedValue2;
    return {
        statement: statement,
        attributeNames: attributeNames,
        attributeValues: attributes,
    };
}
function buildDefaultConditionExpression(attributePath, namePlaceholder, valuePlaceholder, attributeNames, values, existingValueNames, propertyMetadata, operator) {
    var statement;
    var hasValue = true;
    if (isFunctionOperator(operator)) {
        if (isNoParamFunctionOperator(operator)) {
            statement = operator + " (" + namePlaceholder + ")";
            hasValue = false;
        }
        else {
            statement = operator + " (" + namePlaceholder + ", " + valuePlaceholder + ")";
        }
    }
    else {
        statement = [namePlaceholder, operator, valuePlaceholder].join(' ');
    }
    var attributes = {};
    if (hasValue) {
        var attribute = void 0;
        switch (operator) {
            case 'contains':
                // TODO think about validation
                // validateValueForContains(values[0], propertyMetadata)
                attribute = toDbOne(values[0], propertyMetadata);
                break;
            default:
                attribute = toDbOne(values[0], propertyMetadata);
        }
        if (attribute) {
            attributes[valuePlaceholder] = attribute;
        }
    }
    return {
        statement: statement,
        attributeNames: attributeNames,
        attributeValues: attributes,
    };
}
/**
 * Every operator requires a predefined arity of parameters, this method checks for the correct arity and throws an Error
 * if not correct
 *
 * @param {any[]} values The values which will be applied to the operator function implementation
 * @throws {Error} error Throws an error if the amount of values won't match the operator function parameter arity or
 * the given values is not an array
 */
function validateValues(operator, values) {
    var parameterArity = operatorParameterArity(operator);
    if (values === null || values === undefined) {
        if (isFunctionOperator(operator) && !isNoParamFunctionOperator(operator)) {
            // the operator needs some values to work
            throw new Error("expected " + parameterArity + " value(s) for operator " + operator + ", this is not the right amount of method parameters for this operator");
        }
    }
    else if (values && Array.isArray(values)) {
        // check for correct amount of values
        if (values.length !== parameterArity) {
            switch (operator) {
                case 'IN':
                    throw new Error("expected " + parameterArity + " value(s) for operator " + operator + ", this is not the right amount of method parameters for this operator (IN operator requires one value of array type)");
                default:
                    throw new Error("expected " + parameterArity + " value(s) for operator " + operator + ", this is not the right amount of method parameters for this operator");
            }
        }
        // some additional operator dependent validation
        switch (operator) {
            case 'BETWEEN':
                // values must be the same type
                if (typeOf(values[0]) !== typeOf(values[1])) {
                    throw new Error("both values for operator BETWEEN must have the same type, got " + typeOf(values[0]) + " and " + typeOf(values[1]));
                }
                break;
            case 'IN':
                if (!Array.isArray(values[0])) {
                    throw new Error('the provided value for IN operator must be an array');
                }
        }
    }
}
// TODO should we support other types than String, Number, Binary (can we search a boolean set for example with boolean as string?)
// private static validateValueForContains(value: any, propertyMetadata?: PropertyMetadata<any>): { S: string } {
//   let finalValue: { S: string }
//   if (propertyMetadata && propertyMetadata.typeInfo) {
//     switch (propertyMetadata.typeInfo.type) {
//       case Array:
//       case Set:
//         // FIXME REVIEW the validation logic
//         // const genericType = propertyMetadata.typeInfo.genericType
//         // if ((!genericType && (typeof value === 'number' || typeof value === 'string' || typeof value === '')) || (
//         //   genericType &&
//         //   genericType !== String &&
//         //   genericType !== Number &&
//         //   genericType !== Binary)
//         // ) {
//         finalValue = { S: value.toString() }
//         // } else {
//         //   throw new Error(
//         //     'either generic type info is not defined or the generic type is not one of String, Number, Binary',
//         //   )
//         // }
//         break
//       case String:
//       case Number:
//       case Binary:
//         finalValue = { S: value.toString() }
//         break
//       default:
//         throw new Error(`contains expression is not supported for type ${propertyMetadata.typeInfo.type}`)
//     }
//   } else {
//     // no explicit type defined -> try to detect the type from value
//     const type = Util.typeOf(value)
//     if (type === String || type === Number || type === Binary) {
//       finalValue = { S: value.toString() }
//     } else {
//       throw new Error(`contains expression is not supported for type ${type}`)
//     }
//   }
//
//   return finalValue
// }
//# sourceMappingURL=condition-expression-builder.js.map