package prolog.ast.logic

import prolog.Answers
import prolog.Substitutions
import prolog.ast.terms.Functor
import prolog.ast.terms.Goal
import prolog.flags.AppliedCut

/**
 * Collection of [Clause]s with the same [Functor].
 *
 * If a goal is proved, the system looks for a predicate with the same functor, then uses indexing
 * to select candidate clauses and then tries these clauses one-by-one.
 */
class Predicate : Resolvent {
    val functor: Functor
    val clauses: MutableList<Clause>
    var dynamic = false

    /**
     * Creates a predicate with the given functor and an empty list of clauses.
     */
    constructor(functor: Functor, dynamic: Boolean = false) {
        this.functor = functor
        this.clauses = mutableListOf()
        this.dynamic = dynamic
    }

    /**
     * Creates a predicate with the given clauses.
     */
    constructor(clauses: List<Clause>, dynamic: Boolean = false) {
        this.functor = clauses.first().functor
        require(clauses.all { it.functor == functor }) { "All clauses must have the same functor" }
        this.clauses = clauses.toMutableList()
        this.dynamic = dynamic
    }

    /**
     * Adds a clause to the predicate.
     *
     * @param clause The clause to add.
     * @param index The index at which to insert the clause. If null, the clause is added to the end of the list.
     * @param force If true, the clause is added even if the predicate is static.
     */
    fun add(clause: Clause, index: Int? = null, force: Boolean = false) {
        require(clause.functor == functor) { "Clause functor does not match predicate functor" }
        require(dynamic || force) { "No permission to modify static procedure '$functor'" }

        if (index != null) clauses.add(index, clause) else clauses.add(clause)
    }

    /**
     * Adds a list of clauses to the predicate.
     */
    fun addAll(clauses: List<Clause>, force: Boolean = false) {
        require(clauses.all { it.functor == functor }) { "All clauses must have the same functor" }
        require(dynamic || force) { "No permission to modify static procedure '$functor'" }

        this.clauses.addAll(clauses)
    }

    override fun solve(goal: Goal, subs: Substitutions): Answers = sequence {
        require(goal.functor == functor) { "Goal functor does not match predicate functor" }

        // Try to unify the goal with the clause
        // If the unification is successful, yield the substitutions
        clauses.forEach { clause ->
            clause.solve(goal, subs).forEach { clauseResult ->
                clauseResult.fold(
                    onSuccess = {
                        yield(Result.success(it))
                    },
                    onFailure = {
                        if (it is AppliedCut) {
                            if (it.subs != null) {
                                // If it's a cut, yield the result with the left substitutions
                                yield(Result.success(it.subs))
                            }
                            return@sequence
                        } else {
                            yield(Result.failure(it))
                        }
                    }
                )
            }
        }
    }
}