import ValidationError from '../error/validation_error';
import validateExpression from './validate_expression';
import validateEnum from './validate_enum';
import {getType, isString, isNumber, isBoolean} from '../util/get_type';
import {unbundle, deepUnbundle} from '../util/unbundle_jsonlint';
import {isExpressionFilter} from '../feature_filter/index';

import type {StyleReference} from '../reference/latest';
import type {StyleSpecification} from '../types';

type FilterValidatorOptions = {
    key: string;
    value: unknown;
    style: Partial<StyleSpecification>;
    styleSpec: StyleReference;
    layerType?: string;
    object?: {
        type?: string,
        id?: string
    }
};

export default function validateFilter(options: FilterValidatorOptions): ValidationError[] {
    if (isExpressionFilter(deepUnbundle(options.value))) {
        // We default to a layerType of `fill` because that points to a non-dynamic filter definition within the style-spec.
        const layerType = options.layerType || 'fill';

        return validateExpression(Object.assign({}, options, {
            expressionContext: 'filter' as const,
            // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
            valueSpec: options.styleSpec[`filter_${layerType}`]
        }));
    } else {
        return validateNonExpressionFilter(options);
    }
}

function validateNonExpressionFilter(options: FilterValidatorOptions): ValidationError[] {
    const value = options.value;
    const key = options.key;

    if (!Array.isArray(value)) {
        return [new ValidationError(key, value, `array expected, ${getType(value)} found`)];
    }

    if (value.length < 1) {
        return [new ValidationError(key, value, 'filter array must have at least 1 element')];
    }

    const styleSpec = options.styleSpec;
    let errors: ValidationError[] = validateEnum({
        key: `${key}[0]`,
        value: value[0],
        // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
        valueSpec: styleSpec.filter_operator
    });

    const validate = () => {
        if (value.length >= 2) {
            if (!isString(value[1])) {
                errors.push(new ValidationError(`${key}[1]`, value[1], `string expected, ${getType(value[1])} found`));
            }
        }
        for (let i = 2; i < value.length; i++) {
            if (unbundle(value[1]) === '$type') {
                errors = errors.concat(validateEnum({
                    key: `${key}[${i}]`,
                    value: value[i],
                    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
                    valueSpec: styleSpec.geometry_type
                }));
            } else if (!isString(value[i]) && !isNumber(value[i]) && !isBoolean(value[i])) {
                errors.push(new ValidationError(`${key}[${i}]`, value[i], `string, number, or boolean expected, ${getType(value[i])} found.`));
            }
        }
    };

    switch (unbundle(value[0])) {
    case '<':
    case '<=':
    case '>':
    case '>=':
        if (value.length >= 2 && unbundle(value[1]) === '$type') {
            errors.push(new ValidationError(key, value, `"$type" cannot be use with operator "${value[0]}"`));
        }
        if (value.length !== 3) {
            errors.push(new ValidationError(key, value, `filter array for operator "${value[0]}" must have 3 elements`));
        }
        validate();
        break;

    case '==':
    case '!=':
        if (value.length !== 3) {
            errors.push(new ValidationError(key, value, `filter array for operator "${value[0]}" must have 3 elements`));
        }
        validate();
        break;

    case 'in':
    case '!in':
        validate();
        break;

    case 'any':
    case 'all':
    case 'none':
        for (let i = 1; i < value.length; i++) {
            errors = errors.concat(validateNonExpressionFilter({
                key: `${key}[${i}]`,
                value: value[i],
                style: options.style,
                styleSpec: options.styleSpec
            }));
        }
        break;

    case 'has':
    case '!has':
        if (value.length !== 2) {
            errors.push(new ValidationError(key, value, `filter array for "${value[0]}" operator must have 2 elements`));
        } else if (!isString(value[1])) {
            errors.push(new ValidationError(`${key}[1]`, value[1], `string expected, ${getType(value[1])} found`));
        }
        break;
    }

    return errors;
}
