package prolog.builtins

import io.Logger
import prolog.Answers
import prolog.Substitutions
import prolog.ast.Database
import prolog.ast.Database.Program
import prolog.ast.arithmetic.Integer
import prolog.ast.logic.Clause
import prolog.ast.logic.Fact
import prolog.ast.logic.Predicate
import prolog.ast.terms.*
import prolog.logic.applySubstitution
import prolog.logic.unifyLazy

/**
 * (Make) the [Predicate] with the corresponding [Functor] dynamic.
 */
class Dynamic(private val dynamicFunctor: Functor) : Goal(), Body {
    override val functor = Functor(Atom("dynamic"), Integer(1))

    override fun satisfy(subs: Substitutions): Answers {
        val predicate = Program.db.predicates[dynamicFunctor]
        if (predicate == null) {
            return sequenceOf(Result.failure(Exception("Predicate $dynamicFunctor not found")))
        }

        predicate.dynamic = true
        return sequenceOf(Result.success(emptyMap()))
    }

    override fun toString(): String = "dynamic $dynamicFunctor"

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is Dynamic) return false
        return dynamicFunctor == other.dynamicFunctor
    }

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

    override fun applySubstitution(subs: Substitutions): Dynamic = Dynamic(dynamicFunctor)
}

class Assert(clause: Clause) : AssertZ(clause) {
    override val functor = Functor(Atom("assert"), Integer(1))
}

/**
 * Assert a [Clause] as a first clause of the [Predicate] into the [Program].
 */
class AssertA(val clause: Clause) : Operator("asserta", rightOperand = clause) {
    override fun satisfy(subs: Substitutions): Answers {
        // Add clause to the program
        val evaluatedClause = applySubstitution(clause, subs) as Clause
        Program.load(listOf(evaluatedClause), 0)

        return sequenceOf(Result.success(emptyMap()))
    }

    override fun applySubstitution(subs: Substitutions): AssertA = AssertA(applySubstitution(clause, subs) as Clause)
}

/**
 * Assert a [Clause] as a last clause of the [Predicate] into the [Program].
 */
open class AssertZ(val clause: Clause) : Operator("assertz", rightOperand = clause) {
    override fun satisfy(subs: Substitutions): Answers {
        // Add clause to the program
        val evaluatedClause = applySubstitution(clause, subs) as Clause
        Program.load(listOf(evaluatedClause))

        return sequenceOf(Result.success(emptyMap()))
    }

    override fun applySubstitution(subs: Substitutions): AssertZ = AssertZ(applySubstitution(clause, subs) as Clause)
}

/**
 * When [Term] is an [Atom] or a term, it is unified with the first unifying [Clause] in the [Database].
 * The [Fact] or Clause is removed from the Database. It respects the logical update view.
 *
 * @see [SWI-Prolog Predicate retract/1](https://www.swi-prolog.org/pldoc/doc_for?object=retract/1)
 */
open class Retract(val term: Term) : Operator("retract", rightOperand = term) {
    override fun satisfy(subs: Substitutions): Answers = sequence {
        // Check that term is a structure or atom
        if (term !is Structure && term !is Atom) {
            yield(Result.failure(Exception("Cannot retract a non-structure or non-atom")))
            return@sequence
        }

        val functorName = term.functor

        val predicate = Program.db.predicates[functorName]
        if (predicate == null) {
            return@sequence
        }

        // Check if the predicate is dynamic
        if (!predicate.dynamic) {
            yield(Result.failure(Exception("No permission to modify static procedure '$functorName'")))
            return@sequence
        }

        predicate.clauses.toList().forEach { clause ->
            unifyLazy(term, clause.head, subs).forEach { unifyResult ->
                unifyResult.fold(
                    onSuccess = { substitutions ->
                        // If unification is successful, remove the clause
                        predicate.clauses.remove(clause)
                        yield(Result.success(substitutions))
                    },
                    onFailure = {
                        // If unification fails, do nothing
                    }
                )
            }
        }
    }

    override fun applySubstitution(subs: Substitutions): Retract = Retract(applySubstitution(term, subs))
}

class RetractAll(term: Term) : Retract(term) {
    override fun satisfy(subs: Substitutions): Answers {
        // Check that term is a structure or atom
        if (term !is Structure && term !is Atom) {
            return sequenceOf(Result.failure(Exception("Cannot retract a non-structure or non-atom")))
        }

        // If the predicate does not exist, implicitly create it
        val functor = term.functor
        val predicate = Program.db.predicates[functor]
        if (predicate == null) {
            Logger.debug("Predicate $functor not found, creating it")
            Program.db.predicates += functor to Predicate(functor, dynamic = true)
        }

        // Propagate errors from the super class
        super.satisfy(subs).forEach {
            if (it.isFailure) {
                return sequenceOf(it)
            }
        }

        return sequenceOf(Result.success(emptyMap()))
    }
}
