import { isObject } from "../../utils/isObject";
import { SchemaNode } from "../../types";
import { Keyword, JsonSchemaValidatorParams, ValidationReturnType } from "../../Keyword";
import { validateNode } from "../../validateNode";

const KEYWORD = "unevaluatedItems";

/**
 * @draft >= 2019-09
 * Similar to additionalItems, but can "see" into subschemas and across references
 * https://json-schema.org/draft/2019-09/json-schema-core#rfc.section.9.3.1.3
 */
export const unevaluatedItemsKeyword: Keyword = {
    id: KEYWORD,
    keyword: KEYWORD,
    parse: parseUnevaluatedItems,
    addValidate: ({ schema }) => schema[KEYWORD] != null,
    validate: validateUnevaluatedItems
};

export function parseUnevaluatedItems(node: SchemaNode) {
    const { unevaluatedItems } = node.schema;
    if (unevaluatedItems == null || typeof unevaluatedItems === "boolean") {
        return;
    }

    if (!isObject(unevaluatedItems)) {
        return node.createError("schema-error", {
            pointer: node.evaluationPath,
            schema: node.schema,
            value: undefined,
            message: `Keyword '${KEYWORD}' must be an object - received '${typeof unevaluatedItems}'`
        });
    }

    node.unevaluatedItems = node.compileSchema(
        node.schema.unevaluatedItems,
        `${node.evaluationPath}/${KEYWORD}`,
        `${node.schemaLocation}/${KEYWORD}`
    );
}

function validateUnevaluatedItems({ node, data, pointer, path }: JsonSchemaValidatorParams) {
    const { schema } = node;
    // if not in items, and not matches additionalItems
    if (
        !Array.isArray(data) ||
        data.length === 0 ||
        schema.unevaluatedItems == null ||
        schema.unevaluatedItems === true
    ) {
        return undefined;
    }

    // const reducedNode = node;
    let { node: reducedNode } = node.reduceNode(data, { pointer, path });
    reducedNode = reducedNode ?? node;
    if (reducedNode.schema.unevaluatedItems === true || reducedNode.schema.additionalItems === true) {
        return undefined;
    }

    // console.log("EVAL", reducedNode.schema);

    const validIf = node.if != null && validateNode(node.if, data, pointer, path).length === 0;
    const errors: ValidationReturnType = [];
    // "unevaluatedItems with nested items"
    for (let i = 0; i < data.length; i += 1) {
        const value = data[i];
        const { node: child } = node.getNodeChild(i, data, { path });
        // console.log(`CHILD '${i}':`, data[i], "=>", child?.schema);

        if (child) {
            // when a single node is invalid
            if (validateNode(child, value, `${pointer}/${i}`, path).length > 0) {
                // nothing should validate, so we validate unevaluated items only
                const unevaluatedItems = node.unevaluatedItems;
                if (unevaluatedItems) {
                    data.forEach((value) => {
                        const result = validateNode(unevaluatedItems, value, `${pointer}/${i}`, path);
                        if (result == null) {
                            return;
                        }
                        if (Array.isArray(result)) {
                            return errors.push(...result);
                        }
                        errors.push(result);
                    });
                }
                if (node.schema.unevaluatedItems === false) {
                    return node.createError("unevaluated-items-error", {
                        pointer: `${pointer}/${i}`,
                        value: JSON.stringify(value),
                        schema
                    });
                }
            }
        }
        // "unevaluatedItems false"
        if (child === undefined) {
            // valid if ensures node.if is set
            if (validIf && node.if!.prefixItems && node.if!.prefixItems.length > i) {
                // evaluated by if -- skip
            } else if (node.unevaluatedItems) {
                const result = validateNode(node.unevaluatedItems, value, `${pointer}/${i}`, path);
                if (result.length > 0) {
                    errors.push(...result);
                }
            } else {
                errors.push(
                    node.createError("unevaluated-items-error", {
                        pointer: `${pointer}/${i}`,
                        value: JSON.stringify(value),
                        schema
                    })
                );
            }
        }
    }

    return errors;
}
