package prolog.builtins

import prolog.Answers
import prolog.Substitutions
import prolog.ast.Database.Program
import prolog.ast.logic.LogicOperand
import prolog.ast.logic.LogicOperator
import prolog.ast.terms.Atom
import prolog.ast.terms.Body
import prolog.ast.terms.Goal
import prolog.flags.AppliedCut
import prolog.flags.AppliedShift
import prolog.logic.applySubstitution
import prolog.logic.numbervars

/**
 * Always fail.
 */
object Fail : Atom("fail"), Body {
    override fun satisfy(subs: Substitutions): Answers = emptySequence()
    override fun applySubstitution(subs: Substitutions): Fail = this
}

/**
 * Same as fail, but the name has a more declarative connotation.
 */
typealias False = Fail

/**
 * Always succeed.
 */
object True : Atom("true"), Body {
    override fun satisfy(subs: Substitutions): Answers = sequenceOf(Result.success(emptyMap()))
    override fun applySubstitution(subs: Substitutions): True = this
}

// TODO Repeat/0

class Cut() : Atom("!") {
    override fun satisfy(subs: Substitutions): Answers {
        return sequenceOf(Result.failure(AppliedCut(emptyMap())))
    }

    override fun applySubstitution(subs: Substitutions): Cut = this
}

/**
 * Conjunction (and). True if both Goal1 and Goal2 are true.
 */
open class Conjunction(val left: LogicOperand, private val right: LogicOperand) :
    LogicOperator(",", left, right) {
    override fun satisfy(subs: Substitutions): Answers = sequence {
        fun satisfyRight(leftSubs: Substitutions): Answers = sequence {
            right.satisfy(subs + leftSubs).forEach { right ->
                right.fold(
                    // If the right part succeeds, yield the result with the left substitutions
                    onSuccess = { rightSubs ->
                        yield(Result.success(leftSubs + rightSubs))
                    },
                    onFailure = { exception ->
                        // If the right part fails, check if it's a cut
                        if (exception is AppliedCut) {
                            // If it's a cut, yield the result with the left substitutions
                            val newSubs = if (exception.subs != null) leftSubs + exception.subs else null
                            yield(Result.failure(AppliedCut(newSubs)))
                            return@sequence
                        }

                        // If it's not a cut, yield the failure
                        yield(Result.failure(exception))
                    }
                )
            }
        }

        fun findNextCutSolution(appliedCut: AppliedCut): Answers = sequence {
            val leftSubs = appliedCut.subs
            right.satisfy(subs + (appliedCut.subs!!)).firstOrNull()?.map { rightSubs ->
                // If the right part succeeds, yield the result with the left substitutions
                yield(Result.success(leftSubs + rightSubs))
                return@sequence
            } ?: yield(Result.failure(AppliedCut()))
        }

        // Satisfy the left part first, which either succeeds or fails
        left.satisfy(subs).forEach { left ->
            left.fold(
                // If it succeeds, satisfy the right part with the updated substitutions and return all results
                onSuccess = { leftSubs -> yieldAll(satisfyRight(leftSubs)) },
                // If it fails, check these conditions:
                onFailure = { exception ->
                    when (exception) {
                        // 1. If the left part is a cut, satisfy the right part ONCE, and stop searching for more solutions
                        is AppliedCut -> yieldAll(findNextCutSolution(exception))

                        // 2. If the left part is a shift, we need to check if the right part can be satisfied
                        is AppliedShift -> {
                            if (exception.cont == null) {
                                // Pass the right of our disjunction as the continuation
                                val newShift = AppliedShift(exception.subs, exception.ball, right as Goal)
                                yield(Result.failure(newShift))
                            } else {
                                // Satisfy the right part with the updated substitutions
                                yieldAll(satisfyRight(exception.subs))
                            }
                        }

                        // 3. Any other failure should be returned as is
                        else -> yield(Result.failure(exception))
                    }
                }
            )
        }
    }

    override fun applySubstitution(subs: Substitutions): Conjunction = Conjunction(
        applySubstitution(left, subs) as LogicOperand,
        applySubstitution(right, subs) as LogicOperand
    )
}

/**
 * Disjunction (or). True if either Goal1 or Goal2 succeeds.
 */
open class Disjunction(private val left: LogicOperand, private val right: LogicOperand) :
    LogicOperator(";", left, right) {
    override fun satisfy(subs: Substitutions): Answers = sequence {
        left.satisfy(subs).forEach { left ->
            left.fold(
                onSuccess = { leftSubs ->
                    yield(Result.success(leftSubs))
                },
                onFailure = { failure ->
                    if (failure is AppliedCut) {
                        val leftSubs = failure.subs
                        yield(Result.failure(AppliedCut(leftSubs)))
                        return@sequence
                    }
                }
            )
        }
        yieldAll(right.satisfy(subs))
    }

    override fun applySubstitution(subs: Substitutions): Disjunction = Disjunction(
        applySubstitution(left, subs) as LogicOperand,
        applySubstitution(right, subs) as LogicOperand
    )
}

@Deprecated("Use Disjunction instead")
class Bar(leftOperand: LogicOperand, rightOperand: LogicOperand) : Disjunction(leftOperand, rightOperand)

// TODO ->

// TODO *->

/**
 * True if 'Goal' cannot be proven.
 */
open class Not(private val goal: Goal) : LogicOperator("\\+", rightOperand = goal) {
    override fun satisfy(subs: Substitutions): Answers {
        // If the goal can be proven, return an empty sequence
        val goalResults = goal.satisfy(subs).iterator()
        if (goalResults.hasNext()) {
            return emptySequence()
        }
        // If the goal cannot be proven, return a sequence with an empty map
        return sequenceOf(Result.success(emptyMap()))
    }

    override fun applySubstitution(subs: Substitutions): Not = Not(applySubstitution(goal, subs) as Goal)
}
