import { check } from './check'
import { compute } from './compute'
import { create } from './create'
import { extract } from './extract'

export type JSONObject = {
    [key: string]: JSONElement
}

export type JSONArray = JSONElement[]

export type JSONElement = JSONObject | JSONArray | string | number | boolean | null

export const createJSONObject = (object: unknown): JSONObject | undefined => {
    if (!check.object(object)) {
        return undefined
    }

    return create.object.fromEntries(extract.entries(object).map(([key, value]): [string, JSONElement] | undefined => {
        if (!check.string(key)) {
            return undefined
        }

        if (
            !check.string(value)
            && !check.number(value)
            && !check.boolean(value)
            && !check.array(value)
            && !check.object(value)
            && !check.null(value)
        ) {
            return undefined
        }

        if (check.array(value)) {
            return compute.defined(createJSONArray(value), array => {
                return [key, array]
            })
        }
        if (check.object(value)) {
            return compute.defined(createJSONObject(value), object => {
                return [key, object]
            })
        }

        return [key, value]
    }).filter(value => check.defined(value)))
}

export const createJSONArray = (array: unknown): JSONArray | undefined => {
    if (!check.array(array)) {
        return undefined
    }

    return array.map((value): JSONElement | undefined => {
        if (
            !check.string(value)
            && !check.number(value)
            && !check.boolean(value)
            && !check.array(value)
            && !check.object(value)
            && !check.null(value)
        ) {
            return undefined
        }

        if (check.array(value)) {
            return createJSONArray(value)
        }
        if (check.object(value)) {
            return createJSONObject(value)
        }

        return value
    }).filter(value => check.defined(value))
}

export const createJSONElement = (element: unknown): JSONElement | undefined => {
    if (
        !check.string(element)
        && !check.number(element)
        && !check.boolean(element)
        && !check.array(element)
        && !check.object(element)
        && !check.null(element)
    ) {
        return undefined
    }

    if (check.array(element)) {
        return createJSONArray(element)
    }
    if (check.object(element)) {
        return createJSONObject(element)
    }

    return element
}

export const checkJSONObject = (object: unknown): object is JSONObject => {
    if (!check.object(object)) {
        return false
    }

    for (const [key, value] of extract.entries(object)) {
        if (!check.string(key)) {
            return false
        }

        if (
            !check.string(value)
            && !check.number(value)
            && !check.boolean(value)
            && !check.array(value)
            && !check.object(value)
            && !check.null(value)
        ) {
            return false
        }

        if (check.array(value) && !checkJSONArray(value)) {
            return false
        }
        if (check.object(value) && !checkJSONObject(value)) {
            return false
        }
    }

    return true
}

export const checkJSONArray = (array: unknown): array is JSONArray => {
    if (!check.array(array)) {
        return false
    }

    for (const value of array) {
        if (
            !check.string(value)
            && !check.number(value)
            && !check.boolean(value)
            && !check.array(value)
            && !check.object(value)
            && !check.null(value)
        ) {
            return false
        }

        if (check.array(value) && !checkJSONArray(value)) {
            return false
        }
        if (check.object(value) && !checkJSONObject(value)) {
            return false
        }
    }

    return true
}

export const checkJSONElement = (element: unknown): element is JSONElement => {
    if (
        !check.string(element)
        && !check.number(element)
        && !check.boolean(element)
        && !check.array(element)
        && !check.object(element)
        && !check.null(element)
    ) {
        return false
    }

    if (check.array(element) && !checkJSONArray(element)) {
        return false
    }
    if (check.object(element) && !checkJSONObject(element)) {
        return false
    }

    return true
}
