"""
Columnwise Column Generation Functions

Authors: Antony Phillips,  Dr Stuart Mitchell  2008
"""

# Import PuLP modeler functions
from pulp import *


class Pattern:
    """
    Information on a specific pattern in the SpongeRoll Problem
    """

    cost = 1
    trimValue = 0.04
    totalRollLength = 20
    lenOpts = ["5", "7", "9"]
    numPatterns = 0

    def __init__(self, name, lengths=None):
        self.name = name
        self.lengthsdict = dict(zip(self.lenOpts, lengths))
        Pattern.numPatterns += 1

    def __str__(self):
        return self.name

    def trim(self):
        return Pattern.totalRollLength - sum(
            [int(i) * int(self.lengthsdict[i]) for i in self.lengthsdict]
        )


def createMaster():

    rollData = {  # Length Demand SalePrice
        "5": [150, 0.25],
        "7": [200, 0.33],
        "9": [300, 0.40],
    }

    (rollDemand, surplusPrice) = splitDict(rollData)

    # The variable 'prob' is created
    prob = LpProblem("MasterSpongeRollProblem", LpMinimize)

    # The variable 'obj' is created and set as the LP's objective function
    obj = LpConstraintVar("Obj")
    prob.setObjective(obj)

    # The constraints are initialised and added to prob
    constraints = {}
    for l in Pattern.lenOpts:
        constraints[l] = LpConstraintVar("Min" + str(l), LpConstraintGE, rollDemand[l])
        prob += constraints[l]

    # The surplus variables are created
    surplusVars = []
    for i in Pattern.lenOpts:
        surplusVars += [
            LpVariable(
                "Surplus " + i,
                0,
                None,
                LpContinuous,
                -surplusPrice[i] * obj - constraints[i],
            )
        ]

    return prob, obj, constraints


def addPatterns(obj, constraints, newPatterns):

    # A list called Patterns is created to contain all the Pattern class
    # objects created in this function call
    Patterns = []
    for i in newPatterns:

        # The new patterns are checked to see that their length does not exceed
        # the total roll length
        lsum = 0
        for j, k in zip(i, Pattern.lenOpts):
            lsum += j * int(k)
        if lsum > Pattern.totalRollLength:
            raise ("Length Options too large for Roll")

        # The number of rolls of each length in each new pattern is printed
        print("P" + str(Pattern.numPatterns), "=", i)

        # The patterns are instantiated as Pattern objects
        Patterns += [Pattern("P" + str(Pattern.numPatterns), i)]

    # The pattern variables are created
    pattVars = []
    for i in Patterns:
        pattVars += [
            LpVariable(
                "Pattern " + i.name,
                0,
                None,
                LpContinuous,
                (i.cost - Pattern.trimValue * i.trim()) * obj
                + lpSum([constraints[l] * i.lengthsdict[l] for l in Pattern.lenOpts]),
            )
        ]


def masterSolve(prob, relax=True):

    # Unrelaxes the Integer Constraint
    if not relax:
        for v in prob.variables():
            v.cat = LpInteger

    # The problem is solved and rounded
    prob.solve(PULP_CBC_CMD())
    prob.roundSolution()

    if relax:
        # A dictionary of dual variable values is returned
        duals = {}
        for i, name in zip(Pattern.lenOpts, ["Min5", "Min7", "Min9"]):
            duals[i] = prob.constraints[name].pi
        return duals
    else:
        # A dictionary of variable values and the objective value are returned
        varsdict = {}
        for v in prob.variables():
            varsdict[v.name] = v.varValue

        return value(prob.objective), varsdict


def subSolve(duals):

    # The variable 'prob' is created
    prob = LpProblem("SubProb", LpMinimize)

    # The problem variables are created
    vars = LpVariable.dicts("Roll Length", Pattern.lenOpts, 0, None, LpInteger)

    trim = LpVariable("Trim", 0, None, LpInteger)

    # The objective function is entered: the reduced cost of a new pattern
    prob += (Pattern.cost - Pattern.trimValue * trim) - lpSum(
        [vars[i] * duals[i] for i in Pattern.lenOpts]
    ), "Objective"

    # The conservation of length constraint is entered
    prob += (
        lpSum([vars[i] * int(i) for i in Pattern.lenOpts]) + trim
        == Pattern.totalRollLength,
        "lengthEquate",
    )

    # The problem is solved
    prob.solve()

    # The variable values are rounded
    prob.roundSolution()

    newPatterns = []
    # Check if there are more patterns which would reduce the master LP objective function further
    if value(prob.objective) < -(10**-5):
        varsdict = {}
        for v in prob.variables():
            varsdict[v.name] = v.varValue
        # Adds the new pattern to the newPatterns list
        newPatterns += [
            [
                int(varsdict["Roll_Length_5"]),
                int(varsdict["Roll_Length_7"]),
                int(varsdict["Roll_Length_9"]),
            ]
        ]

    return newPatterns
