import getTypeOf, { JSType } from "./getTypeOf";
import { errorOrPromise } from "./utils/filter";
import flattenArray from "./utils/flattenArray";
import { JsonSchema, JsonPointer, JsonError, isJsonError } from "./types";
import { Draft } from "./draft";
import equal from "fast-deep-equal";

function getJsonSchemaType(value: unknown, expectedType: string | string[]): JSType | "integer" {
    const jsType = getTypeOf(value);
    if (
        jsType === "number" &&
        (expectedType === "integer" ||
            (Array.isArray(expectedType) && expectedType.includes("integer")))
    ) {
        return Number.isInteger(value) || isNaN(value as any) ? "integer" : "number";
    }
    return jsType;
}

/**
 * Validates data with json schema
 *
 * @param draft - validator
 * @param value - value to validate
 * @param [schema] - json schema, defaults to rootSchema
 * @param [pointer] - json pointer pointing to value (used for error-messages only)
 * @return list of errors or empty
 */
export default function validate(
    draft: Draft,
    value: unknown,
    schema: JsonSchema = draft.rootSchema,
    pointer: JsonPointer = "#"
): Array<JsonError> {
    schema = draft.resolveRef(schema);

    // @draft >= 07
    if (getTypeOf(schema) === "boolean") {
        if (schema) {
            return [];
        }
        return [draft.errors.invalidDataError({ pointer, schema, value })];
    }

    if (isJsonError(schema)) {
        return [schema as JsonError];
    }

    // @draft >= 6 const
    if (schema.const !== undefined) {
        if (equal(schema.const, value)) {
            return [];
        }
        return [draft.errors.constError({ pointer, schema, value, expected: schema.const })];
    }

    const receivedType = getJsonSchemaType(value, schema.type);
    const expectedType = schema.type || receivedType;

    if (
        receivedType !== expectedType &&
        (!Array.isArray(expectedType) || !expectedType.includes(receivedType))
    ) {
        return [
            draft.errors.typeError({
                pointer,
                schema,
                value,
                received: receivedType,
                expected: expectedType
            })
        ];
    }

    if (draft.validateType[receivedType] == null) {
        return [draft.errors.invalidTypeError({ pointer, schema, value, receivedType })];
    }

    // get type validation results
    const errors = flattenArray(draft.validateType[receivedType](draft, schema, value, pointer));
    return errors.filter(errorOrPromise) as JsonError[]; // ignore promises here
}
