#include "src/logic/Formula.h"
#include "src/utility/initialize.h"
#include "src/utility/storm.h"
#include "src/parser/DFTGalileoParser.h"
#include "src/modelchecker/dft/DFTModelChecker.h"

#include "src/modelchecker/dft/DFTASFChecker.h"
#include "src/cli/cli.h"
#include "src/exceptions/BaseException.h"
#include "src/utility/macros.h"
#include "src/builder/DftSmtBuilder.h"

#include "src/settings/modules/GeneralSettings.h"
#include "src/settings/modules/DFTSettings.h"
#include "src/settings/modules/CoreSettings.h"
#include "src/settings/modules/DebugSettings.h"
//#include "src/settings/modules/CounterexampleGeneratorSettings.h"
//#include "src/settings/modules/CuddSettings.h"
//#include "src/settings/modules/SylvanSettings.h"
#include "src/settings/modules/GmmxxEquationSolverSettings.h"
#include "src/settings/modules/MinMaxEquationSolverSettings.h"
#include "src/settings/modules/NativeEquationSolverSettings.h"
//#include "src/settings/modules/BisimulationSettings.h"
//#include "src/settings/modules/GlpkSettings.h"
//#include "src/settings/modules/GurobiSettings.h"
//#include "src/settings/modules/TopologicalValueIterationEquationSolverSettings.h"
//#include "src/settings/modules/ParametricSettings.h"
#include "src/settings/modules/EliminationSettings.h"

#include <boost/lexical_cast.hpp>

/*!
 * Load DFT from filename, build corresponding Model and check against given property.
 *
 * @param filename Path to DFT file in Galileo format.
 * @param property PCTC formula capturing the property to check.
 */
template <typename ValueType>
void analyzeDFT(std::string filename, std::string property, bool symred, bool allowModularisation, bool enableDC, double approximationError) {
    std::cout << "Running DFT analysis on file " << filename << " with property " << property << std::endl;

    storm::parser::DFTGalileoParser<ValueType> parser;
    storm::storage::DFT<ValueType> dft = parser.parseDFT(filename);
    std::vector<std::shared_ptr<storm::logic::Formula const>> formulas = storm::parseFormulasForExplicit(property);
    STORM_LOG_ASSERT(formulas.size() == 1, "Wrong number of formulas.");

    storm::modelchecker::DFTModelChecker<ValueType> modelChecker;
    modelChecker.check(dft, formulas[0], symred, allowModularisation, enableDC, approximationError);
    modelChecker.printTimings();
    modelChecker.printResult();
}

/*!
 * Analyze the DFT with use of SMT solving.
 *
 * @param filename Path to DFT file in Galileo format.
 */
template<typename ValueType>
void analyzeWithSMT(std::string filename) {
    std::cout << "Running DFT analysis on file " << filename << " with use of SMT" << std::endl;
    
    storm::parser::DFTGalileoParser<ValueType> parser;
    storm::storage::DFT<ValueType> dft = parser.parseDFT(filename);
    storm::modelchecker::DFTASFChecker asfChecker(dft);
    asfChecker.convert();
    asfChecker.toFile("test.smt2");
    //bool sat = dftSmtBuilder.check();
    //std::cout << "SMT result: " << sat << std::endl;
}

/*!
 * Initialize the settings manager.
 */
void initializeSettings() {
    storm::settings::mutableManager().setName("StoRM-DyFTeE", "storm-dft");
    
    // Register all known settings modules.
    storm::settings::addModule<storm::settings::modules::GeneralSettings>();
    storm::settings::addModule<storm::settings::modules::DFTSettings>();
    storm::settings::addModule<storm::settings::modules::CoreSettings>();
    storm::settings::addModule<storm::settings::modules::DebugSettings>();
    //storm::settings::addModule<storm::settings::modules::CounterexampleGeneratorSettings>();
    //storm::settings::addModule<storm::settings::modules::CuddSettings>();
    //storm::settings::addModule<storm::settings::modules::SylvanSettings>();
    storm::settings::addModule<storm::settings::modules::GmmxxEquationSolverSettings>();
    storm::settings::addModule<storm::settings::modules::MinMaxEquationSolverSettings>();
    storm::settings::addModule<storm::settings::modules::NativeEquationSolverSettings>();
    //storm::settings::addModule<storm::settings::modules::BisimulationSettings>();
    //storm::settings::addModule<storm::settings::modules::GlpkSettings>();
    //storm::settings::addModule<storm::settings::modules::GurobiSettings>();
    //storm::settings::addModule<storm::settings::modules::TopologicalValueIterationEquationSolverSettings>();
    //storm::settings::addModule<storm::settings::modules::ParametricSettings>();
    storm::settings::addModule<storm::settings::modules::EliminationSettings>();
}

/*!
 * Entry point for the DyFTeE backend.
 *
 * @param argc The argc argument of main().
 * @param argv The argv argument of main().
 * @return Return code, 0 if successfull, not 0 otherwise.
 */
int main(const int argc, const char** argv) {
    try {
        storm::utility::setUp();
        storm::cli::printHeader("StoRM-DyFTeE", argc, argv);
        initializeSettings();
        
        bool optionsCorrect = storm::cli::parseOptions(argc, argv);
        if (!optionsCorrect) {
            return -1;
        }
        
        storm::settings::modules::DFTSettings const& dftSettings = storm::settings::getModule<storm::settings::modules::DFTSettings>();
        storm::settings::modules::GeneralSettings const& generalSettings = storm::settings::getModule<storm::settings::modules::GeneralSettings>();
        if (!dftSettings.isDftFileSet()) {
            STORM_LOG_THROW(false, storm::exceptions::InvalidSettingsException, "No input model.");
        }
        
        bool parametric = false;
#ifdef STORM_HAVE_CARL
        parametric = generalSettings.isParametricSet();
#endif
        
#ifdef STORM_HAVE_Z3
        if (dftSettings.solveWithSMT()) {
            // Solve with SMT
            if (parametric) {
            //    analyzeWithSMT<storm::RationalFunction>(dftSettings.getDftFilename());
            } else {
                analyzeWithSMT<double>(dftSettings.getDftFilename());
            }
            storm::utility::cleanUp();
            return 0;
        }
#endif
        
        // Set min or max
        bool minimal = true;
        if (dftSettings.isComputeMaximalValue()) {
            STORM_LOG_THROW(!dftSettings.isComputeMinimalValue(), storm::exceptions::InvalidSettingsException, "Cannot compute minimal and maximal values at the same time.");
            minimal = false;
        }
        
        // Construct pctlFormula
        std::string pctlFormula = "";
        std::string operatorType = "";
        std::string targetFormula = "";
        
        if (generalSettings.isPropertySet()) {
            STORM_LOG_THROW(!dftSettings.usePropExpectedTime() && !dftSettings.usePropProbability() && !dftSettings.usePropTimebound(), storm::exceptions::InvalidSettingsException, "More than one property given.");
            pctlFormula = generalSettings.getProperty();
        } else if (dftSettings.usePropExpectedTime()) {
            STORM_LOG_THROW(!dftSettings.usePropProbability() && !dftSettings.usePropTimebound(), storm::exceptions::InvalidSettingsException, "More than one property given.");
            operatorType = "T";
            targetFormula = "F \"failed\"";
        } else if (dftSettings.usePropProbability()) {
            STORM_LOG_THROW(!dftSettings.usePropTimebound(), storm::exceptions::InvalidSettingsException, "More than one property given.");
            operatorType = "P";;
            targetFormula = "F \"failed\"";
        } else {
            STORM_LOG_THROW(dftSettings.usePropTimebound(), storm::exceptions::InvalidSettingsException, "No property given.");
            std::stringstream stream;
            stream << "F<=" << dftSettings.getPropTimebound() << " \"failed\"";
            operatorType = "P";
            targetFormula = stream.str();
        }
        
        if (!targetFormula.empty()) {
            STORM_LOG_ASSERT(pctlFormula.empty(), "Pctl formula not empty.");
            pctlFormula = operatorType + (minimal ? "min" : "max") + "=?[" + targetFormula + "]";
        }
        
        STORM_LOG_ASSERT(!pctlFormula.empty(), "Pctl formula empty.");

        double approximationError = 0.0;
        if (dftSettings.isApproximationErrorSet()) {
            approximationError = dftSettings.getApproximationError();
        }

        // From this point on we are ready to carry out the actual computations.
        if (parametric) {
#ifdef STORM_HAVE_CARL
            analyzeDFT<storm::RationalFunction>(dftSettings.getDftFilename(), pctlFormula, dftSettings.useSymmetryReduction(), dftSettings.useModularisation(), !dftSettings.isDisableDC(), approximationError);
#else
            STORM_LOG_THROW(false, storm::exceptions::NotSupportedException, "Parameters are not supported in this build.");
#endif
        } else {
            analyzeDFT<double>(dftSettings.getDftFilename(), pctlFormula, dftSettings.useSymmetryReduction(), dftSettings.useModularisation(), !dftSettings.isDisableDC(), approximationError);
        }
        
        // All operations have now been performed, so we clean up everything and terminate.
        storm::utility::cleanUp();
        return 0;
    } catch (storm::exceptions::BaseException const& exception) {
        STORM_LOG_ERROR("An exception caused StoRM-DyFTeE to terminate. The message of the exception is: " << exception.what());
    } catch (std::exception const& exception) {
        STORM_LOG_ERROR("An unexpected exception occurred and caused StoRM-DyFTeE to terminate. The message of this exception is: " << exception.what());
    }
}