/**
 * Copyright (C) 2019 Patrik Dufresne Service Logiciel <info@patrikdufresne.com>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
package com.planimod.core.planif;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.patrikdufresne.ilp.IBranchingTechniqueLastAlwaysDown;
import com.patrikdufresne.ilp.IFeasibilityPumpHeuristic;
import com.patrikdufresne.ilp.SolverOption;
import com.patrikdufresne.ilp.util.ValueSnapshot;

/**
 * Abstract class providing a structure to solve a linear problem.
 * 
 * @author Patrik Dufresne
 */
@SuppressWarnings("unqualified-field-access")
abstract class SolveRunnable {

    private static final transient Logger LOGGER = LoggerFactory.getLogger(SolveRunnable.class);

    protected boolean feasible;

    protected SolverOption option;

    /**
     * Reference to the problem builder.
     */
    protected final ProblemBuilder pb;

    public static final Double ONE = Double.valueOf(1);

    public static final Double ZERO = Double.valueOf(0);

    /**
     * Create the solver with default option.
     * 
     * @param pb
     *            reference to ProblemBuilder
     */
    public SolveRunnable(ProblemBuilder problemBuilder) {
        if (problemBuilder == null) {
            throw new IllegalArgumentException();
        }
        this.pb = problemBuilder;
        this.option = pb.solver.createSolverOption();
    }

    /**
     * Used to restore constraints previously destroyed by a call to {@link #release()}.
     * <p>
     * Sub-classes must implement this function to create the constraint again.
     * <p>
     * This implementation always throw an exception.
     */
    public void engage() {
        throw new UnsupportedOperationException();
    }

    /**
     * Used to restore a constraint previously release.
     */
    public void engageOrRun() {
        throw new UnsupportedOperationException();
    }

    /**
     * Sub-classes may implement this function to execute code after the solver determine there is a feasible solution.
     */
    protected void feasible() throws GeneratePlanifException {
        // Nothing to do
    }

    /**
     * Return True if the problem is feasible.
     * 
     * @return
     */
    public boolean isFeasible() {
        return this.feasible;
    }

    /**
     * Used to remove any constraints created by a previous run.
     * <p>
     * Sub-classes should implement this function to dispose any created constraints and store a copy of it to be
     * re-engage. It is recommended to use constraint snapshot.
     * <p>
     * This implementation always throw an exception.
     */
    public void release() {
        throw new UnsupportedOperationException();
    }

    /**
     * Run the solver.
     */
    public boolean run() throws GeneratePlanifException {
        // Setup the solver
        if (setUp()) {
            // Run the solver if required.
            this.feasible = runSolver();
        }
        // Fork depending of the feasibility.
        if (this.feasible) {
            feasible();
        } else {
            unfeasible();
        }
        tearDown();
        return this.feasible;
    }

    /**
     * Execute the solve operation on the linear problem.
     * 
     * @return True if the problem is feasible.
     * @throws GeneratePlanifException
     */
    protected boolean runSolver() throws GeneratePlanifException {
        boolean feasible = this.pb.solver.solve(this.pb.lp, this.option);
        if (feasible) {
            takeSnapshot();
        }
        return feasible;
    }

    /**
     * Sets the branching last always down technique.
     * 
     * @param enabled
     * @return
     */
    protected void setBranchingLastAlwaysDown(boolean enabled) {
        if (!(option instanceof IBranchingTechniqueLastAlwaysDown)) {
            return;
        }
        ((IBranchingTechniqueLastAlwaysDown) option).setBranchingLastAlwaysDown(enabled);
    }

    /**
     * Sets the feasibility pump heuristic if the solver support it.
     * 
     * @param enabled
     *            True to enable, False to disable, null to do nothing
     * @return the previous value
     */
    protected void setFeasibilityPumpHeuristic(boolean enabled) {
        if (!(option instanceof IFeasibilityPumpHeuristic)) {
            return;
        }
        ((IFeasibilityPumpHeuristic) option).setFeasibilityPumpHeuristic(enabled);
    }

    /**
     * Sub-classes may implement this function to execute code before the solver run. i.e.: set constraint or objective
     * 
     * @return True to continue, False to avoid running the solver. Sub-classes must sets the feasible field.
     */
    protected boolean setUp() throws GeneratePlanifException {
        return true;
    }

    /**
     * Take a new snapshot of the feasible solution.
     */
    protected void takeSnapshot() {
        pb.snapshot = ValueSnapshot.create(pb.lp.getVariables());
    }

    /**
     * Sub-classes may implement this function to execute code after the solver run.
     */
    protected void tearDown() throws GeneratePlanifException {
        // Nothing to do
    }

    /**
     * Used to throw new exception when the problem is un-feasible but it should be feasible.
     * 
     * @throws GeneratePlanifException
     */
    protected void throwProblemUnFeasible() throws GeneratePlanifException {
        GeneratePlanifException e = new GeneratePlanifException("Problem is unfeasible.", GeneratePlanifException.UNFEASIBLE);
        LOGGER.warn("problem is unfeasible", e);
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug(this.pb.lp.toString());
        }
        throw e;
    }

    /**
     * Sub-classes may implement this function to run code after the solver determine there is no feasible solution.
     */
    protected void unfeasible() throws GeneratePlanifException {
    }
}