package uk.ac.man.cs.lethe.abduction


import uk.ac.man.cs.lethe.internal.dl.owlapi.{OWLApiConfiguration, OWLApiConverter, OWLExporter}

import java.util.{Date, HashSet, Set}
import scala.collection.JavaConversions._
import scala.collection.immutable.{Queue, Set => scalaSet}
import scala.collection.mutable.{HashMap, MultiMap, Set => mutSet}
import org.semanticweb.owlapi.model.{OWLAxiom, OWLEntity, OWLOntology}
import uk.ac.man.cs.lethe.ontologies._
import uk.ac.man.cs.lethe.interpolation._
import uk.ac.man.cs.lethe.internal.dl.datatypes._
import uk.ac.man.cs.lethe.internal.dl.forgetting.direct.ALCFormulaPreparations
import uk.ac.man.cs.lethe.internal.dl.forgetting.direct.SimpleDefinerEliminator
import uk.ac.man.cs.lethe.internal.dl.owlapi.{OWLApiConverter, OWLExporter}
import uk.ac.man.cs.lethe.internal.forgetting.ForgetterWithBackgroundKB
import uk.ac.man.cs.lethe.internal.tools.{MockProgressBar, ProgressBarAttached}

/**
 * Class for performing abduction without a specified set of abducibles.
 *
 * This is meant to be used for applications where no set of abducibles is
 * specified before hand. Explanations are still computed against a set of
 * abducibles, using an OWLAbducerSignatureBased, but instead of one specified
 * set of abducibles a sequence of sets is used. This sequence starts with
 * explanations that eliminate one symbol from the signature of the observation,
 * and continues with explanations in which more symbols have been eliminated,
 * such that explanations become stronger and stronger towards the end of 
 * the sequence.
 *
 * For details on the computed results see the documentation of 
 * OWLAbducerSignatureBased.
 *
 * @param interpolator The interpolator that is used by the abducer to
 *  perform abductions. Currently, only instances of the class 
 *  AlchTBoxInterpolatorC are supported.
 * @param ontology The ontology against which abductions are
 *  performed.
 *
 * @throws IllegalArgumentException If the given interpolator is not supported.
 */
class OWLAbducerGeneral(interpolator: IOWLInterpolator,
                        ontology: OptimisedOWLOntology) {

  def this(interpolator: IOWLInterpolator,
           ontology: OWLOntology) = this(interpolator,
    new OptimisedOWLOntology(ontology))

  private val abducer = new OWLAbducerSignatureBased(interpolator, ontology)

  /**
   * Compute a first iteration step of abductions from the given 
   * observation. Each explanation is the result of eliminating
   * one symbol occuring in the axioms.
   *
   * @param observation A set of axioms specifying the observation.
   *
   * @return A set of explanations. Each explanation contains
   *  a weakest set of axioms in some subsignature of the
   *  of the observation that, if added to the ontology, entails
   *  the observation. 
   */
  def abduce(observation: Set[_<:OWLAxiom]): Set[Explanation] = {
    abduce(observation, new HashSet())
  }

  /**
   * Compute an iteration step of abductions from the given 
   * observation, where a specified set of symbols is always 
   * eliminated. Each explanation is the result of eliminating
   * one symbol plus the specified set of symbols from the 
   * observation.
   *
   * @param observation A set of axioms specifying the observation.
   *
   * @return A set of explanations. Each explanation contains
   *  a weakest set of axioms in some subsignature of the
   *  of the observation that, if added to the ontology, entails
   *  the observation. 
   */
  def abduce(observation: Set[_<:OWLAxiom],
             alwaysEliminate: Set[OWLEntity]): Set[Explanation] = {

    val obsSignature = asScalaSet(observation).flatMap(_.getSignature)

    // var start = new Date().getTime
    // println("Extracting module")
    // val moduleAx = ModuleExtractor.getModule(ontology, obsSignature)
    // println("time module extraction: "+(new Date().getTime-start))
    // start = new Date().getTime
    // val moduleOnt = new OWLExporter().toOwlOntology(moduleAx)
    // println("time module conversion")
    //val abducer = new OWLAbducerSignatureBased(interpolator, moduleOnt)
    //    val abducer = new OWLAbducerSignatureBased(interpolator, ontology)

    val answer = new HashSet[Explanation]()

    val allSignature = ontology.getOWLSignature--alwaysEliminate
    obsSignature.foreach{ element =>
      val start = new Date().getTime
      //      println("Eliminate: "+element.getIRI.getFragment)
      abducer.setAbducibles(allSignature-element)
      val nextExplanations = abducer.abduce(observation)
      //      println("time single abduction: "+(new Date().getTime-start))
      answer.addAll(nextExplanations)
    }

    answer
  }

  /**
   * Compute all explanations for the given observation.
   *
   * Each explanation entails the observation, if added to the ontology,
   * and uses only a restricted set of abducibles. The first explanations
   * computed are the result of eliminating one symbol occuring in the 
   * observation. Later explanations restrict the set of abducibles
   * allowed further, so that the explanations are stronger towards the 
   * end of the sequence.
   *
   * Since the resulting number of explanations is in the worst case 
   * exponential with respect to the signature of the ontology, an
   * iterator is returned which computes explanations "on demand".
   *
   * @param observation A set of axioms specifying the observation.
   *
   * @return An iterator providing a sequence of explanations with 
   *  increasing restrictions on the signature, such that each
   *  explanation entails the observation, if added to the ontology.
   */
  def abduceAll(observation: Set[_<:OWLAxiom]) = new ExplanationIterator {

    private var currentObservationSets:Queue[Explanation] =
      Queue().enqueue(Explanation(observation.toSet[OWLAxiom],
        new HashSet()))

    private var currentExplanations = Queue[Explanation]()


    private var processed = new HashSet[Explanation]()


    private var _next = Explanation(new HashSet(), new HashSet())
    private var _updated = false
    private var _hasNext = false

    override def hasNext() = {
      computeNext()
      _hasNext
    }

    override def next() = {
      computeNext()
      _updated = false
      _next
    }

    private def computeNext(): Explanation = {
      if(!_updated){
        while(currentExplanations.isEmpty && currentObservationSets.size>0){
          val (currentObservation, b) = currentObservationSets.dequeue
          currentObservationSets = b

          currentExplanations ++= abduce(currentObservation.getAxioms,
            currentObservation.eliminated)
          currentExplanations = currentExplanations.filterNot(processed.contains)
          currentExplanations.foreach{ e: Explanation =>
            currentObservationSets = currentObservationSets.enqueue(e)
          }
          processed ++= currentExplanations
        }

        if(currentExplanations.isEmpty){
          _hasNext = false
        } else{
          val (nextExplanation, b) = currentExplanations.dequeue

          currentExplanations = b
          _hasNext = true
          _next = nextExplanation
        }
        _updated = true
      }

      _next
    }
  }
}

/**
 * Iterator for Explanation objects.
 */
trait ExplanationIterator extends Iterator[Explanation] {
  override def hasNext(): Boolean
  override def next(): Explanation
}

/**
 * Class for performing abduction on OWL ontologies with a specified
 * signature of abducibles. An OWLAbducerSignatureBased is initialised
 * with an interpolator and an ontology, against which abductions are 
 * performed. Before abductions can be performed, a set of abducibles
 * has to be specified either by restricting the signature of the 
 * ontology or by giving it explicitly. 
 * Abducibles are concept symbols
 * we are willing to accept in the abduction result.
 *
 * Given a set of axioms, the "observation", abduction computes a set 
 * of axioms that only uses the specified abducibles in its signature 
 * and that, if added to the ontology, entail the observation. 
 * Furthermore, the logically weakest set of such axioms is computed,
 * that is, adding these axioms to the ontology only leads to as few
 * further entailments as necessary. The result of the abduction
 * operation, is called "explanation". 
 *
 * If the input ontology contains cycles, it is possible that the 
 * weakest possible abduction result cannot be represented finitely
 * in pure OWL. Different to forgetting and uniform interpolation,
 * this cannot be solved by using auxiliary symbols in the result.
 * Therefore, if such a case occurs, the result is approximated.
 *
 * Abduction results are computed by reducing the problem
 * to uniform interpolation. Therefore, the class is initiated with
 * an IOWLInterpolator object, which also determines the maximum
 * expressivity the OWLAbducer can deal with. In order to perform
 * abductions efficiently, the interpolator has to support a technique
 * called "uniform interpolation with background". Currently,
 * AlchTBoxInterpolatorC is the only class that supports this technique.
 *
 * @param interpolator The interpolator that is used by the abducer to
 *  perform abductions. Currently, only instances of the class 
 *  AlchTBoxInterpolatorC are supported.
 * @param ontology The ontology against which abductions are
 *  performed.
 *
 * @throws IllegalArgumentException If the given interpolator is not supported.
 */
class OWLAbducerSignatureBased(interpolator: IOWLInterpolator,
                               ontology: OptimisedOWLOntology) {

  def this(interpolator: IOWLInterpolator,
           ontology: OWLOntology) = this(interpolator,
    new OptimisedOWLOntology(ontology))

  OWLApiConfiguration.SIMPLIFIED_NAMES=false

  private var signature: Set[OWLEntity] = new HashSet()++ontology.getOWLSignature

  /**
   * If set to true, the abduction result is converted into cnf after computation,
   * which can lead to a more intuitive representation. Setting to false can
   * spead up the computation of abduction results.
   */
  var convertToCNF = true

  if(!interpolator.forgetter.isInstanceOf[ForgetterWithBackgroundKB[Ontology, String]])
    throw new IllegalArgumentException("Interpolator not supported: "+interpolator)


  val internalForgetter =
    interpolator.forgetter.asInstanceOf[ForgetterWithBackgroundKB[Ontology, String]]

  // Deactivate progressbar
  internalForgetter match{
    case f: ProgressBarAttached => f.progressBar = MockProgressBar
    case _ => ;
  }

  /**
   * Set the signature of abducibles, that is, the symbols that are
   * allowed in an explanation. Initially, the set of abducibles
   * equals the signature of the ontology, which makes abduction
   * trivial.
   *
   * @param abducibles The set of abducibles that can be used in an 
   *  explanation.
   */
  def setAbducibles(abducibles: Set[OWLEntity]) = {
    this.signature = abducibles
  }

  /**
   * Exclude set of concept or role symbols from the current set of
   * abducibles. In other words, specify a set of symbols that are
   * not allowed in an explanation. 
   *
   * @param abducibles A set of symbols that should be removed from
   *  the current set of abducibles. These symbols will not be used
   *  in the next abduction results computed.
   */
  def removeAbducibles(abducibles: Set[_<:OWLEntity]) = {
    this.signature.removeAll(abducibles)
  }

  def getAbducibles() = scalaSet()++signature

  /**
   * Computes a set of explanations, for the given observation axiom.
   * Each explanation contains a weakest set of axioms that only
   * use the specified set of abducibles and, if added to the 
   * ontology, would imply the observation. 
   *
   * @param observation The OWL axiom that should be entailed by the 
   *  explanation.
   *
   * @return A set of weakest explanations, where each explanation
   *  contains a set of axioms that only use the specified abducibles 
   *  and, if added to the ontology, entail the observation.
   */
  def abduce(observation: OWLAxiom): Set[Explanation] = {
    val obsSet = new HashSet[OWLAxiom]()
    obsSet.add(observation)
    abduce(obsSet)
  }


  /**
   * Computes a set of explanations, for the given observation axioms.
   * Each explanation contains a weakest set of axioms that only
   * use the specified set of abducibles and, if added to the 
   * ontology, would imply the observation. 
   *
   * @param observation The OWL axioms that should be entailed by the 
   *  explanation.
   *
   * @return A set of weakest explanations, where each explanation
   *  contains a set of axioms that only use the specified abducibles 
   *  and, if added to the ontology, entail the observation.
   */
  def abduce(observation: Set[_<:OWLAxiom])
  : Set[Explanation] = {
    val obsScala = asScalaSet(observation).toSet
    val axioms = obsScala.flatMap(OWLApiConverter.convert)

    val negatedTBox = negate(axioms)

    val sig = asScalaSet(signature).toSet
    val toForget = asScalaSet(ontology.getOWLSignature).toSet -- sig

    var start = new Date().getTime
    // val owlRelevantAxioms = toForget.flatMap(
    //   owlOntology.getReferencingAxioms(_)
    // )
    // println("time select relevant axioms: "+(new Date().getTime-start))

    // println("Convert "+new Date())
    // start = new Date().getTime
    // println("just translating")
    // owlRelevantAxioms.foreach(OWLApiConverter.convert)
    // println("time just converting: "+(new Date().getTime-start)) 

    start = new Date().getTime

    //    println("toForget: "+toForget.map(OWLApiConverter.getName))

    val relevantAxioms =
    //owlRelevantAxioms.flatMap(a => OWLApiConverter.convert(a)).map(_.asInstanceOf[Axiom])
      toForget.map(OWLApiConverter.getName).flatMap(ontology.getAxiomsForConcept)

    //    println("relevant: "+relevantAxioms)

    val relevantOnt = new Ontology()
    relevantOnt.tbox = new TBox(relevantAxioms)

    // val relevantOnt = Ontology.buildFrom(owlRelevantAxioms.flatMap(OWLApiConverter.convert))
    //    println("time convert relevant axioms: "+(new Date().getTime-start))

    //    println("Interpolate.... "+new Date())
    val interpolant = internalForgetter.forget(negatedTBox,
      toForget.map(OWLApiConverter.getName),
      relevantOnt)
    //    println("Done."+new Date())
    val explanation = backNegate(interpolant).map(_.filterNot(ontology.contains))

    val exporter = new OWLExporter()

    //    println("Convert to OWL"+new Date())
    val result = explanation.map( disjunct =>
      new Explanation(setAsJavaSet(disjunct.flatMap(exporter.toOwl(ontology.getOWLOntology, _))),
        toForget))
    //    println("Done"+new Date())

    setAsJavaSet[Explanation](result)
  }

  private val tboxRole = BaseRole("t")

  private def negate(statements: scalaSet[DLStatement]) = {
    val concept =
      ConceptConjunction(statements.map(ALCFormulaPreparations.toConcept))

    Ontology.buildFrom(scalaSet(Subsumption(TopConcept,
      ExistentialRoleRestriction(tboxRole,
        ConceptComplement(concept)))))
  }

  private def backNegate(ontology: Ontology)
  : scalaSet[scalaSet[DLStatement]] = {

    ontology.statements.toSet.map{ d: DLStatement => d match {
      case Subsumption(TopConcept,
      ExistentialRoleRestriction(r,
      concept)) if r==tboxRole => {
        val concept2 = definersToTop(concept) // approximate definers (hopefully still negated)
        minimiseAxiomLength(Subsumption(TopConcept,
          ConceptComplement(concept2)))
      }
      // case Subsumption(TopConcept,
      // 		       ConceptDisjunction(ds)) =>
      // 	ds.flatMap{ di => di match { 
      // 	  case ExistentialRoleRestriction(r,
      // 					  concept) if r==tboxRole => { 
      // 	    scalaSet(minimiseAxiomLength(Subsumption(TopConcept,
      // 						     ConceptComplement(concept))))
      // 	  }
      // 	  case _ => scalaSet[Axiom]()
      // 	}}
      case _ => scalaSet[DLStatement]()
    }}
  }

  private def minimiseAxiomLength(axiom: Axiom): scalaSet[DLStatement] = {
    var result = scalaSet[Axiom]()+axiom
    if(convertToCNF){
      //      println("CNF... "+new Date())
      result = axiom match {
        case Subsumption(a, b) => simpleCNF(b).map(Subsumption(a, _))
      }
      val clauses = ALCFormulaPreparations.clauses(scalaSet(axiom))
      val axioms2 = SimpleDefinerEliminator.eliminateDefiners(clauses).map(_.asInstanceOf[Axiom])
    }
    //    println("Beautify... "+new Date())
    var result2 = result.map(OntologyBeautifier.nice)
    //    println("Split axioms... "+new Date())
    result2 = result2.flatMap( _ match {
      case Subsumption(ConceptDisjunction(ds), b) => ds.map(Subsumption(_, b)).toSet[DLStatement]
      case c => scalaSet[DLStatement](c)
    })
    result2 = result2.flatMap(_ match {
      case Subsumption(a, ConceptConjunction(cs)) => cs.map(Subsumption(a,_)).toSet[DLStatement]
      case c => scalaSet[DLStatement](c)
    })
    //    println("Done. "+new Date())
    result2
  }

  private def definersToTop(concept: Concept): Concept = concept match {
    case d: BaseConcept if ALCFormulaPreparations.isDefiner(d) => TopConcept
    case TopConcept => TopConcept
    case BottomConcept => BottomConcept
    case b: BaseConcept => b
    case ConceptComplement(c) => ConceptComplement(definersToTop(c))
    case ConceptConjunction(cs) => ConceptConjunction(cs.map(definersToTop))
    case ConceptDisjunction(ds) => ConceptDisjunction(ds.map(definersToTop))
    case UniversalRoleRestriction(r,f) => UniversalRoleRestriction(r, definersToTop(f))
    case ExistentialRoleRestriction(r,f) => ExistentialRoleRestriction(r, definersToTop(f))
    case MinNumberRestriction(n,r,f) => MinNumberRestriction(n, r, definersToTop(f))
    case MaxNumberRestriction(n,r,f) =>
      MaxNumberRestriction(n, r, definersToTop(f))
    case _ => assert(false, "Definition should not be expanded in this expression: "+concept.toString); concept
  }

  private def simpleCNF(concept: Concept): scalaSet[Concept] = concept match {

    case ConceptDisjunction(ds) => {
      def inner(disjuncts: Set[Concept]): scalaSet[scalaSet[Concept]] = disjuncts.head match {
        case ConceptConjunction(cs) =>
          cs.flatMap(c =>
            inner(disjuncts.tail).map(ds2 => ds2+c))
        case c: Concept =>
          inner(disjuncts.tail).map(ds2 => ds2+c)
      }

      inner(ds).map(ConceptDisjunction).toSet[Concept]
    }
    case c => scalaSet(c)
  }

}

/**
 * Class for explanations computed as result of an abduction operation.
 * Consists of a set of axioms, which form the explanation. Furthermore,
 * objects of this class stores the symbols that have been eliminated in 
 * order to compute the explanation. Note that this is only done for 
 * documentation purposes and that the set of eliminated symbols cannot
 * be used to identify this object, which is reflected in the hashCode
 * and equals methods.
 *
 * @param explanation The set of axioms that form this explanation.
 * @param eliminated The set of symbols that have been eliminated in 
 *  order to compute this explanation. Note that this set is not 
 *  taken into consideration by hashCode and equals.
 */
case class Explanation(private val explanation: Set[OWLAxiom],
                       eliminated: Set[OWLEntity]) {

  /**
   * @return The set of axioms that form this explanation.
   */
  def getAxioms = explanation

  def this(explanation: Set[OWLAxiom]) =
    this(explanation, new HashSet[OWLEntity])

  override def hashCode = explanation.hashCode
  override def equals(other: Any) = other match {
    case Explanation(ex, el) => ex.equals(explanation)
    case _ => false
  }

  // val _hashCode = explanation.map(OWLApiConverter.convert(_).hashCode).sum
  // override def hashCode = _hashCode 
  // override def equals(other: Any) = other match { 
  //   case other: Explanation =>
  //     getAxioms.map(OWLApiConverter.convert(_)) == other.getAxioms.map(OWLApiConverter.convert(_))
  //   case _ => false
  // }
}
