import { isQueryCommand, isComparisonCommand, QUERY_COMMANDS_LITERAL } from '../commands/query';
import { isLogicCommand, LOGIC_COMMANDS_LITERAL } from '../commands/logic';
import { SYMBOL_UNSET_FIELD_NAME, SYMBOL_GEO_POINT } from '../helper/symbol';
import { getType, isObject, isArray, isRegExp, isDate } from '../utils/type';
import { operatorToString } from '../operator-map';
import { flattenQueryObject, isConversionRequired, encodeInternalDataType } from './common';
import { stringifyByEJSON } from '../utils/utils';
import { Validate } from '../validate';
export class QuerySerializer {
    constructor() { }
    static encode(query) {
        const encoder = new QueryEncoder();
        return encoder.encodeQuery(query);
    }
    static encodeEJSON(query) {
        const encoder = new QueryEncoder();
        return stringifyByEJSON(encoder.encodeQuery(query));
    }
}
class QueryEncoder {
    encodeQuery(query, key) {
        if (isConversionRequired(query)) {
            if (isLogicCommand(query)) {
                return this.encodeLogicCommand(query);
            }
            else if (isQueryCommand(query)) {
                return this.encodeQueryCommand(query);
            }
            else if (isRegExp(query)) {
                return { [key]: this.encodeRegExp(query) };
            }
            else if (isDate(query)) {
                return { [key]: query };
            }
            else {
                return { [key]: this.encodeQueryObject(query) };
            }
        }
        else {
            if (isObject(query)) {
                return this.encodeQueryObject(query);
            }
            else {
                return query;
            }
        }
    }
    encodeRegExp(query) {
        return {
            $regularExpression: {
                pattern: query.source,
                options: query.flags
            }
        };
    }
    encodeLogicCommand(query) {
        switch (query.operator) {
            case LOGIC_COMMANDS_LITERAL.NOR:
            case LOGIC_COMMANDS_LITERAL.AND:
            case LOGIC_COMMANDS_LITERAL.OR: {
                const $op = operatorToString(query.operator);
                const subqueries = query.operands.map((oprand) => this.encodeQuery(oprand, query.fieldName));
                return {
                    [$op]: subqueries
                };
            }
            case LOGIC_COMMANDS_LITERAL.NOT: {
                const $op = operatorToString(query.operator);
                const operatorExpression = query.operands[0];
                if (isRegExp(operatorExpression)) {
                    return {
                        [query.fieldName]: {
                            [$op]: this.encodeRegExp(operatorExpression)
                        }
                    };
                }
                else {
                    const subqueries = this.encodeQuery(operatorExpression)[query.fieldName];
                    return {
                        [query.fieldName]: {
                            [$op]: subqueries
                        }
                    };
                }
            }
            default: {
                const $op = operatorToString(query.operator);
                if (query.operands.length === 1) {
                    const subquery = this.encodeQuery(query.operands[0]);
                    return {
                        [$op]: subquery
                    };
                }
                else {
                    const subqueries = query.operands.map(this.encodeQuery.bind(this));
                    return {
                        [$op]: subqueries
                    };
                }
            }
        }
    }
    encodeQueryCommand(query) {
        if (isComparisonCommand(query)) {
            return this.encodeComparisonCommand(query);
        }
        else {
            return this.encodeComparisonCommand(query);
        }
    }
    encodeComparisonCommand(query) {
        if (query.fieldName === SYMBOL_UNSET_FIELD_NAME) {
            throw new Error('Cannot encode a comparison command with unset field name');
        }
        const $op = operatorToString(query.operator);
        switch (query.operator) {
            case QUERY_COMMANDS_LITERAL.EQ:
            case QUERY_COMMANDS_LITERAL.NEQ:
            case QUERY_COMMANDS_LITERAL.LT:
            case QUERY_COMMANDS_LITERAL.LTE:
            case QUERY_COMMANDS_LITERAL.GT:
            case QUERY_COMMANDS_LITERAL.GTE:
            case QUERY_COMMANDS_LITERAL.ELEM_MATCH:
            case QUERY_COMMANDS_LITERAL.EXISTS:
            case QUERY_COMMANDS_LITERAL.SIZE:
            case QUERY_COMMANDS_LITERAL.MOD: {
                return {
                    [query.fieldName]: {
                        [$op]: encodeInternalDataType(query.operands[0])
                    }
                };
            }
            case QUERY_COMMANDS_LITERAL.IN:
            case QUERY_COMMANDS_LITERAL.NIN:
            case QUERY_COMMANDS_LITERAL.ALL: {
                return {
                    [query.fieldName]: {
                        [$op]: encodeInternalDataType(query.operands)
                    }
                };
            }
            case QUERY_COMMANDS_LITERAL.GEO_NEAR: {
                const options = query.operands[0];
                return {
                    [query.fieldName]: {
                        $nearSphere: {
                            $geometry: options.geometry.toJSON(),
                            $maxDistance: options.maxDistance,
                            $minDistance: options.minDistance
                        }
                    }
                };
            }
            case QUERY_COMMANDS_LITERAL.GEO_WITHIN: {
                const options = query.operands[0];
                if (options.centerSphere) {
                    Validate.isCentersPhere(options.centerSphere);
                    const centerSphere = options.centerSphere;
                    if (centerSphere[0]._internalType === SYMBOL_GEO_POINT) {
                        return {
                            [query.fieldName]: {
                                $geoWithin: {
                                    $centerSphere: [centerSphere[0].toJSON().coordinates, centerSphere[1]]
                                }
                            }
                        };
                    }
                    return {
                        [query.fieldName]: {
                            $geoWithin: {
                                $centerSphere: options.centerSphere
                            }
                        }
                    };
                }
                return {
                    [query.fieldName]: {
                        $geoWithin: {
                            $geometry: options.geometry.toJSON()
                        }
                    }
                };
            }
            case QUERY_COMMANDS_LITERAL.GEO_INTERSECTS: {
                const options = query.operands[0];
                return {
                    [query.fieldName]: {
                        $geoIntersects: {
                            $geometry: options.geometry.toJSON()
                        }
                    }
                };
            }
            default: {
                return {
                    [query.fieldName]: {
                        [$op]: encodeInternalDataType(query.operands[0])
                    }
                };
            }
        }
    }
    encodeQueryObject(query) {
        const flattened = flattenQueryObject(query);
        for (const key in flattened) {
            const val = flattened[key];
            if (isLogicCommand(val)) {
                flattened[key] = val._setFieldName(key);
                const condition = this.encodeLogicCommand(flattened[key]);
                this.mergeConditionAfterEncode(flattened, condition, key);
            }
            else if (isComparisonCommand(val)) {
                flattened[key] = val._setFieldName(key);
                const condition = this.encodeComparisonCommand(flattened[key]);
                this.mergeConditionAfterEncode(flattened, condition, key);
            }
            else if (isConversionRequired(val)) {
                flattened[key] = encodeInternalDataType(val);
            }
        }
        return flattened;
    }
    mergeConditionAfterEncode(query, condition, key) {
        if (!condition[key]) {
            delete query[key];
        }
        for (const conditionKey in condition) {
            if (query[conditionKey]) {
                if (isArray(query[conditionKey])) {
                    query[conditionKey] = query[conditionKey].concat(condition[conditionKey]);
                }
                else if (isObject(query[conditionKey])) {
                    if (isObject(condition[conditionKey])) {
                        Object.assign(query, condition);
                    }
                    else {
                        console.warn(`unmergable condition, query is object but condition is ${getType(condition)}, can only overwrite`, condition, key);
                        query[conditionKey] = condition[conditionKey];
                    }
                }
                else {
                    console.warn(`to-merge query is of type ${getType(query)}, can only overwrite`, query, condition, key);
                    query[conditionKey] = condition[conditionKey];
                }
            }
            else {
                query[conditionKey] = condition[conditionKey];
            }
        }
    }
}
