// -*- mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
// vi: set et ts=4 sw=4 sts=4:
/*
  Copyright 2024 Equinor AS

  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/flow/RegionPhasePVAverage.hpp>

#include <opm/input/eclipse/EclipseState/Grid/FieldProps.hpp>

#include <opm/simulators/utils/ParallelCommunication.hpp>

#include <algorithm>
#include <cstddef>
#include <functional>
#include <vector>

namespace {
    std::vector<std::string> fipRegionNames(const std::vector<std::string>& regionNames)
    {
        auto regs = regionNames;

        std::ranges::sort(regs);

        return { regs.begin(), std::unique(regs.begin(), regs.end()) };
    }

    std::vector<std::vector<double>::size_type>
    regionStartPointers(const std::vector<std::string>&                            regionNames,
                        const Opm::RegionPhasePoreVolAverage::RegionArrayAccessor& getRegionArray,
                        const Opm::Parallel::Communication&                        comm)
    {
        // All elements get an initial value of 1 to account for the maximum
        // region ID.  There should be "max ID + 1" elements for each region
        // set.  Unused IDs--e.g., 0 if IDs start at 1--will effectively be
        // wasted in this scheme.  If that becomes a problem we might
        // consider some kind of renumbering approach.
        auto start = std::vector<std::vector<double>::size_type>(regionNames.size() + 1, 1);

        const auto nset = regionNames.size();
        for (auto rset = 0*nset; rset < nset; ++rset) {
            const auto& reg = getRegionArray(regionNames[rset]);

            const auto m = std::ranges::max_element(reg);
            if (m == reg.end()) { // reg.empty()
                continue;
            }

            start[rset + 1] += *m;
        }

        comm.max(start.data(), start.size());

        std::partial_sum(start.begin(), start.end(), start.begin());

        return start;
    }
} // Anonymous namespace

Opm::RegionPhasePoreVolAverage::
RegionPhasePoreVolAverage(const Parallel::Communication&  comm,
                          const std::size_t               numPhases,
                          const std::vector<std::string>& regionNames,
                          RegionArrayAccessor             getRegionArray)
    : comm_           { std::cref(comm) }
    , np_             { numPhases }
    , rsetNames_      { fipRegionNames(regionNames) }
    , getRegionArray_ { std::move(getRegionArray) }
    , rsStart_        { regionStartPointers(rsetNames_, getRegionArray_, comm_) }
    , x_              (rsStart_.back() * numPhases * AvgType::NumTypes * Element::NumElem)
{}

double Opm::RegionPhasePoreVolAverage::fieldValue(const Phase& p) const
{
    return this->averageValueWithFallback(this->fieldStartIx(p.ix));
}

double
Opm::RegionPhasePoreVolAverage::
value(std::string_view rset, const Phase& p, const Region& r) const
{
    auto rsetPos = std::lower_bound(this->rsetNames_.begin(),
                                    this->rsetNames_.end(), rset);

    if ((rsetPos == this->rsetNames_.end()) || (*rsetPos != rset)) {
        // rset is not a known region set name.
        return 0.0;             // Maybe nullopt or throw here...
    }

    const auto rsetIx = std::distance(this->rsetNames_.begin(), rsetPos);
    return this->averageValueWithFallback(this->rsetStartIx(rsetIx, r.ix, p.ix));
}

void Opm::RegionPhasePoreVolAverage::prepareAccumulation()
{
    std::ranges::fill(this->x_, 0.0);
}

void Opm::RegionPhasePoreVolAverage::
addCell(const std::size_t activeCell,
        const Phase&      p,
        const CellValue&  cv)
{
    this->add(this->fieldStartIx(p.ix), cv);

    for (auto rset = 0*this->rsetNames_.size(); rset < this->rsetNames_.size(); ++rset) {
        this->add(this->rsetStartIx(rset, this->regionIndex(rset, activeCell), p.ix), cv);
    }
}

void Opm::RegionPhasePoreVolAverage::accumulateParallel()
{
    this->comm_.get().sum(this->x_.data(), this->x_.size());
}

double Opm::RegionPhasePoreVolAverage::averageValueWithFallback(const Ix start) const
{
    const auto spv_w = this->weight(start, AvgType::SatPV);

    return (spv_w > 0.0)
        ? this->averageValue(start, AvgType::SatPV)
        : this->averageValue(start, AvgType::PV);
}

double Opm::RegionPhasePoreVolAverage::averageValue(const Ix      start,
                                                    const AvgType type) const
{
    return this->value(start, type) / this->weight(start, type);
}

Opm::RegionPhasePoreVolAverage::Ix
Opm::RegionPhasePoreVolAverage::fieldStartIx(const unsigned int phase) const
{
    return this->startIx(0, phase);
}

Opm::RegionPhasePoreVolAverage::Ix
Opm::RegionPhasePoreVolAverage::rsetStartIx(const std::size_t  rset,
                                            const int          region,
                                            const unsigned int phase) const
{
    return this->startIx(this->rsStart_[rset] + region, phase);
}

Opm::RegionPhasePoreVolAverage::Ix
Opm::RegionPhasePoreVolAverage::startIx(const std::size_t offset,
                                        const unsigned int phase) const
{
    return (offset*this->np_ + phase) * AvgType::NumTypes * Element::NumElem;
}

int Opm::RegionPhasePoreVolAverage::regionIndex(const std::size_t rset,
                                                const std::size_t activeCell) const
{
    return this->getRegionArray_(this->rsetNames_[rset])[activeCell];
}

void Opm::RegionPhasePoreVolAverage::add(const Ix start, const CellValue& cv)
{
    this->add(start, AvgType::SatPV, cv.value, cv.sat * cv.porv);
    this->add(start, AvgType::PV   , cv.value,          cv.porv);
}

void Opm::RegionPhasePoreVolAverage::add(const Ix      start,
                                         const AvgType type,
                                         const double  x,
                                         const double  w)
{
    this->value (start, type) += w * x;
    this->weight(start, type) += w;
}

double& Opm::RegionPhasePoreVolAverage::value(const Ix start, const AvgType type)
{
    return this->x_[ this->valueArrayIndex(start, type, Element::Value) ];
}

double& Opm::RegionPhasePoreVolAverage::weight(const Ix start, const AvgType type)
{
    return this->x_[ this->valueArrayIndex(start, type, Element::Weight) ];
}

double Opm::RegionPhasePoreVolAverage::value(const Ix start, const AvgType type) const
{
    return this->x_[ this->valueArrayIndex(start, type, Element::Value) ];
}

double Opm::RegionPhasePoreVolAverage::weight(const Ix start, const AvgType type) const
{
    return this->x_[ this->valueArrayIndex(start, type, Element::Weight) ];
}

Opm::RegionPhasePoreVolAverage::Ix
Opm::RegionPhasePoreVolAverage::valueArrayIndex(const Ix      start,
                                                const AvgType type,
                                                const Element element) const
{
    return start + static_cast<Ix>(type) * static_cast<Ix>(Element::NumElem)
                 + static_cast<Ix>(element);
}
