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

#include <config.h>
#include <ebos/ecltransmissibility.hh>

#include <dune/common/version.hh>
#include <dune/grid/common/mcmgmapper.hh>

#include <opm/grid/CpGrid.hpp>
#include <opm/grid/polyhedralgrid.hh>

#include <opm/parser/eclipse/EclipseState/EclipseState.hpp>
#include <opm/parser/eclipse/EclipseState/Grid/FaceDir.hpp>
#include <opm/parser/eclipse/EclipseState/Grid/FieldPropsManager.hpp>
#include <opm/parser/eclipse/EclipseState/Grid/TransMult.hpp>
#include <opm/parser/eclipse/Units/Units.hpp>

#if HAVE_DUNE_FEM
#include <dune/fem/gridpart/adaptiveleafgridpart.hh>
#include <dune/fem/gridpart/common/gridpart2gridview.hh>
#include <ebos/femcpgridcompat.hh>
#endif

#include <fmt/format.h>

#include <algorithm>
#include <cassert>
#include <cmath>
#include <sstream>
#include <stdexcept>

namespace {

constexpr unsigned elemIdxShift = 32; // bits

std::uint64_t isId(std::uint32_t elemIdx1, std::uint32_t elemIdx2)
{
    std::uint32_t elemAIdx = std::min(elemIdx1, elemIdx2);
    std::uint64_t elemBIdx = std::max(elemIdx1, elemIdx2);

    return (elemBIdx<<elemIdxShift) + elemAIdx;
}

std::pair<std::uint32_t, std::uint32_t> isIdReverse(const std::uint64_t& id)
{
    // Assigning an unsigned integer to a narrower type discards the most significant bits.
    // See "The C programming language", section A.6.2.
    // NOTE that the ordering of element A and B may have changed
    std::uint32_t elemAIdx = id;
    std::uint32_t elemBIdx = (id - elemAIdx) >> elemIdxShift;

    return std::make_pair(elemAIdx, elemBIdx);
}

std::uint64_t directionalIsId(std::uint32_t elemIdx1, std::uint32_t elemIdx2)
{
    return (std::uint64_t(elemIdx1)<<elemIdxShift) + elemIdx2;
}

}

namespace Opm {

template<class Grid, class GridView, class ElementMapper, class Scalar>
EclTransmissibility<Grid,GridView,ElementMapper,Scalar>::
EclTransmissibility(const EclipseState& eclState,
                    const GridView& gridView,
                    const Dune::CartesianIndexMapper<Grid>& cartMapper,
                    const Grid& grid,
                    std::function<std::array<double,dimWorld>(int)> centroids,
                    bool enableEnergy,
                    bool enableDiffusivity)
      : eclState_(eclState)
      , gridView_(gridView)
      , cartMapper_(cartMapper)
      , grid_(grid)
      , centroids_(centroids)
      , enableEnergy_(enableEnergy)
      , enableDiffusivity_(enableDiffusivity)
{
    const UnitSystem& unitSystem = eclState_.getDeckUnitSystem();
    transmissibilityThreshold_  = unitSystem.parse("Transmissibility").getSIScaling() * 1e-6;
}

template<class Grid, class GridView, class ElementMapper, class Scalar>
Scalar EclTransmissibility<Grid,GridView,ElementMapper,Scalar>::
transmissibility(unsigned elemIdx1, unsigned elemIdx2) const
{
    return trans_.at(isId(elemIdx1, elemIdx2));
}

template<class Grid, class GridView, class ElementMapper, class Scalar>
Scalar EclTransmissibility<Grid,GridView,ElementMapper,Scalar>::
transmissibilityBoundary(unsigned elemIdx, unsigned boundaryFaceIdx) const
{
    return transBoundary_.at(std::make_pair(elemIdx, boundaryFaceIdx));
}

template<class Grid, class GridView, class ElementMapper, class Scalar>
Scalar EclTransmissibility<Grid,GridView,ElementMapper,Scalar>::
thermalHalfTrans(unsigned insideElemIdx, unsigned outsideElemIdx) const
{
    return thermalHalfTrans_.at(directionalIsId(insideElemIdx, outsideElemIdx));
}

template<class Grid, class GridView, class ElementMapper, class Scalar>
Scalar EclTransmissibility<Grid,GridView,ElementMapper,Scalar>::
thermalHalfTransBoundary(unsigned insideElemIdx, unsigned boundaryFaceIdx) const
{
    return thermalHalfTransBoundary_.at(std::make_pair(insideElemIdx, boundaryFaceIdx));
}

template<class Grid, class GridView, class ElementMapper, class Scalar>
Scalar EclTransmissibility<Grid,GridView,ElementMapper,Scalar>::
diffusivity(unsigned elemIdx1, unsigned elemIdx2) const
{
    if (diffusivity_.empty())
        return 0.0;

    return diffusivity_.at(isId(elemIdx1, elemIdx2));

}

template<class Grid, class GridView, class ElementMapper, class Scalar>
void EclTransmissibility<Grid,GridView,ElementMapper,Scalar>::
update(bool global)
{
    const auto& cartDims = cartMapper_.cartesianDimensions();
    auto& transMult = eclState_.getTransMult();
    const auto& comm = gridView_.comm();
    ElementMapper elemMapper(gridView_, Dune::mcmgElementLayout());

    // get the ntg values, the ntg values are modified for the cells merged with minpv
    const std::vector<double>& ntg = eclState_.fieldProps().get_double("NTG");
    const bool updateDiffusivity = eclState_.getSimulationConfig().isDiffusive() && enableDiffusivity_;
    unsigned numElements = elemMapper.size();

    extractPermeability_();

    // calculate the axis specific centroids of all elements
    std::array<std::vector<DimVector>, dimWorld> axisCentroids;

    for (unsigned dimIdx = 0; dimIdx < dimWorld; ++dimIdx)
        axisCentroids[dimIdx].resize(numElements);

    auto elemIt = gridView_.template begin</*codim=*/ 0>();
    const auto& elemEndIt = gridView_.template end</*codim=*/ 0>();
    size_t centroidIdx = 0;
    for (; elemIt != elemEndIt; ++elemIt, ++centroidIdx) {
        const auto& elem = *elemIt;
        unsigned elemIdx = elemMapper.index(elem);

        // compute the axis specific "centroids" used for the transmissibilities. for
        // consistency with the flow simulator, we use the element centers as
        // computed by opm-parser's Opm::EclipseGrid class for all axes.
        std::array<double, dimWorld> centroid = centroids_(elemIdx);

        for (unsigned axisIdx = 0; axisIdx < dimWorld; ++axisIdx)
            for (unsigned dimIdx = 0; dimIdx < dimWorld; ++dimIdx)
                axisCentroids[axisIdx][elemIdx][dimIdx] = centroid[dimIdx];
    }

    // reserving some space in the hashmap upfront saves quite a bit of time because
    // resizes are costly for hashmaps and there would be quite a few of them if we
    // would not have a rough idea of how large the final map will be (the rough idea
    // is a conforming Cartesian grid).
    trans_.clear();
    trans_.reserve(numElements*3*1.05);

    transBoundary_.clear();

    // if energy is enabled, let's do the same for the "thermal half transmissibilities"
    if (enableEnergy_) {
        thermalHalfTrans_.clear();
        thermalHalfTrans_.reserve(numElements*6*1.05);

        thermalHalfTransBoundary_.clear();
    }

    // if diffusion is enabled, let's do the same for the "diffusivity"
    if (updateDiffusivity) {
        diffusivity_.clear();
        diffusivity_.reserve(numElements*3*1.05);
        extractPorosity_();
    }

    // The MULTZ needs special case if the option is ALL
    // Then the smallest multiplier is applied.
    // Default is to apply the top and bottom multiplier
    bool useSmallestMultiplier;
    if (comm.rank() == 0) {
        const auto& eclGrid = eclState_.getInputGrid();
        useSmallestMultiplier = eclGrid.getMultzOption() == PinchMode::ModeEnum::ALL;
    }
    if (global && comm.size() > 1) {
        comm.broadcast(&useSmallestMultiplier, 1, 0);
    }

    // compute the transmissibilities for all intersections
    elemIt = gridView_.template begin</*codim=*/ 0>();
    for (; elemIt != elemEndIt; ++elemIt) {
        const auto& elem = *elemIt;
        unsigned elemIdx = elemMapper.index(elem);

        auto isIt = gridView_.ibegin(elem);
        const auto& isEndIt = gridView_.iend(elem);
        unsigned boundaryIsIdx = 0;
        for (; isIt != isEndIt; ++ isIt) {
            // store intersection, this might be costly
            const auto& intersection = *isIt;

            // deal with grid boundaries
            if (intersection.boundary()) {
                // compute the transmissibilty for the boundary intersection
                const auto& geometry = intersection.geometry();
                const auto& faceCenterInside = geometry.center();

                auto faceAreaNormal = intersection.centerUnitOuterNormal();
                faceAreaNormal *= geometry.volume();

                Scalar transBoundaryIs;
                computeHalfTrans_(transBoundaryIs,
                                  faceAreaNormal,
                                  intersection.indexInInside(),
                                  distanceVector_(faceCenterInside,
                                                  intersection.indexInInside(),
                                                  elemIdx,
                                                  axisCentroids),
                                  permeability_[elemIdx]);

                // normally there would be two half-transmissibilities that would be
                // averaged. on the grid boundary there only is the half
                // transmissibility of the interior element.
                transBoundary_[std::make_pair(elemIdx, boundaryIsIdx)] = transBoundaryIs;

                // for boundary intersections we also need to compute the thermal
                // half transmissibilities
                if (enableEnergy_) {
                    Scalar transBoundaryEnergyIs;
                    computeHalfDiffusivity_(transBoundaryEnergyIs,
                                            faceAreaNormal,
                                            distanceVector_(faceCenterInside,
                                                            intersection.indexInInside(),
                                                            elemIdx,
                                                            axisCentroids),
                                            1.0);
                    thermalHalfTransBoundary_[std::make_pair(elemIdx, boundaryIsIdx)] =
                        transBoundaryEnergyIs;
                }

                ++ boundaryIsIdx;
                continue;
            }

            if (!intersection.neighbor()) {
                // elements can be on process boundaries, i.e. they are not on the
                // domain boundary yet they don't have neighbors.
                ++ boundaryIsIdx;
                continue;
            }

            const auto& outsideElem = intersection.outside();
            unsigned outsideElemIdx = elemMapper.index(outsideElem);

            unsigned insideCartElemIdx = cartMapper_.cartesianIndex(elemIdx);
            unsigned outsideCartElemIdx = cartMapper_.cartesianIndex(outsideElemIdx);

            // we only need to calculate a face's transmissibility
            // once...
            if (insideCartElemIdx > outsideCartElemIdx)
                continue;

            // local indices of the faces of the inside and
            // outside elements which contain the intersection
            int insideFaceIdx  = intersection.indexInInside();
            int outsideFaceIdx = intersection.indexInOutside();

            if (insideFaceIdx == -1) {
                // NNC. Set zero transmissibility, as it will be
                // *added to* by applyNncToGridTrans_() later.
                assert(outsideFaceIdx == -1);
                trans_[isId(elemIdx, outsideElemIdx)] = 0.0;
                continue;
            }

            DimVector faceCenterInside;
            DimVector faceCenterOutside;
            DimVector faceAreaNormal;

            typename std::is_same<Grid, Dune::CpGrid>::type isCpGrid;
            computeFaceProperties(intersection,
                                  elemIdx,
                                  insideFaceIdx,
                                  outsideElemIdx,
                                  outsideFaceIdx,
                                  faceCenterInside,
                                  faceCenterOutside,
                                  faceAreaNormal,
                                  isCpGrid);

            Scalar halfTrans1;
            Scalar halfTrans2;

            computeHalfTrans_(halfTrans1,
                              faceAreaNormal,
                              insideFaceIdx,
                              distanceVector_(faceCenterInside,
                                              intersection.indexInInside(),
                                              elemIdx,
                                              axisCentroids),
                              permeability_[elemIdx]);
            computeHalfTrans_(halfTrans2,
                              faceAreaNormal,
                              outsideFaceIdx,
                              distanceVector_(faceCenterOutside,
                                              intersection.indexInOutside(),
                                              outsideElemIdx,
                                              axisCentroids),
                              permeability_[outsideElemIdx]);

            applyNtg_(halfTrans1, insideFaceIdx, elemIdx, ntg);
            applyNtg_(halfTrans2, outsideFaceIdx, outsideElemIdx, ntg);

            // convert half transmissibilities to full face
            // transmissibilities using the harmonic mean
            Scalar trans;
            if (std::abs(halfTrans1) < 1e-30 || std::abs(halfTrans2) < 1e-30)
                // avoid division by zero
                trans = 0.0;
            else
                trans = 1.0 / (1.0/halfTrans1 + 1.0/halfTrans2);

            // apply the full face transmissibility multipliers
            // for the inside ...

            if (useSmallestMultiplier)
            {
                // Currently PINCH(4) is never queries and hence  PINCH(4) == TOPBOT is assumed
                // and in this branch PINCH(5) == ALL holds
                applyAllZMultipliers_(trans, insideFaceIdx, outsideFaceIdx, insideCartElemIdx,
                                      outsideCartElemIdx, transMult, cartDims,
                                      /* pinchTop= */ false);
            }
            else
            {
                applyMultipliers_(trans, insideFaceIdx, insideCartElemIdx, transMult);
                // ... and outside elements
                applyMultipliers_(trans, outsideFaceIdx, outsideCartElemIdx, transMult);
            }

            // apply the region multipliers (cf. the MULTREGT keyword)
            FaceDir::DirEnum faceDir;
            switch (insideFaceIdx) {
            case 0:
            case 1:
                faceDir = FaceDir::XPlus;
                break;

            case 2:
            case 3:
                faceDir = FaceDir::YPlus;
                break;

            case 4:
            case 5:
                faceDir = FaceDir::ZPlus;
                break;

            default:
                throw std::logic_error("Could not determine a face direction");
            }

            trans *= transMult.getRegionMultiplier(insideCartElemIdx,
                                                   outsideCartElemIdx,
                                                   faceDir);

            trans_[isId(elemIdx, outsideElemIdx)] = trans;

            // update the "thermal half transmissibility" for the intersection
            if (enableEnergy_) {

                Scalar halfDiffusivity1;
                Scalar halfDiffusivity2;

                computeHalfDiffusivity_(halfDiffusivity1,
                                        faceAreaNormal,
                                        distanceVector_(faceCenterInside,
                                                        intersection.indexInInside(),
                                                        elemIdx,
                                                        axisCentroids),
                                        1.0);
                computeHalfDiffusivity_(halfDiffusivity2,
                                        faceAreaNormal,
                                        distanceVector_(faceCenterOutside,
                                                        intersection.indexInOutside(),
                                                        outsideElemIdx,
                                                        axisCentroids),
                                        1.0);
                //TODO Add support for multipliers
                thermalHalfTrans_[directionalIsId(elemIdx, outsideElemIdx)] = halfDiffusivity1;
                thermalHalfTrans_[directionalIsId(outsideElemIdx, elemIdx)] = halfDiffusivity2;
           }

            // update the "diffusive half transmissibility" for the intersection
            if (updateDiffusivity) {

                Scalar halfDiffusivity1;
                Scalar halfDiffusivity2;

                computeHalfDiffusivity_(halfDiffusivity1,
                                        faceAreaNormal,
                                        distanceVector_(faceCenterInside,
                                                        intersection.indexInInside(),
                                                        elemIdx,
                                                        axisCentroids),
                                        porosity_[elemIdx]);
                computeHalfDiffusivity_(halfDiffusivity2,
                                        faceAreaNormal,
                                        distanceVector_(faceCenterOutside,
                                                        intersection.indexInOutside(),
                                                        outsideElemIdx,
                                                        axisCentroids),
                                        porosity_[outsideElemIdx]);

                applyNtg_(halfDiffusivity1, insideFaceIdx, elemIdx, ntg);
                applyNtg_(halfDiffusivity2, outsideFaceIdx, outsideElemIdx, ntg);

                //TODO Add support for multipliers
                Scalar diffusivity;
                if (std::abs(halfDiffusivity1) < 1e-30 || std::abs(halfDiffusivity2) < 1e-30)
                    // avoid division by zero
                    diffusivity = 0.0;
                else
                    diffusivity = 1.0 / (1.0/halfDiffusivity1 + 1.0/halfDiffusivity2);


                diffusivity_[isId(elemIdx, outsideElemIdx)] = diffusivity;
           }
        }
    }

    // potentially overwrite and/or modify  transmissibilities based on input from deck
    updateFromEclState_(global);

    // Create mapping from global to local index
    std::unordered_map<std::size_t,int> globalToLocal;

    // loop over all elements (global grid) and store Cartesian index
    elemIt = grid_.leafGridView().template begin<0>();

    for (; elemIt != elemEndIt; ++elemIt) {
        int elemIdx = elemMapper.index(*elemIt);
        int cartElemIdx = cartMapper_.cartesianIndex(elemIdx);
        globalToLocal[cartElemIdx] = elemIdx;
    }
    applyEditNncToGridTrans_(globalToLocal);
    applyNncToGridTrans_(globalToLocal);

    //remove very small non-neighbouring transmissibilities
    removeSmallNonCartesianTransmissibilities_();
}

template<class Grid, class GridView, class ElementMapper, class Scalar>
void EclTransmissibility<Grid,GridView,ElementMapper,Scalar>::
extractPermeability_()
{
    unsigned numElem = gridView_.size(/*codim=*/0);
    permeability_.resize(numElem);

    // read the intrinsic permeabilities from the eclState. Note that all arrays
    // provided by eclState are one-per-cell of "uncompressed" grid, whereas the
    // simulation grid might remove a few elements. (e.g. because it is distributed
    // over several processes.)
    const auto& fp = eclState_.fieldProps();
    if (fp.has_double("PERMX")) {
        const std::vector<double>& permxData = fp.get_double("PERMX");

        std::vector<double> permyData;
        if (fp.has_double("PERMY"))
            permyData = fp.get_double("PERMY");
        else
            permyData = permxData;

        std::vector<double> permzData;
        if (fp.has_double("PERMZ"))
            permzData = fp.get_double("PERMZ");
        else
            permzData = permxData;

        for (size_t dofIdx = 0; dofIdx < numElem; ++ dofIdx) {
            permeability_[dofIdx] = 0.0;
            permeability_[dofIdx][0][0] = permxData[dofIdx];
            permeability_[dofIdx][1][1] = permyData[dofIdx];
            permeability_[dofIdx][2][2] = permzData[dofIdx];
        }

        // for now we don't care about non-diagonal entries

    }
    else
        throw std::logic_error("Can't read the intrinsic permeability from the ecl state. "
                               "(The PERM{X,Y,Z} keywords are missing)");
}

template<class Grid, class GridView, class ElementMapper, class Scalar>
void EclTransmissibility<Grid,GridView,ElementMapper,Scalar>::
extractPorosity_()
{
    // read the intrinsic porosity from the eclState. Note that all arrays
    // provided by eclState are one-per-cell of "uncompressed" grid, whereas the
    // simulation grid might remove a few elements. (e.g. because it is distributed
    // over several processes.)
    const auto& fp = eclState_.fieldProps();
    if (fp.has_double("PORO")) {
        porosity_ = fp.get_double("PORO");
    }
    else
        throw std::logic_error("Can't read the porosityfrom the ecl state. "
                               "(The PORO keywords are missing)");
}

template<class Grid, class GridView, class ElementMapper, class Scalar>
void EclTransmissibility<Grid,GridView,ElementMapper,Scalar>::
removeSmallNonCartesianTransmissibilities_()
{
    const auto& cartDims = cartMapper_.cartesianDimensions();
    for (auto&& trans: trans_) {
        if (trans.second < transmissibilityThreshold_) {
            const auto& id = trans.first;
            const auto& elements = isIdReverse(id);
            int gc1 = std::min(cartMapper_.cartesianIndex(elements.first), cartMapper_.cartesianIndex(elements.second));
            int gc2 = std::max(cartMapper_.cartesianIndex(elements.first), cartMapper_.cartesianIndex(elements.second));

            // only adjust the NNCs
            if (gc2 - gc1 == 1 || gc2 - gc1 == cartDims[0] || gc2 - gc1 == cartDims[0]*cartDims[1])
                continue;

            //remove transmissibilities less than the threshold (by default 1e-6 in the deck's unit system)
            trans.second = 0.0;
        }
    }
}

template<class Grid, class GridView, class ElementMapper, class Scalar>
void EclTransmissibility<Grid,GridView,ElementMapper,Scalar>::
applyAllZMultipliers_(Scalar& trans,
                      unsigned insideFaceIdx,
                      unsigned outsideFaceIdx,
                      unsigned insideCartElemIdx,
                      unsigned outsideCartElemIdx,
                      const TransMult& transMult,
                      const std::array<int, dimWorld>& cartDims,
                      bool pinchTop)
{
    if (insideFaceIdx > 3) { // top or or bottom
        assert(insideFaceIdx==5); // as insideCartElemIdx < outsideCartElemIdx holds for the Z column
        assert(outsideCartElemIdx > insideCartElemIdx);
        auto lastCartElemIdx = outsideCartElemIdx - cartDims[0]*cartDims[1];
        // Last multiplier
        Scalar mult = transMult.getMultiplier(lastCartElemIdx , FaceDir::ZPlus);

        if ( !pinchTop )
        {
            // pick the smallest multiplier for Z+ while looking down the pillar until reaching the other end of the connection
            // While Z- is not all used here.
            for(auto cartElemIdx = insideCartElemIdx; cartElemIdx < lastCartElemIdx;
                cartElemIdx += cartDims[0]*cartDims[1])
            {
                mult = std::min(mult, transMult.getMultiplier(cartElemIdx, FaceDir::ZPlus));
            }
        }

        trans *= mult;
        applyMultipliers_(trans, outsideFaceIdx, outsideCartElemIdx, transMult);
    }
    else
    {
        applyMultipliers_(trans, insideFaceIdx, insideCartElemIdx, transMult);
        applyMultipliers_(trans, outsideFaceIdx, outsideCartElemIdx, transMult);
    }
}

template<class Grid, class GridView, class ElementMapper, class Scalar>
void EclTransmissibility<Grid,GridView,ElementMapper,Scalar>::
updateFromEclState_(bool global)
{
    const FieldPropsManager* fp =
        (global) ? &(eclState_.fieldProps()) :
        &(eclState_.globalFieldProps());

    std::array<bool,3> is_tran {fp->tran_active("TRANX"),
                                fp->tran_active("TRANY"),
                                fp->tran_active("TRANZ")};

    if( !(is_tran[0] ||is_tran[1] || is_tran[2]) )
    {
        // Skip unneeded expensive traversals
        return;
    }

    std::array<std::string, 3> keywords {"TRANX", "TRANY", "TRANZ"};
    std::array<std::vector<double>,3> trans = createTransmissibilityArrays_(is_tran);
    auto key = keywords.begin();
    auto perform = is_tran.begin();

    for (auto it = trans.begin(); it != trans.end(); ++it, ++key, ++perform)
    {
        if(perform)
            fp->apply_tran(*key, *it);
    }

    resetTransmissibilityFromArrays_(is_tran, trans);
}

template<class Grid, class GridView, class ElementMapper, class Scalar>
std::array<std::vector<double>,3>
EclTransmissibility<Grid,GridView,ElementMapper,Scalar>::
createTransmissibilityArrays_(const std::array<bool,3>& is_tran)
{
    const auto& cartDims = cartMapper_.cartesianDimensions();
    ElementMapper elemMapper(gridView_, Dune::mcmgElementLayout());

    auto numElem = gridView_.size(/*codim=*/0);
    std::array<std::vector<double>,3> trans =
        { std::vector<double>(is_tran[0] ? numElem : 0, 0),
          std::vector<double>(is_tran[1] ? numElem : 0, 0),
          std::vector<double>(is_tran[2] ? numElem : 0, 0)};

    // compute the transmissibilities for all intersections
    auto elemIt = gridView_.template begin</*codim=*/ 0>();
    const auto& elemEndIt = gridView_.template end</*codim=*/ 0>();

    for (; elemIt != elemEndIt; ++elemIt) {
        const auto& elem = *elemIt;
        auto isIt = gridView_.ibegin(elem);
        const auto& isEndIt = gridView_.iend(elem);
        for (; isIt != isEndIt; ++ isIt) {
            // store intersection, this might be costly
            const auto& intersection = *isIt;
            if (!intersection.neighbor())
                continue; // intersection is on the domain boundary

            // In the EclState TRANX[c1] is transmissibility in X+
            // direction. Ordering of compressed (c1,c2) and cartesian index
            // (gc1, gc2) is coherent (c1 < c2 <=> gc1 < gc2). This also
            // holds for the global grid. While distributing changes the
            // order of the local indices, the transmissibilities are still
            // stored at the cell with the lower global cartesian index as
            // the fieldprops are communicated by the grid.
            unsigned c1 = elemMapper.index(intersection.inside());
            unsigned c2 = elemMapper.index(intersection.outside());
            int gc1 = cartMapper_.cartesianIndex(c1);
            int gc2 = cartMapper_.cartesianIndex(c2);

            if (gc1 > gc2)
                continue; // we only need to handle each connection once, thank you.

            auto isID = isId(c1, c2);

            if (gc2 - gc1 == 1 && cartDims[0] > 1) {
                if (is_tran[0])
                    // set simulator internal transmissibilities to values from inputTranx
                     trans[0][c1] = trans_[isID];
            }
            else if (gc2 - gc1 == cartDims[0] && cartDims[1] > 1) {
                if (is_tran[1])
                    // set simulator internal transmissibilities to values from inputTrany
                     trans[1][c1] = trans_[isID];
            }
            else if (gc2 - gc1 == cartDims[0]*cartDims[1]) {
                if (is_tran[2])
                    // set simulator internal transmissibilities to values from inputTranz
                     trans[2][c1] = trans_[isID];
            }
            //else.. We don't support modification of NNC at the moment.
        }
    }

    return trans;
}

template<class Grid, class GridView, class ElementMapper, class Scalar>
void EclTransmissibility<Grid,GridView,ElementMapper,Scalar>::
resetTransmissibilityFromArrays_(const std::array<bool,3>& is_tran,
                                 const std::array<std::vector<double>,3>& trans)
{
    const auto& cartDims = cartMapper_.cartesianDimensions();
    ElementMapper elemMapper(gridView_, Dune::mcmgElementLayout());

    // compute the transmissibilities for all intersections
    auto elemIt = gridView_.template begin</*codim=*/ 0>();
    const auto& elemEndIt = gridView_.template end</*codim=*/ 0>();

    for (; elemIt != elemEndIt; ++elemIt) {
        const auto& elem = *elemIt;
        auto isIt = gridView_.ibegin(elem);
        const auto& isEndIt = gridView_.iend(elem);
        for (; isIt != isEndIt; ++ isIt) {
            // store intersection, this might be costly
            const auto& intersection = *isIt;
            if (!intersection.neighbor())
                continue; // intersection is on the domain boundary

            // In the EclState TRANX[c1] is transmissibility in X+
            // direction. Ordering of compressed (c1,c2) and cartesian index
            // (gc1, gc2) is coherent (c1 < c2 <=> gc1 < gc2). This also
            // holds for the global grid. While distributing changes the
            // order of the local indices, the transmissibilities are still
            // stored at the cell with the lower global cartesian index as
            // the fieldprops are communicated by the grid.
            unsigned c1 = elemMapper.index(intersection.inside());
            unsigned c2 = elemMapper.index(intersection.outside());
            int gc1 = cartMapper_.cartesianIndex(c1);
            int gc2 = cartMapper_.cartesianIndex(c2);
            if (gc1 > gc2)
                continue; // we only need to handle each connection once, thank you.

            auto isID = isId(c1, c2);

            if (gc2 - gc1 == 1 && cartDims[0] > 1) {
                if (is_tran[0])
                    // set simulator internal transmissibilities to values from inputTranx
                    trans_[isID] = trans[0][c1];
            }
            else if (gc2 - gc1 == cartDims[0] && cartDims[1] > 1) {
                if (is_tran[1])
                    // set simulator internal transmissibilities to values from inputTrany
                    trans_[isID] = trans[1][c1];
            }
            else if (gc2 - gc1 == cartDims[0]*cartDims[1]) {
                if (is_tran[2])
                    // set simulator internal transmissibilities to values from inputTranz
                    trans_[isID] = trans[2][c1];
            }
            //else.. We don't support modification of NNC at the moment.
        }
    }
}

template<class Grid, class GridView, class ElementMapper, class Scalar>
template<class Intersection>
void EclTransmissibility<Grid,GridView,ElementMapper,Scalar>::
computeFaceProperties(const Intersection& intersection,
                      const int,
                      const int,
                      const int,
                      const int,
                      DimVector& faceCenterInside,
                      DimVector& faceCenterOutside,
                      DimVector& faceAreaNormal,
                      /*isCpGrid=*/std::false_type) const
{
    // default implementation for DUNE grids
    const auto& geometry = intersection.geometry();
    faceCenterInside = geometry.center();
    faceCenterOutside = faceCenterInside;

    faceAreaNormal = intersection.centerUnitOuterNormal();
    faceAreaNormal *= geometry.volume();
}


template<class Grid, class GridView, class ElementMapper, class Scalar>
template<class Intersection>
void EclTransmissibility<Grid,GridView,ElementMapper,Scalar>::
computeFaceProperties(const Intersection& intersection,
                      const int insideElemIdx,
                      const int insideFaceIdx,
                      const int outsideElemIdx,
                      const int outsideFaceIdx,
                      DimVector& faceCenterInside,
                      DimVector& faceCenterOutside,
                      DimVector& faceAreaNormal,
                      /*isCpGrid=*/std::true_type) const
{
    int faceIdx = intersection.id();
    faceCenterInside = grid_.faceCenterEcl(insideElemIdx, insideFaceIdx);
    faceCenterOutside = grid_.faceCenterEcl(outsideElemIdx, outsideFaceIdx);
    faceAreaNormal = grid_.faceAreaNormalEcl(faceIdx);
}

template<class Grid, class GridView, class ElementMapper, class Scalar>
std::tuple<std::vector<NNCdata>, std::vector<NNCdata>>
EclTransmissibility<Grid,GridView,ElementMapper,Scalar>::
applyNncToGridTrans_(const std::unordered_map<std::size_t,int>& cartesianToCompressed)
{
    // First scale NNCs with EDITNNC.
    std::vector<NNCdata> unprocessedNnc;
    std::vector<NNCdata> processedNnc;
    const auto& nnc_input = eclState_.getInputNNC().input();
    if (nnc_input.empty())
        return std::make_tuple(processedNnc, unprocessedNnc);

    for (const auto& nncEntry : nnc_input) {
        auto c1 = nncEntry.cell1;
        auto c2 = nncEntry.cell2;
        auto lowIt = cartesianToCompressed.find(c1);
        auto highIt = cartesianToCompressed.find(c2);
        int low = (lowIt == cartesianToCompressed.end())? -1 : lowIt->second;
        int high = (highIt == cartesianToCompressed.end())? -1 : highIt->second;

        if (low > high)
            std::swap(low, high);

        if (low == -1 && high == -1)
            // Silently discard as it is not between active cells
            continue;

        if (low == -1 || high == -1) {
            // Discard the NNC if it is between active cell and inactive cell
            std::ostringstream sstr;
            sstr << "NNC between active and inactive cells ("
                 << low << " -> " << high << ") with globalcell is (" << c1 << "->" << c2 <<")";
            OpmLog::warning(sstr.str());
            continue;
        }

        auto candidate = trans_.find(isId(low, high));

        if (candidate == trans_.end())
            // This NNC is not resembled by the grid. Save it for later
            // processing with local cell values
            unprocessedNnc.push_back(nncEntry);
        else {
            // NNC is represented by the grid and might be a neighboring connection
            // In this case the transmissibilty is added to the value already
            // set or computed.
            candidate->second += nncEntry.trans;
            processedNnc.push_back(nncEntry);
        }
    }
    return std::make_tuple(processedNnc, unprocessedNnc);
}

template<class Grid, class GridView, class ElementMapper, class Scalar>
void EclTransmissibility<Grid,GridView,ElementMapper,Scalar>::
applyEditNncToGridTrans_(const std::unordered_map<std::size_t,int>& globalToLocal)
{
    const auto& nnc_input = eclState_.getInputNNC();
    const auto& editNnc = nnc_input.edit();
    if (editNnc.empty())
        return;
    const auto& cartDims = cartMapper_.cartesianDimensions();

    auto format_ijk = [&cartDims](std::size_t cell) -> std::string {
        auto i = cell % cartDims[0]; cell /= cartDims[0];
        auto j = cell % cartDims[1];
        auto k = cell / cartDims[1];

        return fmt::format("({},{},{})", i + 1,j + 1,k + 1);
    };

    auto print_warning = [&format_ijk, &nnc_input] (const NNCdata& nnc) {
            const auto& location = nnc_input.edit_location( nnc );
            auto warning =  fmt::format("Problem with EDITNNC keyword\n"
                                        "In {} line {} \n"
                                        "No NNC defined for connection {} -> {}", location.filename,
                                        location.lineno, format_ijk(nnc.cell1), format_ijk(nnc.cell2));
            OpmLog::warning("EDITNNC", warning);
    };

    // editNnc is supposed to only reference non-neighboring connections and not
    // neighboring connections. Use all entries for scaling if there is an NNC.
    // variable nnc incremented in loop body.
    auto nnc = editNnc.begin();
    auto end = editNnc.end();
    std::size_t warning_count = 0;
    while (nnc != end) {
        auto c1 = nnc->cell1;
        auto c2 = nnc->cell2;
        auto lowIt = globalToLocal.find(c1);
        auto highIt = globalToLocal.find(c2);

        if (lowIt == globalToLocal.end() || highIt == globalToLocal.end()) {
            print_warning(*nnc);
            ++nnc;
            warning_count++;
            continue;
        }

        auto low = lowIt->second, high = highIt->second;

        if (low > high)
            std::swap(low, high);

        auto candidate = trans_.find(isId(low, high));
        if (candidate == trans_.end()) {
            print_warning(*nnc);
            ++nnc;
            warning_count++;
        }
        else {
            // NNC exists
            while (nnc!= end && c1==nnc->cell1 && c2==nnc->cell2) {
                candidate->second *= nnc->trans;
                ++nnc;
            }
        }
    }

    if (warning_count > 0) {
        auto warning = fmt::format("Problems with EDITNNC keyword\n"
                                   "A total of {} connections not defined in grid", warning_count);
        OpmLog::warning(warning);
    }
}

template<class Grid, class GridView, class ElementMapper, class Scalar>
void EclTransmissibility<Grid,GridView,ElementMapper,Scalar>::
computeHalfTrans_(Scalar& halfTrans,
                  const DimVector& areaNormal,
                  int faceIdx, // in the reference element that contains the intersection
                  const DimVector& distance,
                  const DimMatrix& perm) const
{
    assert(faceIdx >= 0);
    unsigned dimIdx = faceIdx/2;
    assert(dimIdx < dimWorld);
    halfTrans = perm[dimIdx][dimIdx];

    Scalar val = 0;
    for (unsigned i = 0; i < areaNormal.size(); ++i)
        val += areaNormal[i]*distance[i];

    halfTrans *= std::abs(val);
    halfTrans /= distance.two_norm2();
}

template<class Grid, class GridView, class ElementMapper, class Scalar>
void EclTransmissibility<Grid,GridView,ElementMapper,Scalar>::
computeHalfDiffusivity_(Scalar& halfDiff,
                        const DimVector& areaNormal,
                        const DimVector& distance,
                        const Scalar& poro) const
{
    halfDiff = poro;
    Scalar val = 0;
    for (unsigned i = 0; i < areaNormal.size(); ++i)
        val += areaNormal[i]*distance[i];

    halfDiff *= std::abs(val);
    halfDiff /= distance.two_norm2();
}

template<class Grid, class GridView, class ElementMapper, class Scalar>
typename EclTransmissibility<Grid,GridView,ElementMapper,Scalar>::DimVector
EclTransmissibility<Grid,GridView,ElementMapper,Scalar>::
distanceVector_(const DimVector& center,
                int faceIdx, // in the reference element that contains the intersection
                unsigned elemIdx,
                const std::array<std::vector<DimVector>, dimWorld>& axisCentroids) const
{
    assert(faceIdx >= 0);
    unsigned dimIdx = faceIdx/2;
    assert(dimIdx < dimWorld);
    DimVector x = center;
    x -= axisCentroids[dimIdx][elemIdx];

    return x;
}

template<class Grid, class GridView, class ElementMapper, class Scalar>
void EclTransmissibility<Grid,GridView,ElementMapper,Scalar>::
applyMultipliers_(Scalar& trans,
                  unsigned faceIdx,
                  unsigned cartElemIdx,
                  const TransMult& transMult) const
{
    // apply multiplyer for the transmissibility of the face. (the
    // face index is the index of the reference-element face which
    // contains the intersection of interest.)
    switch (faceIdx) {
    case 0: // left
        trans *= transMult.getMultiplier(cartElemIdx, FaceDir::XMinus);
        break;
    case 1: // right
        trans *= transMult.getMultiplier(cartElemIdx, FaceDir::XPlus);
        break;

    case 2: // front
        trans *= transMult.getMultiplier(cartElemIdx, FaceDir::YMinus);
        break;
    case 3: // back
        trans *= transMult.getMultiplier(cartElemIdx, FaceDir::YPlus);
        break;

    case 4: // bottom
        trans *= transMult.getMultiplier(cartElemIdx, FaceDir::ZMinus);
        break;
    case 5: // top
        trans *= transMult.getMultiplier(cartElemIdx, FaceDir::ZPlus);
        break;
    }
}

template<class Grid, class GridView, class ElementMapper, class Scalar>
void EclTransmissibility<Grid,GridView,ElementMapper,Scalar>::
applyNtg_(Scalar& trans,
          unsigned faceIdx,
          unsigned elemIdx,
          const std::vector<double>& ntg) const
{
    // apply multiplyer for the transmissibility of the face. (the
    // face index is the index of the reference-element face which
    // contains the intersection of interest.)
    switch (faceIdx) {
    case 0: // left
        trans *= ntg[elemIdx];
        break;
    case 1: // right
        trans *= ntg[elemIdx];
        break;

    case 2: // front
        trans *= ntg[elemIdx];
        break;
    case 3: // back
        trans *= ntg[elemIdx];
        break;

        // NTG does not apply to top and bottom faces
    }
}

#ifdef HAVE_DUNE_FEM
template class EclTransmissibility<Dune::CpGrid,
                                   Dune::GridView<Dune::Fem::GridPart2GridViewTraits<Dune::Fem::AdaptiveLeafGridPart<Dune::CpGrid, Dune::PartitionIteratorType(4), false>>>,
                                   Dune::MultipleCodimMultipleGeomTypeMapper<Dune::GridView<Dune::Fem::GridPart2GridViewTraits<Dune::Fem::AdaptiveLeafGridPart<Dune::CpGrid, Dune::PartitionIteratorType(4), false>>>>,
                                   double>;
template class EclTransmissibility<Dune::CpGrid,
                                   Dune::Fem::GridPart2GridViewImpl<
                                       Dune::Fem::AdaptiveLeafGridPart<
                                           Dune::CpGrid,
                                           Dune::PartitionIteratorType(4),
                                           false> >,
                                   Dune::MultipleCodimMultipleGeomTypeMapper<
                                       Dune::Fem::GridPart2GridViewImpl<
                                           Dune::Fem::AdaptiveLeafGridPart<
                                               Dune::CpGrid,
                                               Dune::PartitionIteratorType(4),
                                               false> > >,
                                   double>;
#else
template class EclTransmissibility<Dune::CpGrid,
                                   Dune::GridView<Dune::DefaultLeafGridViewTraits<Dune::CpGrid>>,
                                   Dune::MultipleCodimMultipleGeomTypeMapper<Dune::GridView<Dune::DefaultLeafGridViewTraits<Dune::CpGrid>>>,
                                   double>;
#endif

template class EclTransmissibility<Dune::PolyhedralGrid<3,3,double>,
                                   Dune::GridView<Dune::PolyhedralGridViewTraits<3, 3, double, Dune::PartitionIteratorType(4)>>,
                                   Dune::MultipleCodimMultipleGeomTypeMapper<Dune::GridView<Dune::PolyhedralGridViewTraits<3,3,double,Dune::PartitionIteratorType(4)>>>,
                                   double>;

} // namespace Opm
