package pl.edu.pw.mini.sg.solver.scipsolver

import jscip._
import pl.edu.pw.mini.sg.solver.{Binary, Feasible, Infeasible, Integer, Maximize, ObjectiveType, Real, SolverModel, VariableType, _}
import squants.time.Time

class ScipSolverModel(scipModel: Scip) extends SolverModel[ScipVariable, ScipLinExprInner, ScipLinExpr, ScipConstraint] {

  implicit val scip: Scip = scipModel

  override def addVariable(lowerBound: Double, upperBound: Double, obj: Double, variableType: VariableType, name: String): ScipVariable = {
    new ScipVariable(scip.createVar(name, lowerBound, upperBound, obj, variableTypeToScipVarType(variableType)))
  }

  override def addVariables(lowerBounds: Seq[Double], upperBounds: Seq[Double], objs: Seq[Double], variableTypes: Seq[VariableType], names: Seq[String]): Array[ScipVariable] = {
    lowerBounds.indices.map(i => addVariable(lowerBounds(i), upperBounds(i), objs(i), variableTypes(i), names(i))).toArray
  }

  override def addVariables(lowerBounds: Double, upperBounds: Double, objs: Double, variableTypes: VariableType, names: Seq[String]): Array[ScipVariable] = {
    names.map(n => addVariable(lowerBounds, upperBounds, objs, variableTypes, n)).toArray
  }

  override def createLinExpr(): ScipLinExpr = {
    new ScipLinExpr()
  }

  override def addConstraint(lhsExpr: ScipLinExpr, constraintType: ConstraintType, rhsExpr: ScipLinExpr, name: String): ScipConstraint = {
    val (lhsConst, rhsConst) = createConstraintType(constraintType, rhsExpr.inner.constants.sum - lhsExpr.inner.constants.sum)
    val vars = (lhsExpr.inner.termVars ++ rhsExpr.inner.termVars).map(_.inner).toArray
    val vals = (lhsExpr.inner.termConsts ++ rhsExpr.inner.termConsts.map(-_)).toArray
    val cons = scip.createConsLinear(name, vars, vals, lhsConst, rhsConst)
    scip.addCons(cons)
    scip.releaseCons(cons)
    new ScipConstraint(cons)
  }

  override def addConstraint(lhsExpr: ScipLinExpr, constraintType: ConstraintType, rhsVariable: ScipVariable, name: String): ScipConstraint = {
    val (lhsConst, rhsConst) = createConstraintType(constraintType, 0 - lhsExpr.inner.constants.sum)
    val vars = (lhsExpr.inner.termVars :+ rhsVariable).map(_.inner).toArray
    val vals = (lhsExpr.inner.termConsts :+ -1.0).toArray
    val cons = scip.createConsLinear(name, vars, vals, lhsConst, rhsConst)
    scip.addCons(cons)
    scip.releaseCons(cons)
    new ScipConstraint(cons)
  }

  override def addConstraint(lhsExpr: ScipLinExpr, constraintType: ConstraintType, rhs: Double, name: String): ScipConstraint = {
    val (lhsConst, rhsConst) = createConstraintType(constraintType, rhs - lhsExpr.inner.constants.sum)
    val vars = lhsExpr.inner.termVars.map(_.inner).toArray
    val vals = lhsExpr.inner.termConsts.toArray
    val cons = scip.createConsLinear(name, vars, vals, lhsConst, rhsConst)
    scip.addCons(cons)
    scip.releaseCons(cons)
    new ScipConstraint(cons)
  }

  override def addConstraint(lhsVariable: ScipVariable, constraintType: ConstraintType, rhsVariable: ScipVariable, name: String): ScipConstraint = {
    val (lhsConst, rhsConst) = createConstraintType(constraintType, 0)
    val vars = List(lhsVariable, rhsVariable).map(_.inner).toArray
    val vals = Array(1.0, -1.0)
    val cons = scip.createConsLinear(name, vars, vals, lhsConst, rhsConst)
    scip.addCons(cons)
    scip.releaseCons(cons)
    new ScipConstraint(cons)
  }

  override def addConstraint(lhsVariable: ScipVariable, constraintType: ConstraintType, rhs: Double, name: String): ScipConstraint = {
    val (lhsConst, rhsConst) = createConstraintType(constraintType, rhs)
    val vars = List(lhsVariable).map(_.inner).toArray
    val vals = Array(1.0)
    val cons = scip.createConsLinear(name, vars, vals, lhsConst, rhsConst)
    scip.addCons(cons)
    scip.releaseCons(cons)
    new ScipConstraint(cons)
  }

  override def addConstraints(expr: Vector[ScipLinExpr], constraintType: Array[ConstraintType],
                              rhs: Array[Double], name: Array[String]) : Array[ScipConstraint] = {
    expr.view.zip(constraintType).zip(rhs).zip(name).map{
      case (((e, c), r), n) =>
        addConstraint(e, c, r, n)
    }.toArray
  }

  override def removeConstraint(constraint: ScipConstraint): Unit = ???

  override def setObjective(solverExpr: ScipLinExpr): Unit = {

    scip.setObjective(solverExpr.inner.termVars.map(_.inner).toArray,
      solverExpr.inner.termConsts.map(java.lang.Double.valueOf).toArray,
      true)
  }

  override def setObjectiveType(objectiveType: ObjectiveType): Unit = {
    if(objectiveType == Maximize)
      scip.setMaximize()
    else
      scip.setMinimize()
  }

  override def getObjective: Double = {
    scip.getSolOrigObj(scip.getBestSol)
  }

  override def optimize(): ModelSolutionType = {
    scip.solve()
    if(scip.getNSols == 0)
      return Infeasible
    Feasible
  }

  def optimize(timeout: Time) : ModelWithTimeoutSolutionType = {
    ???
  }


  override def update(): Unit = {
  // SCIP nie potrzebuje update aby dodac zmienne do solwera
  }

  override def close(): Unit = {
    scip.free()
  }

  private def variableTypeToScipVarType(variableType: VariableType): SCIP_Vartype = {
    variableType match {
      case Binary => SCIP_Vartype.SCIP_VARTYPE_BINARY
      case Integer => SCIP_Vartype.SCIP_VARTYPE_INTEGER
      case Real => SCIP_Vartype.SCIP_VARTYPE_CONTINUOUS
    }
  }

  private def createConstraintType(constraintType: ConstraintType, value: Double): (Double, Double) = {
    constraintType match {
      case LessEqual => (-scip.infinity(), value)
      case Equal => (value, value)
      case GreaterEqual => (value, scip.infinity())
    }
  }

}
