package prolog.logic

import prolog.Answers
import prolog.Substitutions
import prolog.ast.arithmetic.Expression
import prolog.ast.arithmetic.Integer
import prolog.ast.terms.Term
import prolog.ast.terms.Variable
import prolog.ast.arithmetic.Number

/**
 * Low and High are integers, High ≥Low.
 *
 * 1. If Value is an integer, Low ≤ Value ≤ High.
 * 2. When Value is a variable it is successively bound to all integers between Low and High.
 *    If High is inf or infinite between/3 is true iff Value ≥ Low, a feature that is particularly interesting for
 *    generating integers from a certain value.
 */
fun between(
    low: Integer,
    high: Integer,
    value: Integer
): Answers {
    return if (value.value in low.value..high.value) {
        sequenceOf(Result.success(emptyMap()))
    } else {
        emptySequence()
    }
}

fun between(
    low: Integer,
    high: Integer,
    variable: Variable
): Answers {
    return sequence {
        for (i in low.value..high.value) {
            yield(Result.success(mapOf(variable to Integer(i))))
        }
    }
}

/**
 * True if Int2 = Int1 + 1 and Int1 ≥ 0.
 *
 * At least one of the arguments must be instantiated to a natural number.
 *
 * @throws IllegalArgumentException the domain error not_less_than_zero if called with a negative integer.
 * E.g. succ(X, 0) fails silently and succ(X, -1) raises a domain error.125
 */
fun succ(term1: Expression, term2: Expression, subs: Substitutions): Answers {
    if (term2 is Integer) {
        require(term2.value >= 0) { "Domain error: not_less_than_zero" }
    }
    // If term1 is a variable, we need to check if it is bound to a negative integer
    return sequence {
        plus(term1, Integer(1), term2, subs).forEach {
            it.fold(
                onSuccess = { result ->
                    val t1 = applySubstitution(term1, result)
                    if (t1 in result || t1 in result.values) {
                        val e1 = t1.simplify(result)
                        if (e1.to is Integer && e1.to.value < 0) {
                            return@sequence
                        }
                    }
                    yield(Result.success(result))
                },
                onFailure = { yield(Result.success(emptyMap())) }
            )
        }
    }
}

/**
 * True if Int3 = Int1 + Int2.
 *
 * At least two of the three arguments must be instantiated to integers.
 */
fun plus(term1: Expression, term2: Expression, term3: Expression, subs: Substitutions): Answers =
    operate(term1, term2, term3, subs, Number::plus, Number::minus)

fun minus(term1: Expression, term2: Expression, term3: Expression, subs: Substitutions): Answers =
    operate(term1, term2, term3, subs, Number::minus, Number::plus)

fun mul(term1: Expression, term2: Expression, term3: Expression, subs: Substitutions): Answers =
    operate(term1, term2, term3, subs, Number::times, Number::div)

fun div(term1: Expression, term2: Expression, term3: Expression, subs: Substitutions): Answers =
    operate(term1, term2, term3, subs, Number::div, Number::times)

fun operate(
    term1: Expression,
    term2: Expression,
    term3: Expression,
    subs: Substitutions,
    op: (Number, Number) -> Number,
    inverseOp: (Number, Number) -> Number
): Answers = sequence {
    val t1 = applySubstitution(term1, subs)
    val t2 = applySubstitution(term2, subs)
    val t3 = applySubstitution(term3, subs)

    when {
        nonvariable(t1, subs) && nonvariable(t2, subs) && nonvariable(t3, subs) -> {
            val e1 = t1.simplify(subs)
            val e2 = t2.simplify(subs)
            val e3 = t3.simplify(subs)

            val int3Value = op(e1.to as Number, e2.to as Number)
            if (equivalent(int3Value, e3.to, emptyMap())) {
                val opSubs: Substitutions = listOfNotNull(e1.mapped, e2.mapped, e3.mapped)
                    .filter{ pair: Pair<Term, Term>? -> pair != null && !subs.contains(pair.first) }
                    .toMap()
                yield(Result.success(opSubs))
            }
        }

        nonvariable(t1, subs) && nonvariable(t2, subs) && variable(t3, subs) -> {
            val e1 = t1.simplify(subs)
            val e2 = t2.simplify(subs)

            val int3Value = op(e1.to as Number, e2.to as Number)
            val int3 = t3 as Variable
            yield(Result.success(mapOf(int3 to int3Value) + listOfNotNull(e1.mapped, e2.mapped)))
        }

        ((nonvariable(t1, subs) && variable(t2, subs)) || (variable(t1, subs) && nonvariable(t2, subs))) && nonvariable(t3, subs) -> {
            val t = if (nonvariable(t1, subs)) t2 else t1
            val e = if (nonvariable(t1, subs)) t1.simplify(subs) else t2.simplify(subs)
            val e3 = t3.simplify(subs)

            val value = inverseOp(e3.to as Number, e.to as Number)
            val int = t as Variable
            yield(Result.success(mapOf(int to value) + listOfNotNull(e.mapped, e3.mapped)))
        }

        else -> {
            yield(Result.failure(IllegalArgumentException("At least two arguments must be instantiated to integers")))
        }
    }
}

// TODO divmod

// TODO nth_integer_root_and_remainder