package uk.ac.man.cs.lethe.internal.forgetting.generalResolution

import scala.collection.immutable.Queue

import uk.ac.man.cs.lethe.internal.fol.datatypes._
import uk.ac.man.cs.lethe.internal.forgetting.Forgetter
import uk.ac.man.cs.lethe.internal.fol.unification.{ Unifier, NotMatchableException }
import uk.ac.man.cs.lethe.internal.normalForms.Skolemizer

import com.typesafe.scalalogging.Logger

object GeneralResolutionForgetter extends Forgetter[Formula, String] { 

//  implicit val (logger, formatter, appender) = ZeroLoggerFactory.newLogger(this)

  val logger = Logger(GeneralResolutionForgetter.getClass)

  var nonBaseSymbols: Set[String] = _

  var newFormulas: Queue[Formula] = _

  var oldFormulas: List[Formula] = _

  var result: List[Formula] = _

  var count: Int = _

  override def steps = count

  override def forget(formula: Formula, symbols: Set[String]) = { 

    logger.info("General Resolution is applied to forget the following symbols: ")
    logger.info(symbols.mkString(" - ", "\n - ", "\n\n"))

    count = 0

    nonBaseSymbols = symbols
    
    result = List()

    Skolemizer.skolemize(formula) match { 
      case Conjunction(cs) => newFormulas = Queue() ++ cs
      case _ => newFormulas = Queue(formula)
    }
    
    oldFormulas = List()

    run()

    logger.info("Resolution result:")
    logger.info(result.mkString(" - ", "\n - ", "\n"))
    
    FolHelpers.conjunction(result)
  }

  def run(){ 
    while(!newFormulas.isEmpty){ 
      val currentFormula = newFormulas.head

      logger.debug("Current Formula: " + currentFormula.toString)
      newFormulas = newFormulas.tail

      if(hasNonBaseSymbols(currentFormula)) { 
	val selected = getSelectedPredicates(currentFormula)
	logger.debug("Selected atoms: " + selected.mkString(", "))
	if(!selected.isEmpty) 
	  selected.foreach(applyResolutions(currentFormula, _))
	else { 
	  getMaximalPredicate(currentFormula).foreach{ max => 
	      logger.debug("Maximal atom: "+max.toString)
	      applyResolutions(currentFormula, max)
	      newFormulas = newFormulas.enqueue(applySelfResolution(currentFormula, max))
	  }
	}
        oldFormulas = currentFormula::result
      }
      else 
	result = currentFormula::result
    }
  }

  def applyResolutions(currentFormula: Formula, p1: Predicate) = 
    (newFormulas++oldFormulas).foreach { f =>
      if(getSelectedPredicates(f).isEmpty)
	getMaximalPredicate(f).foreach{ p2 => 
	  if(p1.name==p2.name) {  
	    try { 
	      val subst = Unifier.unify(p1, p2)
	      newFormulas = newFormulas enqueue applyResolution(subst.apply(currentFormula), 
								subst.apply(f), 
								subst.apply(p1))
	    } catch { 
	      case _: NotMatchableException => 
	      case e: Throwable => throw e
	    }
	  }
	}
     }

  private def hasNonBaseSymbols(formula: Formula): Boolean = 
    !formula.signature.intersect(nonBaseSymbols).isEmpty

  def getMaximalPredicate(formula: Formula): Option[Predicate] = formula match { 
    case Literal(_, p) if nonBaseSymbols.contains(p.name) => Some(p)
    case Negation(f) => getMaximalPredicate(f)
    case Disjunction(ds) if !ds.isEmpty => Some(ds.flatMap(getMaximalPredicate(_)).max)
    case Conjunction(cs) if !cs.isEmpty => Some(cs.flatMap(getMaximalPredicate(_)).max)
    case Implication(a, b) => Some(List(getMaximalPredicate(a), getMaximalPredicate(b)).flatten.max)
    case Equivalence(a, b) => Some(List(getMaximalPredicate(a), getMaximalPredicate(b)).flatten.max)
    case _ => None
  }

  def getSelectedPredicates(formula: Formula): Set[Predicate] = { 
    var result: Set[Predicate] = Set()

    def inner(formula: Formula, marking: Boolean): Unit = (formula, marking) match { 
      case (Literal(true, pred), false) => if(nonBaseSymbols.contains(pred.name)) 
	result = result + pred
      case (Literal(false, pred), true) => if(nonBaseSymbols.contains(pred.name)) 
	result = result + pred
      case (Negation(f), marking) => inner(f, !marking)
      case (Disjunction(ds), true) => ds.foreach(d => inner(d, true))
      case (Conjunction(cs), false) => cs.foreach(c => inner(c, false))
      case (Implication(a, b), true) => inner(a, false); inner(b, true)
      case _ => return
    }    

    inner(formula, true)
    result
  }

  def applyResolution(formula1: Formula, formula2: Formula, pred: Predicate) = { 
    logger.debug("applying resolution on atom "+pred.toString+" on ")
    logger.debug("  " + formula1.toString)
    logger.debug("  " + formula2.toString)

    val result = Disjunction(Set(changeLiteral(formula1, pred, FolHelpers.True), changeLiteral(formula2, pred, FolHelpers.False)))

    logger.debug("result: " + result.toString)

    val simplified = simplify(result)

    logger.debug("simplified: " + simplified.toString)

    count += 1

    simplified
  }

  def applySelfResolution(formula: Formula, pred: Predicate) = { 
    logger.debug("applying self-resolution on atom "+pred.toString+" on ")
    logger.debug("  " + formula.toString)

    val result = Disjunction(Set(changeLiteral(formula, pred, FolHelpers.True), changeLiteral(formula, pred, FolHelpers.False)))

    logger.debug("result: " + result.toString)

    val simplified = simplify(result)

    logger.debug("simplified: " + simplified.toString)

    count += 1

    simplified
  }

  /**
   * Simplifies formula by removing every true or false value it finds
   */
  def simplify(formula: Formula): Formula = formula match{ 
    case l: Literal => l
    case Implication(a1, Equivalence(a2, b)) if a1==a2 => simplify(Implication(a1, b))
    case Implication(a1, Implication(a2, b)) if a1==a2 => simplify(Implication(a1, b))
    case Implication(a, b) =>
      (simplify(a), simplify(b)) match { 
	case (a, b) if a == FolHelpers.True  => b
	case (a, b) if b == FolHelpers.True  => FolHelpers.True
	case (a, b) if a == FolHelpers.False => FolHelpers.True 
	case (a, b) if b == FolHelpers.False => simplify(Negation(a))
	case (a, b) if a == b => FolHelpers.True
	case (Implication(a1, a2), Implication(b1, b2)) if a2==FolHelpers.False && b1==FolHelpers.True => simplify(Implication(a1, b2))
	case (Implication(a1, a2), Implication(b1, b2)) if b2==FolHelpers.False && a1==FolHelpers.True => simplify(Implication(b1, a2))
	case (a1, Implication(a2, b)) => simplify(Implication(Conjunction(Set(a1, a2)), b))
	case (a, b) => Implication(a, b)
      }
    case Equivalence(a, b) =>
      (simplify(a), simplify(b)) match { 
	case (a, b) if a == FolHelpers.True  => b
	case (a, b) if b == FolHelpers.True  => a
	case (a, b) if a == FolHelpers.False => simplify(Negation(b))
	case (a, b) if b == FolHelpers.False => simplify(Negation(a))
	case (a, b) => Equivalence(a, b)
      }
    case Negation(Negation(f)) => simplify(f)
    case Negation(Literal(p, l)) => Literal(!p, l)
    case Negation(f) => Negation(simplify(f))
    case Disjunction(ds) => 
      val newDs = ds.map(simplify)
      if(newDs.size==1)
	newDs.head
      else if(newDs.contains(FolHelpers.True))
	FolHelpers.True
      else
	implication(newDs)
    case Conjunction(cs) => 
      val newCs = cs.map(simplify)
      if(newCs.size==1)
	newCs.head
      else if(newCs.contains(FolHelpers.False))
	FolHelpers.False
      else
	FolHelpers.conjunction(newCs)
  }

  /**
   * Tries to represent a set of disjuncts as a implication, and
   * returns the disjunction (or a simplification it) if it's not
   * possible in a nice way.
   */
  def implication(disjuncts: Iterable[Formula]): Formula = { 
    val (leftHand, rightHand) = disjuncts.partition(_ match { 
      case Literal(false, l) => true
      case Negation(f) => true
      case f => false
    })
    logger.trace("left hand formulas of possible implication in " + disjuncts.mkString(" || ") + ": " + leftHand.mkString(", "))
    logger.trace("right hand formulas of possible implication in " + disjuncts.mkString(" || ") + ": "  + rightHand.mkString(", "))
    if(leftHand.isEmpty || rightHand.isEmpty)
      FolHelpers.disjunction(disjuncts)
    else
      simplify(Implication(FolHelpers.conjunction(leftHand.map(FolHelpers.negate)), FolHelpers.disjunction(rightHand)))
  }
  
  def changeLiteral(formula: Formula, p: Predicate, f2: Formula): Formula = formula match { 
    case Literal(true, pred) if pred==p => f2
    case Literal(false, pred) if pred==p => Negation(f2)
    case _: Literal => formula 
    case Negation(f) => Negation(changeLiteral(f, p, f2))
    case Disjunction(ds) => Disjunction(ds.map(changeLiteral(_, p, f2)))
    case Conjunction(cs) => Conjunction(cs.map(changeLiteral(_, p, f2)))
    case Implication(a, b) => Implication(changeLiteral(a, p, f2), changeLiteral(b, p, f2))
    case Equivalence(a, b) => Equivalence(changeLiteral(a, p, f2), changeLiteral(b, p, f2))
  }

}
