/*
  Copyright 2017 SINTEF Digital, Mathematics and Cybernetics.
  Copyright 2017 Statoil ASA.
  Copyright 2018 IRIS

  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 3 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/>.
*/

#include <config.h>
#include <opm/simulators/wells/WellGroupControls.hpp>

#include <opm/input/eclipse/EclipseState/Phase.hpp>
#include <opm/input/eclipse/Schedule/Schedule.hpp>
#include <opm/input/eclipse/Schedule/Group/GConSale.hpp>
#include <opm/input/eclipse/Schedule/Group/Group.hpp>
#include <opm/input/eclipse/Schedule/ScheduleTypes.hpp>

#include <opm/material/densead/Evaluation.hpp>
#include <opm/material/fluidsystems/BlackOilDefaultFluidSystemIndices.hpp>

#include <opm/simulators/wells/GroupState.hpp>
#include <opm/simulators/wells/TargetCalculator.hpp>
#include <opm/simulators/wells/GroupStateHelper.hpp>
#include <opm/simulators/wells/WellInterfaceGeneric.hpp>
#include <opm/simulators/wells/WellState.hpp>

#include <algorithm>
#include <cstddef>
#include <cassert>

namespace Opm {

template<typename Scalar, typename IndexTraits>
template<class EvalWell>
void WellGroupControls<Scalar, IndexTraits>::
getGroupInjectionControl(const Group& group,
                         const GroupStateHelperType& groupStateHelper,
                         const InjectorType& injectorType,
                         const EvalWell& bhp,
                         const EvalWell& injection_rate,
                         const RateConvFunc& rateConverter,
                         Scalar efficiencyFactor,
                         EvalWell& control_eq) const
{
    const auto& well_state = groupStateHelper.wellState();
    const auto& group_state = groupStateHelper.groupState();
    const auto& schedule = groupStateHelper.schedule();
    const auto& summaryState = groupStateHelper.summaryState();
    // Setting some defaults to silence warnings below.
    // Will be overwritten in the switch statement.
    Phase injectionPhase = Phase::WATER;
    switch (injectorType) {
    case InjectorType::WATER:
    {
        injectionPhase = Phase::WATER;
        break;
    }
    case InjectorType::OIL:
    {
        injectionPhase = Phase::OIL;
        break;
    }
    case InjectorType::GAS:
    {
        injectionPhase = Phase::GAS;
        break;
    }
    default:
        // Should not be here.
        assert(false);
    }

    auto currentGroupControl = group_state.injection_control(group.name(), injectionPhase);
    if (currentGroupControl == Group::InjectionCMode::FLD ||
        currentGroupControl == Group::InjectionCMode::NONE) {
        if (!group.injectionGroupControlAvailable(injectionPhase)) {
            // We cannot go any further up the hierarchy. This could
            // be the FIELD group, or any group for which this has
            // been set in GCONINJE or GCONPROD. If we are here
            // anyway, it is likely that the deck set inconsistent
            // requirements, such as GRUP control mode on a well with
            // no appropriate controls defined on any of its
            // containing groups. We will therefore use the wells' bhp
            // limit equation as a fallback.
            const auto& controls = well_.wellEcl().injectionControls(summaryState);
            control_eq = bhp - controls.bhp_limit;
            return;
        } else {
            // Inject share of parents control
            const auto& parent = schedule.getGroup(group.parent(), well_.currentStep());
            efficiencyFactor *= group.getGroupEfficiencyFactor();
            getGroupInjectionControl(parent, groupStateHelper,
                                     injectorType,
                                     bhp, injection_rate,
                                     rateConverter,
                                     efficiencyFactor,
                                     control_eq);
            return;
        }
    }

    const auto& well = well_.wellEcl();

    if (!group.isInjectionGroup()) {
        // use bhp as control eq and let the updateControl code find a valid control
        const auto& controls = well.injectionControls(summaryState);
        control_eq = bhp - controls.bhp_limit;
        return;
    }

    const auto target_rate = well_state.well(well_.indexOfWell()).group_target;
    if (target_rate) {
        control_eq = injection_rate - target_rate->target_value;
    } else {
        const auto& controls = well.injectionControls(summaryState);
        control_eq = bhp - controls.bhp_limit;
    }
    return;
}

template<typename Scalar, typename IndexTraits>
std::optional<Scalar>
WellGroupControls<Scalar, IndexTraits>::
getGroupInjectionTargetRate(const Group& group,
                            const GroupStateHelperType& groupStateHelper,
                            const InjectorType& injectorType,
                            const RateConvFunc& rateConverter,
                            Scalar efficiencyFactor) const
{
    // Setting some defaults to silence warnings below.
    // Will be overwritten in the switch statement.
    Phase injectionPhase = Phase::WATER;
    switch (injectorType) {
    case InjectorType::WATER:
    {
        injectionPhase = Phase::WATER;
        break;
    }
    case InjectorType::OIL:
    {
        injectionPhase = Phase::OIL;
        break;
    }
    case InjectorType::GAS:
    {
        injectionPhase = Phase::GAS;
        break;
    }
    default:
        // Should not be here.
        assert(false);
    }
    auto currentGroupControl = groupStateHelper.groupState().injection_control(group.name(), injectionPhase);
    if (currentGroupControl == Group::InjectionCMode::FLD ||
        currentGroupControl == Group::InjectionCMode::NONE) {
        if (!group.injectionGroupControlAvailable(injectionPhase)) {
            // We cannot go any further up the hierarchy. This could
            // be the FIELD group, or any group for which this has
            // been set in GCONINJE or GCONPROD. If we are here
            // anyway, it is likely that the deck set inconsistent
            // requirements, such as GRUP control mode on a well with
            // no appropriate controls defined on any of its
            // containing groups. We will therefore use the wells' bhp
            // limit equation as a fallback.
            return std::nullopt;
        } else {
            // Inject share of parents control
            const auto& parent = groupStateHelper.schedule().getGroup( group.parent(), well_.currentStep());
            efficiencyFactor *= group.getGroupEfficiencyFactor();
            return getGroupInjectionTargetRate(parent, groupStateHelper, injectorType,
                                               rateConverter, efficiencyFactor);
        }
    }

    if (!group.isInjectionGroup()) {
        return std::nullopt;
    }

    const auto& group_target = groupStateHelper.wellState().well(well_.indexOfWell()).group_target;
    if (!group_target.has_value()) {
        return std::nullopt;
    }

    return group_target->target_value;
}

template<typename Scalar, typename IndexTraits>
template<class EvalWell>
void WellGroupControls<Scalar, IndexTraits>::
getGroupProductionControl(const Group& group,
                          const GroupStateHelperType& groupStateHelper,
                          const EvalWell& bhp,
                          const std::vector<EvalWell>& rates,
                          const RateConvFunc& rateConverter,
                          Scalar efficiencyFactor,
                          EvalWell& control_eq) const
{
    const auto& group_state = groupStateHelper.groupState();
    const auto& summaryState = groupStateHelper.summaryState();
    const auto& schedule = groupStateHelper.schedule();
    const auto& well_state = groupStateHelper.wellState();
    const Group::ProductionCMode& currentGroupControl = group_state.production_control(group.name());
    if (currentGroupControl == Group::ProductionCMode::FLD ||
        currentGroupControl == Group::ProductionCMode::NONE) {
        if (!group.productionGroupControlAvailable()) {
            // We cannot go any further up the hierarchy. This could
            // be the FIELD group, or any group for which this has
            // been set in GCONINJE or GCONPROD. If we are here
            // anyway, it is likely that the deck set inconsistent
            // requirements, such as GRUP control mode on a well with
            // no appropriate controls defined on any of its
            // containing groups. We will therefore use the wells' bhp
            // limit equation as a fallback.
            const auto& controls = well_.wellEcl().productionControls(summaryState);
            control_eq = bhp - controls.bhp_limit;
            return;
        } else {
            // Produce share of parents control
            const auto& parent = schedule.getGroup(group.parent(), well_.currentStep());
            efficiencyFactor *= group.getGroupEfficiencyFactor();
            getGroupProductionControl(parent, groupStateHelper, bhp,
                                      rates, rateConverter,
                                      efficiencyFactor, control_eq);
            return;
        }
    }

    const auto& well = well_.wellEcl();

    if (!group.isProductionGroup()) {
        // use bhp as control eq and let the updateControl code find a valid control
        const auto& controls = well.productionControls(summaryState);
        control_eq = bhp - controls.bhp_limit;
        return;
    }

    // If we are here, we are at the topmost group to be visited in the recursion.
    // This is the group containing the control we will check against.

    // Make conversion factors for RESV <-> surface rates.
    std::vector<Scalar> resv_coeff(well_.phaseUsage().numActivePhases(), 1.0);
    rateConverter(0, well_.pvtRegionIdx(), group.name(), resv_coeff); // FIPNUM region 0 here, should use FIPNUM from WELSPECS.

    // gconsale may adjust the grat target.
    // the adjusted rates is send to the targetCalculator

    GroupStateHelpers::TargetCalculator<Scalar, IndexTraits> tcalc{groupStateHelper, resv_coeff, group};

    const auto target_rate = well_state.well(well_.indexOfWell()).group_target;
    if (target_rate) {
        const auto current_rate = -tcalc.calcModeRateFromRates(rates); // Switch sign since 'rates' are negative for producers.
        control_eq = current_rate - target_rate->target_value;
    } else {
        const auto& controls = well.productionControls(summaryState);
        control_eq = bhp - controls.bhp_limit;
    }
    return;
}

template<typename Scalar, typename IndexTraits>
Scalar
WellGroupControls<Scalar, IndexTraits>::
getGroupProductionTargetRate(const Group& group,
                             const GroupStateHelperType& groupStateHelper,
                             const RateConvFunc& rateConverter,
                             Scalar efficiencyFactor) const
{
    const auto& well_state = groupStateHelper.wellState();
    const auto& group_state = groupStateHelper.groupState();
    const auto& schedule = groupStateHelper.schedule();
    const Group::ProductionCMode& currentGroupControl = group_state.production_control(group.name());
    if (currentGroupControl == Group::ProductionCMode::FLD ||
        currentGroupControl == Group::ProductionCMode::NONE) {
        if (!group.productionGroupControlAvailable()) {
            return 1.0;
        } else {
            // Produce share of parents control
            const auto& parent = schedule.getGroup(group.parent(), well_.currentStep());
            efficiencyFactor *= group.getGroupEfficiencyFactor();
            return getGroupProductionTargetRate(parent, groupStateHelper,
                                                rateConverter, efficiencyFactor);
        }
    }

    if (!group.isProductionGroup()) {
        return 1.0;
    }

    // If we are here, we are at the topmost group to be visited in the recursion.
    // This is the group containing the control we will check against.

    // Make conversion factors for RESV <-> surface rates.
    std::vector<Scalar> resv_coeff(well_.phaseUsage().numActivePhases(), 1.0);
    rateConverter(0, well_.pvtRegionIdx(), group.name(), resv_coeff); // FIPNUM region 0 here, should use FIPNUM from WELSPECS.

    GroupStateHelpers::TargetCalculator<Scalar, IndexTraits> tcalc{groupStateHelper,
                                                                   resv_coeff,
                                                                   group};

    const auto target_rate = well_state.well(well_.indexOfWell()).group_target;
    if (!target_rate) {
        return 1.0;
    }
    if (target_rate->target_value == 0.0) {
        return 0.0;
    }
    const auto& ws = well_state.well(well_.indexOfWell());
    const auto& rates = ws.surface_rates;
    const auto current_rate = -tcalc.calcModeRateFromRates(rates); // Switch sign since 'rates' are negative for producers.
    Scalar scale = 1.0;
    if (current_rate > 1e-14)
        scale = target_rate->target_value / current_rate;

    return scale;
}

#define INSTANTIATE(T,...)                                               \
    template void WellGroupControls<T, BlackOilDefaultFluidSystemIndices>::                                 \
        getGroupInjectionControl(const Group&,                           \
                                 const GroupStateHelper<T, BlackOilDefaultFluidSystemIndices>&,             \
                                 const InjectorType&,                    \
                                 const __VA_ARGS__& bhp,                 \
                                 const __VA_ARGS__& injection_rate,      \
                                 const RateConvFunc& rateConverter,      \
                                 T efficiencyFactor,                     \
                                 __VA_ARGS__& control_eq) const;         \
    template void WellGroupControls<T, BlackOilDefaultFluidSystemIndices>::                                 \
        getGroupProductionControl(const Group&,                          \
                                  const GroupStateHelper<T, BlackOilDefaultFluidSystemIndices>&,            \
                                  const __VA_ARGS__& bhp,                \
                                  const std::vector<__VA_ARGS__>&,       \
                                  const RateConvFunc& rateConverter,     \
                                  T efficiencyFactor,                    \
                                  __VA_ARGS__& control_eq) const;

#define INSTANTIATE_TYPE(T)                      \
    template class WellGroupControls<T, BlackOilDefaultFluidSystemIndices>;         \
    INSTANTIATE(T,DenseAd::Evaluation<T,3,0u>)   \
    INSTANTIATE(T,DenseAd::Evaluation<T,4,0u>)   \
    INSTANTIATE(T,DenseAd::Evaluation<T,5,0u>)   \
    INSTANTIATE(T,DenseAd::Evaluation<T,6,0u>)   \
    INSTANTIATE(T,DenseAd::Evaluation<T,7,0u>)   \
    INSTANTIATE(T,DenseAd::Evaluation<T,8,0u>)   \
    INSTANTIATE(T,DenseAd::Evaluation<T,9,0u>)   \
    INSTANTIATE(T,DenseAd::Evaluation<T,10,0u>)  \
    INSTANTIATE(T,DenseAd::Evaluation<T,-1,4u>)  \
    INSTANTIATE(T,DenseAd::Evaluation<T,-1,5u>)  \
    INSTANTIATE(T,DenseAd::Evaluation<T,-1,6u>)  \
    INSTANTIATE(T,DenseAd::Evaluation<T,-1,7u>)  \
    INSTANTIATE(T,DenseAd::Evaluation<T,-1,8u>)  \
    INSTANTIATE(T,DenseAd::Evaluation<T,-1,9u>)  \
    INSTANTIATE(T,DenseAd::Evaluation<T,-1,10u>) \
    INSTANTIATE(T,DenseAd::Evaluation<T,-1,11u>)

INSTANTIATE_TYPE(double)

#if FLOW_INSTANTIATE_FLOAT
INSTANTIATE_TYPE(float)
#endif

} // namespace Opm
