"""
The Full Sponge Roll Problem for the PuLP Modeller

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


def calculatePatterns(totalRollLength, lenOpts, head):
    """
    Recursively calculates the list of options lists for a cutting stock problem. The input
    'tlist' is a pointer, and will be the output of the function call.

    The inputs are:
    totalRollLength - the length of the roll
    lenOpts - a list of the sizes of remaining cutting options
    head - the current list that has been passed down though the recusion

    Returns the list of patterns

    Authors: Bojan Blazevic, Dr Stuart Mitchell    2007
    """
    if lenOpts:
        patterns = []
        # take the first option off lenOpts
        opt = lenOpts[0]
        for rep in range(int(totalRollLength / opt) + 1):
            # reduce the length
            l = totalRollLength - rep * opt
            h = head[:]
            h.append(rep)

            patterns.extend(calculatePatterns(l, lenOpts[1:], h))
    else:
        # end of the recursion
        patterns = [head]
    return patterns


def makePatterns(totalRollLength, lenOpts):
    """
    Makes the different cutting patterns for a cutting stock problem.

    The inputs are:
    totalRollLength : the length of the roll
    lenOpts: a list of the sizes of cutting options as strings

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

    # calculatePatterns is called to create a list of the feasible cutting options in 'tlist'
    patterns = calculatePatterns(totalRollLength, lenOpts, [])

    # The list 'PatternNames' is created
    PatternNames = []
    for i in range(len(patterns)):
        PatternNames += ["P" + str(i)]

    # The amount of trim (unused material) for each pattern is calculated and added to the dictionary
    # 'trim', with the reference key of the pattern name.
    trim = {}
    for name, pattern in zip(PatternNames, patterns):
        ssum = 0
        for rep, l in zip(pattern, lenOpts):
            ssum += rep * l
        trim[name] = totalRollLength - ssum
    # The different cutting lengths are printed, and the number of each roll of that length in each
    # pattern is printed below. This is so the user can see what each pattern contains.
    print("Lens: %s" % lenOpts)
    for name, pattern in zip(PatternNames, patterns):
        print(name + "  = %s" % pattern)

    return (PatternNames, patterns, trim)


# Import PuLP modeler functions
from pulp import *

# The Total Roll Length is entered
totalRollLength = 20

# The cost of each 20cm long sponge roll used
cost = 1

# The sale value of each cm of trim
trimValue = 0.04

# A list of all the roll lengths is created
LenOpts = ["5", "7", "9"]

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

# The pattern names and the patterns are created as lists, and the associated trim with each pattern
# is created as a dictionary. The inputs are the total roll length and the list (as integers) of
# cutting options.
(PatternNames, patterns, trim) = makePatterns(
    totalRollLength, [int(l) for l in LenOpts]
)

# The RollData is made into separate dictionaries
(rollDemand, surplusPrice) = splitDict(rollData)

# The pattern data  is made into a dictionary so it can be called by patterns["7"]["P3"] for example.
# This will return the number of rolls of length "7" in pattern "P3"
patterns = makeDict([PatternNames, LenOpts], patterns, 0)

# The variable 'prob' is created
prob = LpProblem("Cutting Stock Problem", LpMinimize)

# The problem variables of the number of each pattern to make are created
pattVars = LpVariable.dicts("Patt", PatternNames, 0, None, LpInteger)

# The problem variables of the number of surplus rolls for each length are created
surplusVars = LpVariable.dicts("Surp", LenOpts, 0, None, LpInteger)

# The objective function is entered: (the total number of large rolls used * the cost of each) - (the value of the surplus stock) - (the value of the trim)
prob += (
    lpSum([pattVars[i] * cost for i in PatternNames])
    - lpSum([surplusVars[i] * surplusPrice[i] for i in LenOpts])
    - lpSum([pattVars[i] * trim[i] * trimValue for i in PatternNames]),
    "Net Production Cost",
)

# The demand minimum constraint is entered
for j in LenOpts:
    prob += (
        lpSum([pattVars[i] * patterns[i][j] for i in PatternNames]) - surplusVars[j]
        >= rollDemand[j],
        "Ensuring enough %s cm rolls" % j,
    )

# The problem data is written to an .lp file
prob.writeLP("SpongeRollProblem.lp")

# The problem is solved using PuLP's choice of Solver
prob.solve()

# The status of the solution is printed to the screen
print("Status:", LpStatus[prob.status])

# Each of the variables is printed with it's resolved optimum value
for v in prob.variables():
    print(v.name, "=", v.varValue)

# The optimised objective function value is printed to the screen
print("Production Costs = ", value(prob.objective))
