/*
 * Copyright 1997-2025 Optimatika
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.ojalgo.optimisation;

import static org.ojalgo.function.constant.PrimitiveMath.ZERO;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

import org.ojalgo.ProgrammingError;
import org.ojalgo.array.NumberList;
import org.ojalgo.array.SparseArray;
import org.ojalgo.function.multiary.MultiaryFunction;
import org.ojalgo.function.multiary.MultiaryFunction.TwiceDifferentiable;
import org.ojalgo.matrix.store.MatrixStore;
import org.ojalgo.matrix.store.PhysicalStore;
import org.ojalgo.matrix.store.PhysicalStore.Factory;
import org.ojalgo.matrix.store.R064Store;
import org.ojalgo.matrix.store.RowsSupplier;
import org.ojalgo.matrix.store.SparseStructure2D;
import org.ojalgo.netio.BasicLogger;
import org.ojalgo.structure.Access1D;
import org.ojalgo.structure.Access2D;
import org.ojalgo.structure.Access2D.RowView;
import org.ojalgo.structure.ElementView2D;
import org.ojalgo.structure.Mutate1D;
import org.ojalgo.structure.Mutate2D;
import org.ojalgo.type.CalendarDateDuration;
import org.ojalgo.type.CalendarDateUnit;
import org.ojalgo.type.Stopwatch;

public abstract class GenericSolver implements Optimisation.Solver {

    public static abstract class Builder<B extends Builder<B, S>, S extends GenericSolver> implements Optimisation.ProblemStructure {

        private static final Factory<Double, R064Store> FACTORY = R064Store.FACTORY;

        /**
         * Converts any {@link Access2D} into a {@link RowsSupplier} – one {@link SparseArray} row per matrix
         * row, storing nonzero elements only. This is the common "data structure containing SparseArray" that
         * all the constraint-adding methods produce before merging it into {@code myAE} / {@code myAI}.
         */
        private static RowsSupplier<Double> toRowsSupplier(final Access2D<?> matrix) {

            int nbRows = matrix.getRowDim();
            int nbCols = matrix.getColDim();

            RowsSupplier<Double> block = FACTORY.makeRowsSupplier(nbCols);
            block.addRows(nbRows);

            if (matrix instanceof SparseStructure2D) {

                for (ElementView2D<?, ?> nz : matrix.nonzeros()) {
                    block.getRow(Math.toIntExact(nz.row())).set(nz.column(), nz.doubleValue());
                }

            } else {

                for (int i = 0; i < nbRows; i++) {
                    SparseArray<Double> row = block.getRow(i);
                    for (int j = 0; j < nbCols; j++) {
                        double value = matrix.doubleValue(i, j);
                        if (value != ZERO) {
                            row.set(j, value);
                        }
                    }
                }
            }

            return block;
        }

        protected static final void append(final StringBuilder builder, final String label, final MatrixStore<Double> matrix) {
            if (builder != null && label != null && matrix != null) {
                builder.append("\n[");
                builder.append(label);
                builder.append("] = ");
                builder.append(Access2D.toString(matrix));
            }
        }

        /**
         * Assumed constrained to be <= 0.0
         */
        private Map<String, MultiaryFunction.TwiceDifferentiable<Double>> myAdditionalConstraints = null;
        private RowsSupplier<Double> myAE = null;
        private RowsSupplier<Double> myAI = null;
        private NumberList<Double> myBE = null;
        private NumberList<Double> myBI = null;
        private R064Store myLowerBounds = null;
        private transient int myNumberOfVariables = -1;
        private MultiaryFunction.TwiceDifferentiable<Double> myObjective = null;
        private R064Store myUpperBounds = null;

        protected Builder() {
            super();
        }

        public final S build() {
            this.validate();
            return this.doBuild(new Optimisation.Options());
        }

        public final S build(final Optimisation.Options options) {
            ProgrammingError.throwIfNull(options);
            this.validate();
            return this.doBuild(options);
        }

        @Override
        public int countAdditionalConstraints() {
            return myAdditionalConstraints != null ? myAdditionalConstraints.size() : 0;
        }

        @Override
        public int countEqualityConstraints() {
            return myAE != null ? myAE.getRowDim() : 0;
        }

        @Override
        public int countInequalityConstraints() {
            return myAI != null ? myAI.getRowDim() : 0;
        }

        @Override
        public int countVariables() {
            if (myNumberOfVariables < 0) {
                myNumberOfVariables = this.doCountVariables();
            }
            return myNumberOfVariables;
        }

        public void reset() {

            if (myAdditionalConstraints != null) {
                myAdditionalConstraints.clear();
            }

            myAdditionalConstraints = null;
            myAE = null;
            myAI = null;
            myBE = null;
            myBI = null;
            myLowerBounds = null;
            myObjective = null;
            myUpperBounds = null;

            myNumberOfVariables = -1;
        }

        public final Optimisation.Result solve() {
            return this.build().solve().withSolutionLength(this.countVariables());
        }

        @Override
        public final String toString() {

            String simpleName = this.getClass().getSimpleName();

            StringBuilder retVal = new StringBuilder();

            retVal.append("<");
            retVal.append(simpleName);
            retVal.append(">");

            this.append(retVal);

            retVal.append("\n</");
            retVal.append(simpleName);
            retVal.append(">");

            return retVal.toString();
        }

        protected void append(final StringBuilder builder) {
            Builder.append(builder, "AE", this.getAE());
            Builder.append(builder, "BE", this.getBE());
            Builder.append(builder, "AI", this.getAI());
            Builder.append(builder, "BI", this.getBI());
            Builder.append(builder, "C", this.getC());
        }

        protected abstract S doBuild(Optimisation.Options options);

        protected int doCountVariables() {

            if (myObjective != null) {
                return myObjective.arity();
            }

            if (myAE != null) {
                return myAE.getColDim();
            }

            if (myAI != null) {
                return myAI.getColDim();
            }

            throw new ProgrammingError("Cannot deduce the number of variables!");
        }

        /**
         * Appends the equality constraints {@code [mtrxAE][X] == [mtrxBE]} (one row each).
         */
        public final B equalities(final Access2D<?> mtrxAE, final Access1D<?> mtrxBE) {
            ProgrammingError.throwIfNull(mtrxAE, mtrxBE);
            ProgrammingError.throwIfNotEqualRowDimensions(mtrxAE, mtrxBE);
            this.mergeEqualities(Builder.toRowsSupplier(mtrxAE), mtrxBE);
            return (B) this;
        }

        /**
         * Appends {@code rhs.length} equality constraints with the given right-hand side; returns a
         * {@link Mutate2D} for filling in the (initially empty) constraint bodies.
         */
        public final Mutate2D equalities(final double[] rhs) {
            RowsSupplier<Double> block = FACTORY.makeRowsSupplier(this.countVariables());
            block.addRows(rhs.length);
            this.mergeEqualities(block, R064Store.wrap(rhs));
            return block;
        }

        /**
         * Appends a single equality constraint with the given right-hand side; returns a {@link Mutate1D} for
         * filling in the (initially empty) constraint body.
         */
        public final Mutate1D equality(final double rhs) {
            RowsSupplier<Double> block = FACTORY.makeRowsSupplier(this.countVariables());
            SparseArray<Double> row = block.addRow();
            this.mergeEqualities(block, FACTORY.makeSingle(rhs));
            return row;
        }

        /**
         * Appends the single equality constraint {@code [factors][X] == rhs}.
         */
        public final B equality(final double rhs, final double... factors) {

            RowsSupplier<Double> block = FACTORY.makeRowsSupplier(this.countVariables());
            SparseArray<Double> row = block.addRow();
            for (int i = 0, limit = Math.min(factors.length, this.countVariables()); i < limit; i++) {
                row.set(i, factors[i]);
            }

            this.mergeEqualities(block, FACTORY.makeSingle(rhs));

            return (B) this;
        }

        /**
         * Equality constraints body: [AE][X] == [BE]
         */
        protected MatrixStore<Double> getAE() {
            if (myAE != null) {
                return myAE;
            } else {
                return FACTORY.makeZero(0, this.countVariables());
            }
        }

        protected SparseArray<Double> getAE(final int row) {
            return myAE.getRow(row);
        }

        protected RowsSupplier<Double> getAE(final int... rows) {
            return myAE.selectRows(rows);
        }

        /**
         * Inequality constraints body: [AI][X] <= [BI]
         */

        protected MatrixStore<Double> getAI() {
            if (myAI != null) {
                return myAI;
            } else {
                return FACTORY.makeZero(0, this.countVariables());
            }
        }

        protected SparseArray<Double> getAI(final int row) {
            return myAI.getRow(row);
        }

        protected RowsSupplier<Double> getAI(final int... rows) {
            return myAI.selectRows(rows);
        }

        /**
         * Equality constraints RHS: [AE][X] == [BE]
         */
        protected MatrixStore<Double> getBE() {
            if (myBE != null) {
                return FACTORY.makeWrapperColumn(myBE);
            } else {
                return FACTORY.makeZero(0, 1);
            }
        }

        protected double getBE(final int row) {
            return myBE.doubleValue(row);
        }

        /**
         * Inequality constraints RHS: [AI][X] <= [BI]
         */
        protected MatrixStore<Double> getBI() {
            if (myBI != null) {
                return FACTORY.makeWrapperColumn(myBI);
            } else {
                return FACTORY.makeZero(0, 1);
            }
        }

        protected double getBI(final int row) {
            return myBI.doubleValue(row);
        }

        protected MatrixStore<Double> getC() {
            return this.getObjective().getLinearFactors(false);
        }

        protected PhysicalStore.Factory<Double, R064Store> getFactory() {
            return FACTORY;
        }

        protected R064Store getLowerBounds(final double defaultValue) {
            if (myLowerBounds == null) {
                myLowerBounds = FACTORY.make(this.countVariables(), 1);
                myLowerBounds.fillAll(defaultValue);
            }
            return myLowerBounds;
        }

        protected MultiaryFunction.TwiceDifferentiable<Double> getObjective() {
            return myObjective;
        }

        protected <T extends MultiaryFunction.TwiceDifferentiable<Double>> T getObjective(final Class<T> type) {
            return (T) myObjective;
        }

        protected RowView<Double> getRowsAE() {
            return myAE.rows();
        }

        protected RowView<Double> getRowsAI() {
            return myAI.rows();
        }

        protected R064Store getUpperBounds(final double defaultValue) {
            if (myUpperBounds == null) {
                myUpperBounds = FACTORY.make(this.countVariables(), 1);
                myUpperBounds.fillAll(defaultValue);
            }
            return myUpperBounds;
        }

        /**
         * Appends the inequality constraints {@code [mtrxAI][X] <= [mtrxBI]} (one row each).
         */
        public final B inequalities(final Access2D<?> mtrxAI, final Access1D<?> mtrxBI) {
            ProgrammingError.throwIfNull(mtrxAI, mtrxBI);
            ProgrammingError.throwIfNotEqualRowDimensions(mtrxAI, mtrxBI);
            this.mergeInequalities(Builder.toRowsSupplier(mtrxAI), mtrxBI);
            return (B) this;
        }

        /**
         * Appends {@code rhs.length} inequality constraints with the given right-hand side; returns a
         * {@link Mutate2D} for filling in the (initially empty) constraint bodies.
         */
        public final Mutate2D inequalities(final double[] rhs) {
            RowsSupplier<Double> block = FACTORY.makeRowsSupplier(this.countVariables());
            block.addRows(rhs.length);
            this.mergeInequalities(block, R064Store.wrap(rhs));
            return block;
        }

        /**
         * Appends a single inequality constraint with the given right-hand side; returns a {@link Mutate1D}
         * for filling in the (initially empty) constraint body.
         */
        public final Mutate1D inequality(final double rhs) {
            RowsSupplier<Double> block = FACTORY.makeRowsSupplier(this.countVariables());
            SparseArray<Double> row = block.addRow();
            this.mergeInequalities(block, FACTORY.makeSingle(rhs));
            return row;
        }

        /**
         * Appends the single inequality constraint {@code [factors][X] <= rhs}.
         */
        public final B inequality(final double rhs, final double... factors) {

            RowsSupplier<Double> block = FACTORY.makeRowsSupplier(this.countVariables());
            SparseArray<Double> row = block.addRow();
            for (int i = 0, limit = Math.min(factors.length, this.countVariables()); i < limit; i++) {
                row.set(i, factors[i]);
            }

            this.mergeInequalities(block, FACTORY.makeSingle(rhs));

            return (B) this;
        }

        protected void setNumberOfVariables(final int numberOfVariables) {
            if (numberOfVariables < 0) {
                throw new IllegalArgumentException();
            }
            if (myNumberOfVariables >= 0 && myNumberOfVariables != numberOfVariables) {
                throw new IllegalStateException();
            }
            myNumberOfVariables = numberOfVariables;
        }

        protected void setObjective(final MultiaryFunction.TwiceDifferentiable<Double> objective) {

            ProgrammingError.throwIfNull(objective);

            myObjective = objective;
        }

        void addConstraint(final String key, final TwiceDifferentiable<Double> value) {
            if (myAdditionalConstraints == null) {
                myAdditionalConstraints = new HashMap<>();
            }
            myAdditionalConstraints.put(key, value);
        }

        /**
         * Merges the supplied {@link RowsSupplier} block into {@code myAE} (appending its {@link SparseArray}
         * rows by reference – no copy) and appends {@code addBE} to {@code myBE}.
         */
        void mergeEqualities(final RowsSupplier<Double> addAE, final Access1D<?> addBE) {

            if (myAE == null || myBE == null) {
                myAE = FACTORY.makeRowsSupplier(addAE.getColDim());
                myBE = NumberList.factory(FACTORY.array()).make();
            }

            myAE.append(addAE);
            for (int i = 0, limit = Math.toIntExact(addBE.count()); i < limit; i++) {
                myBE.add(addBE.doubleValue(i));
            }
        }

        /**
         * Merges the supplied {@link RowsSupplier} block into {@code myAI} (appending its {@link SparseArray}
         * rows by reference – no copy) and appends {@code addBI} to {@code myBI}.
         */
        void mergeInequalities(final RowsSupplier<Double> addAI, final Access1D<?> addBI) {

            if (myAI == null || myBI == null) {
                myAI = FACTORY.makeRowsSupplier(addAI.getColDim());
                myBI = NumberList.factory(FACTORY.array()).make();
            }

            myAI.append(addAI);
            for (int i = 0, limit = Math.toIntExact(addBI.count()); i < limit; i++) {
                myBI.add(addBI.doubleValue(i));
            }
        }

        void setBounds(final Access1D<Double> lower, final Access1D<Double> upper) {

            ProgrammingError.throwIfNull(lower, upper);

            if (lower instanceof R064Store) {
                myLowerBounds = (R064Store) lower;
            } else {
                myLowerBounds = FACTORY.column(lower);
            }

            if (upper instanceof R064Store) {
                myUpperBounds = (R064Store) upper;
            } else {
                myUpperBounds = FACTORY.column(upper);
            }
        }

        void validate() {

            ProgrammingError.throwIfNull(myObjective);

            if (myAE != null || myBE != null) {
                ProgrammingError.throwIfNull(myAE, myBE);
                ProgrammingError.throwIfNotEqualRowDimensions(myAE, myBE);
            }

            if (myAI != null || myBI != null) {
                ProgrammingError.throwIfNull(myAI, myBI);
                ProgrammingError.throwIfNotEqualRowDimensions(myAI, myBI);
            }

            // Check number of variables/columns

            int nbVariables = this.countVariables();

            if (myAE != null && myAE.getColDim() != nbVariables) {
                throw new ProgrammingError("AE has the wrong number of columns!");
            }

            if (myAI != null && myAI.getColDim() != nbVariables) {
                throw new ProgrammingError("AI has the wrong number of columns!");
            }

            if (myObjective != null && myObjective.arity() != nbVariables) {
                throw new ProgrammingError("The objective function has the wrong arity!");
            }
        }

    }

    public final Optimisation.Options options;

    private transient String myClassSimpleName = null;

    private final AtomicInteger myIterationsCount = new AtomicInteger(0);
    private final Stopwatch myStopwatch = new Stopwatch();

    protected State state = State.UNEXPLORED;

    protected GenericSolver(final Optimisation.Options optimisationOptions) {

        super();

        ProgrammingError.throwIfNull(optimisationOptions);

        options = optimisationOptions;
    }

    protected final int countIterations() {
        return myIterationsCount.get();
    }

    /**
     * The number of ms since solver instantiated or iterations count reset.
     */
    protected final long countTime() {
        return myStopwatch.countMillis();
    }

    protected final void error(final String messagePattern, final Object... arguments) {
        BasicLogger.error(messagePattern, arguments);
    }

    protected final String getClassSimpleName() {
        if (myClassSimpleName == null) {
            myClassSimpleName = this.getClass().getSimpleName();
        }
        return myClassSimpleName;
    }

    /**
     * The number of s since solver instantiated or iterations count reset.
     */
    protected final CalendarDateDuration getDuration() {
        return this.getDuration(CalendarDateUnit.SECOND);
    }

    protected final CalendarDateDuration getDuration(final CalendarDateUnit unit) {
        return myStopwatch.stop(unit);
    }

    /**
     * Should be called after a completed iteration. The iterations count is not "1" untill the first
     * iteration is completed.
     */
    protected final int incrementIterationsCount() {
        int iterationsDone = myIterationsCount.incrementAndGet();
        if (this.isLogProgress() && iterationsDone % 100_000 == 0) {
            this.logProgress(iterationsDone, this.getClassSimpleName(), this.getDuration());
        }
        return iterationsDone;
    }

    /**
     * Should be called at the start of an iteration (before it actually starts) to check if you should abort
     * instead. Will return false if either the iterations count or the execution time has reached their
     * respective limits.
     */
    protected final boolean isIterationAllowed() {

        if (state.isFailure() || Thread.currentThread().isInterrupted() || state.isOptimal()) {
            return false;
        }

        if (state.isFeasible() && (this.countTime() >= options.time_suffice || this.countIterations() >= options.iterations_suffice)) {
            return false;
        }

        if (this.countTime() >= options.time_abort || this.countIterations() >= options.iterations_abort) {
            return false;
        }

        return true;
    }

    /**
     * Detailed debug logging
     */
    protected final boolean isLogDebug() {
        return options.logger_detailed && this.isLogProgress();
    }

    /**
     * No logging
     */
    protected final boolean isLogOff() {
        return options.logger_appender == null || !options.logger_solver.isAssignableFrom(this.getClass());
    }

    /**
     * Cursory progress logging (at least)
     */
    protected final boolean isLogProgress() {
        return options.logger_appender != null && options.logger_solver.isAssignableFrom(this.getClass());
    }

    protected final void log() {
        if (options.logger_appender != null) {
            options.logger_appender.println();
        }
    }

    protected final void log(final int tabs, final String messagePattern, final Object... arguments) {
        if (options.logger_appender != null) {
            options.logger_appender.println(tabs, messagePattern, arguments);
        }
    }

    protected final void log(final String descripttion, final Access2D<?> matrix) {
        if (options.logger_appender != null) {
            options.logger_appender.printmtrx(descripttion, matrix, options.print);
        }
    }

    protected final void log(final String messagePattern, final Object... arguments) {
        if (options.logger_appender != null) {
            options.logger_appender.println(messagePattern, arguments);
        }
    }

    protected void logProgress(final int iterationsDone, final String classSimpleName, final CalendarDateDuration duration) {
        this.log("Done {} {} iterations in {}.", iterationsDone, classSimpleName, duration);
    }

    protected final void printf(final String format, final Object... args) {
        if (options.logger_appender != null) {
            options.logger_appender.printf(format, args);
        }
    }

    protected final void resetIterationsCount() {
        myIterationsCount.set(0);
        myStopwatch.reset();
    }

}
