// -*- mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
// vi: set et ts=4 sw=4 sts=4:
/*
  This file is part of the Open Porous Media project (OPM).

  OPM is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 2 of the License, or
  (at your option) any later version.

  OPM 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 General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with OPM.  If not, see <http://www.gnu.org/licenses/>.

  Consult the COPYING file in the top-level source directory of this
  module for the precise wording of the license and the list of
  copyright holders.
*/
/*!
 * \file
 *
 * \copydoc Opm::co2ptflashproblem
 */
#ifndef OPM_CO2PTFLASH_PROBLEM_HH
#define OPM_CO2PTFLASH_PROBLEM_HH

#include <opm/common/Exceptions.hpp>

#include <opm/material/components/SimpleCO2.hpp>
#include <opm/material/components/C10.hpp>
#include <opm/material/components/C1.hpp>
#include <opm/material/fluidmatrixinteractions/RegularizedBrooksCorey.hpp>
#include <opm/material/fluidmatrixinteractions/BrooksCorey.hpp>
#include <opm/material/constraintsolvers/PTFlash.hpp> 
#include <opm/material/fluidsystems/GenericOilGasFluidSystem.hpp>
#include <opm/material/common/Valgrind.hpp>
#include <opm/models/immiscible/immisciblemodel.hh>
#include <opm/models/discretization/ecfv/ecfvdiscretization.hh>
#include <opm/models/ptflash/flashmodel.hh>
#include <opm/models/io/structuredgridvanguard.hh>
#include <opm/models/utils/propertysystem.hh>
#include <opm/models/utils/start.hh>
#include <opm/simulators/linalg/parallelistlbackend.hh>
#include <opm/simulators/linalg/parallelbicgstabbackend.hh>
#include <dune/grid/yaspgrid.hh>
#include <dune/grid/io/file/dgfparser/dgfyasp.hh>
#include <dune/common/version.hh>
#include <dune/common/fvector.hh>
#include <dune/common/fmatrix.hh>

#include <sstream>
#include <iostream>
#include <string>

namespace Opm {
template <class TypeTag>
class CO2PTProblem;
} // namespace Opm */

namespace Opm::Properties {

namespace TTag {
struct CO2PTBaseProblem {};
} // end namespace TTag

template <class TypeTag, class MyTypeTag>
struct Temperature { using type = UndefinedProperty; };
template <class TypeTag, class MyTypeTag>
struct SimulationName { using type = UndefinedProperty; };
template <class TypeTag, class MyTypeTag>
struct EpisodeLength { using type = UndefinedProperty;};

template <class TypeTag, class MyTypeTag>
struct Initialpressure { using type = UndefinedProperty;};

template <class TypeTag, class MyTypeTag>
struct NumComp { using type = UndefinedProperty; };

template <class TypeTag>
struct NumComp<TypeTag, TTag::CO2PTBaseProblem> {
    static constexpr int value = 3;
};

// Set the grid type: --->2D
template <class TypeTag>
struct Grid<TypeTag, TTag::CO2PTBaseProblem> { using type = Dune::YaspGrid</*dim=*/2>; };

// Set the problem property
template <class TypeTag>
struct Problem<TypeTag, TTag::CO2PTBaseProblem>
{ using type = Opm::CO2PTProblem<TypeTag>; };

// Set flash solver
template <class TypeTag>
struct FlashSolver<TypeTag, TTag::CO2PTBaseProblem> {
private:
    using Scalar = GetPropType<TypeTag, Properties::Scalar>;
    using FluidSystem = GetPropType<TypeTag, Properties::FluidSystem>;
    using Evaluation = GetPropType<TypeTag, Properties::Evaluation>;

public:
    using type = Opm::PTFlash<Scalar, FluidSystem>;
};

// Set fluid configuration
template <class TypeTag>
struct FluidSystem<TypeTag, TTag::CO2PTBaseProblem>
{
private:
    using Scalar = GetPropType<TypeTag, Properties::Scalar>;
    static constexpr int num_comp = getPropValue<TypeTag, Properties::NumComp>();

public:
    using type = Opm::GenericOilGasFluidSystem<Scalar, num_comp>;
};

// Set the material Law
template <class TypeTag>
struct MaterialLaw<TypeTag, TTag::CO2PTBaseProblem> {
private:
    using FluidSystem = GetPropType<TypeTag, Properties::FluidSystem>;
    enum { oilPhaseIdx = FluidSystem::oilPhaseIdx };
    enum { gasPhaseIdx = FluidSystem::gasPhaseIdx };

    using Scalar = GetPropType<TypeTag, Properties::Scalar>;
    using Traits = Opm::TwoPhaseMaterialTraits<Scalar,
                                               //  /*wettingPhaseIdx=*/FluidSystem::waterPhaseIdx, // TODO
                                               /*nonWettingPhaseIdx=*/FluidSystem::oilPhaseIdx,
                                               /*gasPhaseIdx=*/FluidSystem::gasPhaseIdx>;

    // define the material law which is parameterized by effective saturation
    using EffMaterialLaw = Opm::NullMaterial<Traits>;
    //using EffMaterialLaw = Opm::BrooksCorey<Traits>;

public:
     using type = EffMaterialLaw;
};

// Enable gravity false
template <class TypeTag>
struct EnableGravity<TypeTag, TTag::CO2PTBaseProblem> { static constexpr bool value = false;
};

// set the defaults for the problem specific properties
 template <class TypeTag>
 struct Temperature<TypeTag, TTag::CO2PTBaseProblem> {
     using type = GetPropType<TypeTag, Scalar>;
     static constexpr type value = 423.25;//TODO
 };

template <class TypeTag>
struct Initialpressure<TypeTag, TTag::CO2PTBaseProblem> {
    using type = GetPropType<TypeTag, Scalar>;
    static constexpr type value = 75.e5;
};

template <class TypeTag>
struct SimulationName<TypeTag, TTag::CO2PTBaseProblem> {
    static constexpr auto value = "co2_ptflash";
};

// The default for the end time of the simulation
template <class TypeTag>
struct EndTime<TypeTag, TTag::CO2PTBaseProblem> {
    using type = GetPropType<TypeTag, Scalar>;
    static constexpr type value = 60. * 60.;
};

// convergence control
template <class TypeTag>
struct InitialTimeStepSize<TypeTag, TTag::CO2PTBaseProblem> {
    using type = GetPropType<TypeTag, Scalar>;
    static constexpr type value = 0.1 * 60. * 60.;
};

template <class TypeTag>
struct LinearSolverTolerance<TypeTag, TTag::CO2PTBaseProblem> {
    using type = GetPropType<TypeTag, Scalar>;
    static constexpr type value = 1e-3;
};

template <class TypeTag>
struct LinearSolverAbsTolerance<TypeTag, TTag::CO2PTBaseProblem> {
    using type = GetPropType<TypeTag, Scalar>;
    static constexpr type value = 0.;
};

template <class TypeTag>
struct NewtonMaxIterations<TypeTag, TTag::CO2PTBaseProblem> {
    static constexpr int value = 30;
};

// output
template <class TypeTag>
struct VtkWriteFilterVelocities<TypeTag, TTag::CO2PTBaseProblem> {
    static constexpr bool value = true;
};

template <class TypeTag>
struct VtkWritePotentialGradients<TypeTag, TTag::CO2PTBaseProblem> {
    static constexpr bool value = true;
};

template <class TypeTag>
struct VtkWriteTotalMassFractions<TypeTag, TTag::CO2PTBaseProblem> {
    static constexpr bool value = true;
};

template <class TypeTag>
struct VtkWriteTotalMoleFractions<TypeTag, TTag::CO2PTBaseProblem> {
    static constexpr bool value = true;
};

template <class TypeTag>
struct VtkWriteFugacityCoeffs<TypeTag, TTag::CO2PTBaseProblem> {
    static constexpr bool value = true;
};

template <class TypeTag>
struct VtkWriteLiquidMoleFractions<TypeTag, TTag::CO2PTBaseProblem> {
    static constexpr bool value = true;
};

template <class TypeTag>
struct VtkWriteEquilibriumConstants<TypeTag, TTag::CO2PTBaseProblem> {
    static constexpr bool value = true;
};

// this is kinds of telling the report step length
template <class TypeTag>
struct EpisodeLength<TypeTag, TTag::CO2PTBaseProblem> {
    using type = GetPropType<TypeTag, Scalar>;
    static constexpr type value = 0.1 * 60. * 60.;
};

// mesh grid
template <class TypeTag>
struct Vanguard<TypeTag, TTag::CO2PTBaseProblem> {
    using type = Opm::StructuredGridVanguard<TypeTag>;
};

//\Note: from the Julia code, the problem is a 1D problem with 3X1 cell.
//\Note: DomainSizeX is 3.0 meters
//\Note: DomainSizeY is 1.0 meters
template <class TypeTag>
struct DomainSizeX<TypeTag, TTag::CO2PTBaseProblem> {
    using type = GetPropType<TypeTag, Scalar>;
    static constexpr type value = 300; // meter
};

template <class TypeTag>
struct DomainSizeY<TypeTag, TTag::CO2PTBaseProblem> {
    using type = GetPropType<TypeTag, Scalar>;
    static constexpr type value = 1.0;
};

// DomainSizeZ is not needed, while to keep structuredgridvanguard.hh compile
template <class TypeTag>
struct DomainSizeZ<TypeTag, TTag::CO2PTBaseProblem> {
    using type = GetPropType<TypeTag, Scalar>;
    static constexpr type value = 1.0;
};

template<class TypeTag>
struct CellsX<TypeTag, TTag::CO2PTBaseProblem> { static constexpr unsigned value = 30; };
template<class TypeTag>
struct CellsY<TypeTag, TTag::CO2PTBaseProblem> { static constexpr unsigned value = 1; };
// CellsZ is not needed, while to keep structuredgridvanguard.hh compile
template<class TypeTag>
struct CellsZ<TypeTag, TTag::CO2PTBaseProblem> { static constexpr unsigned value = 1; };

template <class TypeTag>
struct EnableEnergy<TypeTag, TTag::CO2PTBaseProblem> {
    static constexpr bool value = false;
};

} // namespace Opm::Properties

namespace Opm::Parameters {

// Write the Newton convergence behavior to disk?
template <class TypeTag>
struct NewtonWriteConvergence<TypeTag, Properties::TTag::CO2PTBaseProblem>
{ static constexpr bool value = false; };

template <class TypeTag>
struct NewtonTolerance<TypeTag, Properties::TTag::CO2PTBaseProblem>
{
    using type = GetPropType<TypeTag, Properties::Scalar>;
    static constexpr type value = 1e-3;
};

template <class TypeTag>
struct NewtonTargetIterations<TypeTag, Properties::TTag::CO2PTBaseProblem>
{
    using type = GetPropType<TypeTag, Properties::Scalar>;
    static constexpr type value = 6;
};

} // namespace Opm::Parameters

namespace Opm {
/*!
 * \ingroup TestProblems
 *
 * \brief 3 component simple testproblem with ["CO2", "C1", "C10"]
 *
 */
template <class TypeTag>
class CO2PTProblem : public GetPropType<TypeTag, Properties::BaseProblem>
{
    using ParentType = GetPropType<TypeTag, Properties::BaseProblem>;

    using Scalar = GetPropType<TypeTag, Properties::Scalar>;
    using Evaluation = GetPropType<TypeTag, Properties::Evaluation>;
    using GridView = GetPropType<TypeTag, Properties::GridView>;
    using FluidSystem = GetPropType<TypeTag, Properties::FluidSystem>;
    enum { dim = GridView::dimension };
    enum { dimWorld = GridView::dimensionworld };
    using Indices = GetPropType<TypeTag, Properties::Indices>;
    using PrimaryVariables = GetPropType<TypeTag, Properties::PrimaryVariables>;
    using RateVector = GetPropType<TypeTag, Properties::RateVector>;
    using BoundaryRateVector = GetPropType<TypeTag, Properties::BoundaryRateVector>;
    using MaterialLaw = GetPropType<TypeTag, Properties::MaterialLaw>;
    using Simulator = GetPropType<TypeTag, Properties::Simulator>;
    using Model = GetPropType<TypeTag, Properties::Model>;
    using MaterialLawParams = GetPropType<TypeTag, Properties::MaterialLawParams>;    

    using Toolbox = Opm::MathToolbox<Evaluation>;
    using CoordScalar = typename GridView::ctype;

    enum { numPhases = FluidSystem::numPhases };
    enum { oilPhaseIdx = FluidSystem::oilPhaseIdx };
    enum { gasPhaseIdx = FluidSystem::gasPhaseIdx };
    enum { conti0EqIdx = Indices::conti0EqIdx };
    enum { numComponents = getPropValue<TypeTag, Properties::NumComponents>() };
    enum { enableEnergy = getPropValue<TypeTag, Properties::EnableEnergy>() };
    enum { enableDiffusion = getPropValue<TypeTag, Properties::EnableDiffusion>() };

    using GlobalPosition = Dune::FieldVector<CoordScalar, dimWorld>;
    using DimMatrix = Dune::FieldMatrix<Scalar, dimWorld, dimWorld>;
    using DimVector = Dune::FieldVector<Scalar, dimWorld>;
    using ComponentVector = Dune::FieldVector<Evaluation, numComponents>;
    using FlashSolver = GetPropType<TypeTag, Properties::FlashSolver>;

public:
    using FluidState = Opm::CompositionalFluidState<Evaluation, FluidSystem, enableEnergy>;
    /*!
     * \copydoc Doxygen::defaultProblemConstructor
     */
    explicit CO2PTProblem(Simulator& simulator)
        : ParentType(simulator)
    {
        const Scalar epi_len = Parameters::get<TypeTag, Properties::EpisodeLength>();
        simulator.setEpisodeLength(epi_len);
        FluidSystem::init();
        using CompParm = typename FluidSystem::ComponentParam;
        using CO2 = Opm::SimpleCO2<Scalar>;
        using C1 = Opm::C1<Scalar>;
        using C10 = Opm::C10<Scalar>;
        FluidSystem::addComponent(CompParm {CO2::name(), CO2::molarMass(), CO2::criticalTemperature(),
                                   CO2::criticalPressure(), CO2::criticalVolume(), CO2::acentricFactor()});
        FluidSystem::addComponent(CompParm {C1::name(), C1::molarMass(), C1::criticalTemperature(),
                                   C1::criticalPressure(), C1::criticalVolume(), C1::acentricFactor()});
        FluidSystem::addComponent(CompParm{C10::name(), C10::molarMass(), C10::criticalTemperature(),
                                   C10::criticalPressure(), C10::criticalVolume(), C10::acentricFactor()});
        // FluidSystem::add
    }

    void initPetrophysics()
    {
        temperature_ = Parameters::get<TypeTag, Properties::Temperature>();
        K_ = this->toDimMatrix_(9.869232667160131e-14);

        porosity_ = 0.1;
    }

    template <class Context>
    const DimVector&
    gravity([[maybe_unused]]const Context& context, [[maybe_unused]] unsigned spaceIdx, [[maybe_unused]] unsigned timeIdx) const
    {
        return gravity();
    }

    const DimVector& gravity() const
    {
        return gravity_;
    }

    /*!
     * \copydoc FvBaseProblem::finishInit
     */
    void finishInit()
    {
        ParentType::finishInit();
        // initialize fixed parameters; temperature, permeability, porosity
        initPetrophysics();
    }

    /*!
     * \copydoc co2ptflashproblem::registerParameters
     */
    static void registerParameters()
    {
        ParentType::registerParameters();

        Parameters::registerParam<TypeTag, Properties::Temperature>
            ("The temperature [K] in the reservoir");
        Parameters::registerParam<TypeTag, Properties::Initialpressure>
            ("The initial pressure [Pa s] in the reservoir");
        Parameters::registerParam<TypeTag, Properties::SimulationName>
            ("The name of the simulation used for the output files");
        Parameters::registerParam<TypeTag, Properties::EpisodeLength>
            ("Time interval [s] for episode length");
    }

    /*!
     * \copydoc FvBaseProblem::name
     */
    std::string name() const
    {
        std::ostringstream oss;
        oss << Parameters::get<TypeTag, Properties::SimulationName>();
        return oss.str();
    }

    // This method must be overridden for the simulator to continue with
    // a new episode. We just start a new episode with the same length as
    // the old one.
    void endEpisode()
    {
        Scalar epi_len = Parameters::get<TypeTag, Properties::EpisodeLength>();
        this->simulator().startNextEpisode(epi_len);
    }

    // only write output when episodes change, aka. report steps, and
    // include the initial timestep too
    bool shouldWriteOutput()
    {
        return this->simulator().episodeWillBeOver() || (this->simulator().timeStepIndex() == -1);
    }

    // we don't care about doing restarts from every fifth timestep, it
    // will just slow us down
    bool shouldWriteRestartFile()
    {
        return false;
    }

    /*!
     * \copydoc FvBaseProblem::endTimeStep
     */
    void endTimeStep()
    {
        Scalar tol = this->model().newtonMethod().tolerance() * 1e5;
        this->model().checkConservativeness(tol);

        // Calculate storage terms
        PrimaryVariables storageO, storageW;
        this->model().globalPhaseStorage(storageO, oilPhaseIdx);

        // Calculate storage terms
         PrimaryVariables storageL, storageG;
         this->model().globalPhaseStorage(storageL, /*phaseIdx=*/0);
         this->model().globalPhaseStorage(storageG, /*phaseIdx=*/1);

         // Write mass balance information for rank 0
        //  if (this->gridView().comm().rank() == 0) {
        //      std::cout << "Storage: liquid=[" << storageL << "]"
        //                << " gas=[" << storageG << "]\n" << std::flush;
        //  }
    }

    /*!
     * \copydoc FvBaseProblem::initial
     */
    template <class Context>
    void initial(PrimaryVariables& values, const Context& context, unsigned spaceIdx, unsigned timeIdx) const
    {
        Opm::CompositionalFluidState<Evaluation, FluidSystem> fs;
        initialFluidState(fs, context, spaceIdx, timeIdx);
        values.assignNaive(fs);
    }

    // Constant temperature
    template <class Context>
    Scalar temperature([[maybe_unused]] const Context& context, [[maybe_unused]] unsigned spaceIdx, [[maybe_unused]] unsigned timeIdx) const
    {
        return temperature_;
    }


    // Constant permeability
    template <class Context>
    const DimMatrix& intrinsicPermeability([[maybe_unused]] const Context& context,
                                           [[maybe_unused]] unsigned spaceIdx,
                                           [[maybe_unused]] unsigned timeIdx) const
    {
        return K_;
    }

    // Constant porosity
    template <class Context>
    Scalar porosity([[maybe_unused]] const Context& context, [[maybe_unused]] unsigned spaceIdx, [[maybe_unused]] unsigned timeIdx) const
    {
        int spatialIdx = context.globalSpaceIndex(spaceIdx, timeIdx);
        int inj = 0;
        int prod = Parameters::get<TypeTag, Properties::CellsX>() - 1;
        if (spatialIdx == inj || spatialIdx == prod) {
            return 1.0;
        } else {
            return porosity_;
        }
    }

        /*!
     * \copydoc FvBaseMultiPhaseProblem::materialLawParams
     */
    template <class Context>
    const MaterialLawParams& materialLawParams([[maybe_unused]] const Context& context,
                                               [[maybe_unused]] unsigned spaceIdx,
                                               [[maybe_unused]] unsigned timeIdx) const
    {
        return this->mat_;
    }


    // No flow (introduce fake wells instead)
    template <class Context>
    void boundary(BoundaryRateVector& values,
                  const Context& /*context*/,
                  unsigned /*spaceIdx*/,
                  unsigned /*timeIdx*/) const
    { values.setNoFlow(); }

    // No source terms
    template <class Context>
    void source(RateVector& source_rate,
                [[maybe_unused]] const Context& context,
                [[maybe_unused]] unsigned spaceIdx,
                [[maybe_unused]] unsigned timeIdx) const
    {
        source_rate = Scalar(0.0);
    }

private:

    /*!
     * \copydoc FvBaseProblem::initial
     */
    template <class FluidState, class Context>
    void initialFluidState(FluidState& fs, const Context& context, unsigned spaceIdx, unsigned timeIdx) const
    {
        // z0 = [0.5, 0.3, 0.2]
        // zi = [0.99, 0.01-1e-3, 1e-3]
        // p0 = 75e5
        // T0 = 423.25
        int inj = 0;
        int prod = Parameters::get<TypeTag, Properties::CellsX>() - 1;
        int spatialIdx = context.globalSpaceIndex(spaceIdx, timeIdx);
        ComponentVector comp;
        comp[0] = Evaluation::createVariable(0.5, 1);
        comp[1] = Evaluation::createVariable(0.3, 2);
        comp[2] = 1. - comp[0] - comp[1];
        if (spatialIdx == inj) {
            comp[0] = Evaluation::createVariable(0.99, 1);
            comp[1] = Evaluation::createVariable(0.01 - 1e-3, 2);
            comp[2] = 1. - comp[0] - comp[1];
        }
        ComponentVector sat;
        sat[0] = 1.0;
        sat[1] = 1.0 - sat[0];

        Scalar p0 = Parameters::get<TypeTag, Properties::Initialpressure>();

        //\Note, for an AD variable, if we multiply it with 2, the derivative will also be scalced with 2,
        //\Note, so we should not do it.
        if (spatialIdx == inj) {
            p0 *= 2.0;
        }
        if (spatialIdx == prod) {
            p0 *= 0.5;
        }
        Evaluation p_init = Evaluation::createVariable(p0, 0);

        fs.setPressure(FluidSystem::oilPhaseIdx, p_init);
        fs.setPressure(FluidSystem::gasPhaseIdx, p_init);

        for (unsigned compIdx = 0; compIdx < numComponents; ++compIdx) {
            fs.setMoleFraction(FluidSystem::oilPhaseIdx, compIdx, comp[compIdx]);
            fs.setMoleFraction(FluidSystem::gasPhaseIdx, compIdx, comp[compIdx]);
        }

        // It is used here only for calculate the z
        fs.setSaturation(FluidSystem::oilPhaseIdx, sat[0]);
        fs.setSaturation(FluidSystem::gasPhaseIdx, sat[1]);

        fs.setTemperature(temperature_);

        // ParameterCache paramCache;
        {
            typename FluidSystem::template ParameterCache<Evaluation> paramCache;
            paramCache.updatePhase(fs, FluidSystem::oilPhaseIdx);
            paramCache.updatePhase(fs, FluidSystem::gasPhaseIdx);
            fs.setDensity(FluidSystem::oilPhaseIdx, FluidSystem::density(fs, paramCache, FluidSystem::oilPhaseIdx));
            fs.setDensity(FluidSystem::gasPhaseIdx, FluidSystem::density(fs, paramCache, FluidSystem::gasPhaseIdx));
            fs.setViscosity(FluidSystem::oilPhaseIdx, FluidSystem::viscosity(fs, paramCache, FluidSystem::oilPhaseIdx));
            fs.setViscosity(FluidSystem::gasPhaseIdx, FluidSystem::viscosity(fs, paramCache, FluidSystem::gasPhaseIdx));
        }

        // Set initial K and L
        for (unsigned compIdx = 0; compIdx < numComponents; ++compIdx) {
            const Evaluation Ktmp = fs.wilsonK_(compIdx);
            fs.setKvalue(compIdx, Ktmp);
        }

        const Evaluation& Ltmp = -1.0;
        fs.setLvalue(Ltmp);
    }

    DimMatrix K_;
    Scalar porosity_;
    Scalar temperature_;
    MaterialLawParams mat_;
    DimVector gravity_;
};
} // namespace Opm

#endif