package prolog.ast.logic

import prolog.Answers
import prolog.Substitutions
import prolog.ast.Database.Program
import prolog.ast.terms.*
import prolog.builtins.True
import prolog.flags.AppliedCut
import prolog.logic.applySubstitution
import prolog.logic.numbervars
import prolog.logic.occurs
import prolog.logic.unifyLazy

/**
 * ‘Sentence’ of a Prolog program.
 *
 * A clause consists of a [Head] and body separated by the neck operator, or it is a [Fact].
 *
 * @see [Variable]
 * @see [Predicate]
 */
abstract class Clause(var head: Head, var body: Body) : Term, Resolvent {
    val functor: Functor = head.functor

    override fun solve(goal: Goal, subs: Substitutions): Answers = sequence {
        // If the clause is a rule, unify the goal with the head and then try to prove the body.
        // Only if the body can be proven, the substitutions should be returned.
        // Do this in a lazy way.

        // Since we are only interested in substitutions in the goal (as opposed to the head of this clause),
        //  we can use variable renaming and filter out the substitutions that are not in the goal.
        val (headEnd, headRenaming) = numbervars(head, Program.variableRenamingStart, emptyMap())
        Program.variableRenamingStart = headEnd

        val renamedHead = applySubstitution(head, headRenaming)
        val simplifiedGoal = applySubstitution(goal, subs)
        unifyLazy(simplifiedGoal, renamedHead, emptyMap()).forEach { headAnswer ->
            headAnswer.map { headAndGoalSubs ->
                // Filter the substitutions to only include those that map from head to goal
                val headToGoalSubs = headAndGoalSubs.filterKeys { it in headRenaming.values }
                val goalToHeadSubs = headAndGoalSubs.filterKeys { occurs(it as Variable, simplifiedGoal) }
                val headResult = headToGoalSubs.filterKeys { key ->
                    goalToHeadSubs.any { occurs(key as Variable, it.value) }
                }
                val newSubs = headRenaming + headToGoalSubs
                body.satisfy(newSubs).forEach { bodyAnswer ->
                    bodyAnswer.fold(
                        onSuccess = { bodySubs ->
                            // If the body can be proven, yield the (combined) substitutions
                            val goalResult = goalToHeadSubs.mapValues { applySubstitution(it.value, bodySubs) }
                            val bodyResult = bodySubs.filterKeys { occurs(it as Variable, simplifiedGoal) }
                            yield(Result.success(goalResult + headResult + bodyResult))
                        },
                        onFailure = { error ->
                            if (error is AppliedCut) {
                                // Find single solution and return immediately
                                if (error.subs != null) {
                                    val bodySubs = error.subs
                                    val goalResult = goalToHeadSubs.mapValues { applySubstitution(it.value, bodySubs) }
                                    val bodyResult = bodySubs.filterKeys { occurs(it as Variable, simplifiedGoal) }
                                    yield(Result.failure(AppliedCut(goalResult + headResult + bodyResult)))
                                } else {
                                    yield(Result.failure(AppliedCut()))
                                }
                                return@sequence
                            } else {
                                yield(Result.failure(error))
                            }
                        }
                    )
                }
            }
        }
    }

    override fun toString(): String = if (body is True) head.toString() else "$head :- $body"

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is Clause) return false

        if (head != other.head) return false
        if (body != other.body) return false

        return true
    }

    override fun hashCode(): Int = super.hashCode()
}
