#ifndef STORM_MONOTONICITYHELPER_H
#define STORM_MONOTONICITYHELPER_H

#include <map>
#include "Order.h"
#include "LocalMonotonicityResult.h"
#include "OrderExtender.h"
#include "AssumptionMaker.h"
#include "MonotonicityResult.h"


#include "storm/logic/Formula.h"

#include "storm/models/ModelBase.h"
#include "storm/models/sparse/Dtmc.h"
#include "storm/models/sparse/Mdp.h"

#include "storm/solver/Z3SmtSolver.h"

#include "storm/storage/SparseMatrix.h"
#include "storm/storage/expressions/BinaryRelationExpression.h"
#include "storm/storage/expressions/ExpressionManager.h"
#include "storm/storage/expressions/RationalFunctionToExpression.h"

#include "storm/utility/constants.h"

#include "storm-pars/api/region.h"

namespace storm {
    namespace analysis {

        template <typename ValueType, typename ConstantType>
        class MonotonicityHelper {

        public:
            typedef typename utility::parametric::VariableType<ValueType>::type VariableType;
            typedef typename utility::parametric::CoefficientType<ValueType>::type CoefficientType;
            typedef typename MonotonicityResult<VariableType>::Monotonicity Monotonicity;
            typedef typename storage::ParameterRegion<ValueType> Region;

            /*!
             * Constructor of MonotonicityHelper.
             *
             * @param model The model considered.
             * @param formulas The formulas considered.
             * @param regions The regions to consider.
             * @param numberOfSamples Number of samples taken for monotonicity checking, default 0,
             *          if 0 then no check on samples is executed.
             * @param precision Precision on which the samples are compared
             * @param dotOutput Whether or not dot output should be generated for the ROs.
             */
            MonotonicityHelper(std::shared_ptr<models::sparse::Model<ValueType>> model, std::vector<std::shared_ptr<logic::Formula const>> formulas, std::vector<storage::ParameterRegion<ValueType>> regions, uint_fast64_t numberOfSamples=0, double const& precision=0.000001, bool dotOutput = false);

            /*!
             * Checks if a derivative >=0 or/and <=0
             *
             * @param derivative The derivative you want to check
             * @return pair of bools, >= 0 and <= 0
             */
            static std::pair<bool, bool> checkDerivative(ValueType derivative, storage::ParameterRegion<ValueType> reg) {
                bool monIncr = false;
                bool monDecr = false;

                if (derivative.isZero()) {
                    monIncr = true;
                    monDecr = true;
                } else if (derivative.isConstant()) {
                    monIncr = derivative.constantPart() >= 0;
                    monDecr = derivative.constantPart() <= 0;
                } else {

                    std::shared_ptr<utility::solver::SmtSolverFactory> smtSolverFactory = std::make_shared<utility::solver::MathsatSmtSolverFactory>();
                    std::shared_ptr<expressions::ExpressionManager> manager(new expressions::ExpressionManager());
                    solver::Z3SmtSolver s(*manager);
                    std::set<VariableType> variables = derivative.gatherVariables();

                    expressions::Expression exprBounds = manager->boolean(true);
                    for (auto variable : variables) {
                        auto managerVariable = manager->declareRationalVariable(variable.name());
                        auto lb = utility::convertNumber<RationalNumber>(reg.getLowerBoundary(variable));
                        auto ub = utility::convertNumber<RationalNumber>(reg.getUpperBoundary(variable));
                        exprBounds = exprBounds && manager->rational(lb) < managerVariable && managerVariable < manager->rational(ub);
                    }

                    auto converter = expressions::RationalFunctionToExpression<ValueType>(manager);

                    // < 0, so not monotone increasing. If this is unsat, then it should be monotone increasing.
                    expressions::Expression exprToCheck = converter.toExpression(derivative) < manager->rational(0);
                    s.add(exprBounds);
                    s.add(exprToCheck);
                    monIncr = s.check() == solver::SmtSolver::CheckResult::Unsat;

                    // > 0, so not monotone decreasing. If this is unsat it should be monotone decreasing.
                    exprToCheck = converter.toExpression(derivative) > manager->rational(0);
                    s.reset();
                    s.add(exprBounds);
                    s.add(exprToCheck);
                    monDecr = s.check() == solver::SmtSolver::CheckResult::Unsat;
                }
                assert (!(monIncr && monDecr) || derivative.isZero());

                return std::pair<bool, bool>(monIncr, monDecr);
            }

            /*!
             * Builds Reachability Orders for the given model and simultaneously uses them to check for Monotonicity.
             *
             * @param outfile Outfile to which results are written.
             * @param dotOutfileName Name for the files of the dot outputs should they be generated
             * @return Map which maps each order to its Reachability Order and used assumptions.
             */
            std::map<std::shared_ptr<Order>, std::pair<std::shared_ptr<MonotonicityResult<VariableType>>, std::vector<std::shared_ptr<expressions::BinaryRelationExpression>>>> checkMonotonicityInBuild(std::ostream& outfile, bool usePLA = false, std::string dotOutfileName = "dotOutput");

            /*!
             * Checks for local monotonicity at the given state.
             *
             * @param order the order on which the monotonicity should be checked
             * @param state the considerd state
             * @param var the variable in which we check for monotonicity
             * @param region the region on which we check the monotonicity
             * @return Incr, Decr, Constant, Unknown or Not
             */
            Monotonicity checkLocalMonotonicity(std::shared_ptr<Order> order, uint_fast64_t state, VariableType var, storage::ParameterRegion<ValueType> region);

        private:
            void createOrder();

            void checkMonotonicityOnSamples(std::shared_ptr<models::sparse::Dtmc<ValueType>> model, uint_fast64_t numberOfSamples);

            void checkMonotonicityOnSamples(std::shared_ptr<models::sparse::Mdp<ValueType>> model, uint_fast64_t numberOfSamples);

            void extendOrderWithAssumptions(std::shared_ptr<Order> order, uint_fast64_t val1, uint_fast64_t val2, std::vector<std::shared_ptr<expressions::BinaryRelationExpression>> assumptions, std::shared_ptr<MonotonicityResult<VariableType>> monRes);

            Monotonicity checkTransitionMonRes(ValueType function, VariableType param, Region region);

            ValueType getDerivative(ValueType function, VariableType var);


            std::shared_ptr<models::ModelBase> model;

            std::vector<std::shared_ptr<logic::Formula const>> formulas;

            bool dotOutput;

            bool checkSamples;

            bool onlyCheckOnOrder;

            MonotonicityResult<VariableType> resultCheckOnSamples;

            std::map<VariableType, std::vector<uint_fast64_t>> occuringStatesAtVariable;

            std::map<std::shared_ptr<Order>, std::pair<std::shared_ptr<MonotonicityResult<VariableType>>, std::vector<std::shared_ptr<expressions::BinaryRelationExpression>>>> monResults;

            OrderExtender<ValueType, ConstantType> *extender;

            ConstantType precision;

            Region region;

            storage::SparseMatrix<ValueType> matrix;

            std::unordered_map<ValueType, std::unordered_map<VariableType, ValueType>> derivatives;

            AssumptionMaker<ValueType, ConstantType> assumptionMaker;


        };
    }
}
#endif //STORM_MONOTONICITYHELPER_H