
package uk.ac.man.cs.lethe.internal.fol.unification

import com.typesafe.scalalogging.Logger

import uk.ac.man.cs.lethe.internal.fol.datatypes._

// object VariableBuilder {
//     var counter = 0
//     def newVariable(): Variable = {
//     	counter = counter + 1
// 	new Variable("_x"+counter)
//     }
// }


object UnificationLogger { 
//  implicit val (logger, formatter, appender) =
//  ZeroLoggerFactory.newLogger(this)
  val logger = Logger(UnificationLogger.getClass)
}

class InvalidSubstitutionException(message: String) extends Exception(message)

class GeneralReplacement(term1: Term, term2: Term) {

  import UnificationLogger._

  def apply[T <: Expression](expr: T): T = { 
    UnificationLogger.logger.trace("replacing " + term1.toString + " with " + term2.toString + " in " + expr.toString)
    def inner = expr match {
      case term: Term if term==term1 => term2
      case Function(n, args) => new Function(n, args.map(apply))
      case IdentityPredicate(a1, a2) => new IdentityPredicate(apply(a1), apply(a2))
      case GenericPredicate(n, args) => new GenericPredicate(n, args.map(apply))
      case Literal(positive, predicate) => new Literal(positive, apply(predicate))
      case Clause(literals) => new Clause(literals.map(apply))
      case Negation(f) => new Negation(apply(f))
      case Disjunction(fs) => new Disjunction(fs.map(apply))
      case Conjunction(fs) => new Conjunction(fs.map(apply))
      case Implication(f1, f2) => new Implication(apply(f1),apply(f2))
      case Equivalence(f1, f2) => new Equivalence(apply(f1), apply(f2))
      case ExistentialQuantification(v,f) if v==term1 && term2.isInstanceOf[Variable] => 
	new ExistentialQuantification(term2.asInstanceOf[Variable], apply(f)).asInstanceOf[T]
      case UniversalQuantification(v,f) if v==term1 && term2.isInstanceOf[Variable] =>
	new UniversalQuantification(term2.asInstanceOf[Variable], apply(f))
      case ExistentialQuantification(v,f) if v != term1 => new ExistentialQuantification(v, apply(f))
      case UniversalQuantification(v,f) if v != term1 => new UniversalQuantification(v, apply(f))
      case _: ExistentialQuantification => throw new InvalidSubstitutionException("Tried to replace quantifier variable with "+term2)
      case _: UniversalQuantification => throw new InvalidSubstitutionException("Tried to replace quantifier variable with "+term2)
      case _ => expr
    }

    inner.asInstanceOf[T]
  }

}

case class Substitut(variable: Variable, term: Term) extends GeneralReplacement(variable, term) { 
  override def toString = variable+"/"+term
}

case class GeneralSubstitution(var substituts: Set[GeneralReplacement]) { 
  def apply[T <: Expression](expr: T): T = {
    var result: T = expr
    substituts foreach { s => result = s.apply(result).asInstanceOf[T] }
    result
  }
  
}


class Substitution(var substituts: Set[Substitut]) {
  def apply[T <: Expression](expr: T): T = {
    var result: T = expr
    substituts foreach { s => result = s.apply(result).asInstanceOf[T] }
    result
  }
  
  def getSubstitute(v: Variable): Option[Term] = 
    substituts collectFirst{ case x if x.variable == v => x.term }

  def addSubstitute(v: Variable, exp: Term) = {
    substituts = substituts + new Substitut(v, exp)


    // apply new substitut
    substituts = substituts map ( s => new Substitut(s.variable, apply(s.term).asInstanceOf[Term]))
  }

  def addPassive(v: Variable, exp: Term) = { 
    substituts = substituts + new Substitut(v, exp)    
  }
  
  def copy = new Substitution(substituts)
  
  override def toString = substituts.mkString("{", ", ", "}")
}

class NotMatchableException(message: String) extends Exception(message) { 
  def this() = this("")
}

object Unifier {
//  implicit val (logger, formatter, appender) =
//  ZeroLoggerFactory.newLogger(this)
  val logger = Logger(Unifier.getClass)

  def unifies(exp1: Expression, exp2: Expression): Boolean = try { 
    unify(exp1, exp2)
    true
  } catch { 
    case _: NotMatchableException => false
    case e: Throwable => throw e
  }


  def unify(exp1: Expression, exp2: Expression): Substitution = {
    logger.trace(exp1.toString)
    logger.trace(exp2.toString)
    
    val subst: Substitution = new Substitution(Set())
    
    unify(exp1,exp2, subst)

    logger.trace("Unification result: "+subst)
    subst
  }    

  // Unifies by using common substituion object
  private def unify(e1:Expression, e2:Expression, subst: Substitution): Unit = (e1, e2) match { 
    case _ if e1==e2 => None
    case (v1: Variable, v2: Variable) => {
      val s = subst.getSubstitute(v1)
      if(s==None || s.get != v2){
	subst.addSubstitute(v1, v2)
	subst.addSubstitute(v2, v1)
      }
    }
    case (v1: Variable, t2: Term) => {
      logger.trace(v1.toString + " with " + t2.toString)
      val t2s:Term = subst.apply(t2).asInstanceOf[Term]
      val s = subst.getSubstitute(v1)
      if(s==None)
	subst.addSubstitute(v1,t2s)
      else if(s!=t2s)
	throw new NotMatchableException()
    }
    case (t1: Term, v2: Variable) => {
      logger.trace(t1.toString + " with " + v2.toString)
      val t1s = subst.apply(t1).asInstanceOf[Term]
      val s = subst.getSubstitute(v2)
      if(s==None)
	subst.addSubstitute(v2,t1s)
      else if(s!=t1s)
	throw new NotMatchableException()
    }
    case (Function(n1, args1), Function(n2, args2)) if n1==n2 => {
      args1.zip(args2) foreach { x => unify(x._1, x._2, subst) } 	     	 
    }
    case (GenericPredicate(n1, args1), GenericPredicate(n2, args2)) if n1==n2 => {
      args1.zip(args2) foreach { x => unify(x._1, x._2, subst) } 	     	 
    } 
    case (IdentityPredicate(a1, b1), IdentityPredicate(a2, b2)) => {
      unify(a1, a2, subst)
      unify(b1, b2, subst)
    } 
    case (Literal(pos1, pred1), Literal(pos2, pred2)) if pos1==pos2 => {
      unify(pred1, pred2, subst)
    } 
    case (Clause(l1), Clause(l2)) => {
      assert(false, "Not implemented!")
    }
    case _ => throw new NotMatchableException()
  }	 

  // ensures by replacing variables, that each occuring variable is unique 
  def makeVarsUnique[T <: Expression](expr: T): (Substitution, T) = {
    val subst = new Substitution(Set())
    
    def inner(exp2: Expression): Expression = exp2 match {
      case v: Variable => {
	val s = subst.getSubstitute(v)
	if(s!=None){
	  s.get
	}
	else {
	  val newV = VariableBuilder.newVariable()
	  subst.addSubstitute(v, newV)
	  newV
	}   
      }
      case Function(n, args) => new Function(n, args.map{ inner(_).asInstanceOf[Term] })
      case GenericPredicate(n, args) => new GenericPredicate(n, args map{ inner(_).asInstanceOf[Term] })
      case IdentityPredicate(arg1, arg2) => new IdentityPredicate(inner(arg1).asInstanceOf[Term], inner(arg2).asInstanceOf[Term])
      case Literal(positive, predicate) => new Literal(positive, inner(predicate).asInstanceOf[Predicate])
      case Clause(literals) => new Clause(literals map{ inner(_).asInstanceOf[Literal] })
      case _ => exp2 
    }
    
    (subst, inner(expr).asInstanceOf[T])
  }
}

// Objekt to simplify variable names
object VariableSimplifier { 
  def simplify[T <: Formula](f: T) = { 
    var varMap: Map[Variable, Variable] = Map()
    VariableBuilder.reinit()

    def inner[T <: Expression](f: T): T = f match { 
      case v: Variable if varMap.contains(v) => varMap(v).asInstanceOf[T]
      case v: Variable => 
	varMap = varMap.updated(v, VariableBuilder.newVariable)
	varMap(v).asInstanceOf[T]
      case c: Constant => c.asInstanceOf[T].asInstanceOf[T]
      case Function(f, args) => Function(f, args.map(inner)).asInstanceOf[T]
      case GenericPredicate(n, args) => GenericPredicate(n, args.map(inner)).asInstanceOf[T]
      case IdentityPredicate(a, b) => IdentityPredicate(inner(a), inner(b)).asInstanceOf[T]
      case s: SimpleProposition => s.asInstanceOf[T]
      case Literal(p, pr) => Literal(p, inner(pr)).asInstanceOf[T]
      case Negation(f) => inner(f).asInstanceOf[T]
      case Disjunction(ds) => Disjunction(ds.map(inner)).asInstanceOf[T]
      case Conjunction(cs) => Conjunction(cs.map(inner)).asInstanceOf[T]
      case Implication(a, b) => Implication(inner(a), inner(b)).asInstanceOf[T]
      case Equivalence(a, b) => Equivalence(inner(a), inner(b)).asInstanceOf[T]
     
      case Clause(cs) => Clause(cs.map(inner)).asInstanceOf[T]
    }
    inner(f)
  }
}
