/*
 * (C) Copyright 2013 ECMWF.
 *
 * This software is licensed under the terms of the Apache Licence Version 2.0
 * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
 * In applying this licence, ECMWF does not waive the privileges and immunities
 * granted to it by virtue of its status as an intergovernmental organisation
 * nor does it submit to any jurisdiction.
 */
#include <algorithm>
#include <cmath>
#include <iomanip>
#include <limits>
#include <memory>
#include <numeric>
#include <vector>

#include "eckit/log/ProgressTimer.h"
#include "eckit/types/FloatCompare.h"
#include "eckit/utils/Hash.h"

#include "atlas/array.h"
#include "atlas/array/ArrayView.h"
#include "atlas/array/MakeView.h"
#include "atlas/field/Field.h"
#include "atlas/grid/Distribution.h"
#include "atlas/grid/Partitioner.h"
#include "atlas/grid/StructuredGrid.h"
#include "atlas/library/config.h"
#include "atlas/mesh/ElementType.h"
#include "atlas/mesh/Elements.h"
#include "atlas/mesh/HybridElements.h"
#include "atlas/mesh/Mesh.h"
#include "atlas/mesh/Nodes.h"
#include "atlas/meshgenerator/detail/MeshGeneratorFactory.h"
#include "atlas/meshgenerator/detail/StructuredMeshGenerator.h"
#include "atlas/parallel/mpi/mpi.h"
#include "atlas/runtime/Log.h"
#include "atlas/runtime/Trace.h"
#include "atlas/util/CoordinateEnums.h"

#define DEBUG_OUTPUT 0

using namespace atlas::array;
using atlas::Mesh;
using Topology = atlas::mesh::Nodes::Topology;

namespace atlas {
namespace meshgenerator {

namespace {
static double to_rad = M_PI / 180.;
static double to_deg = 180. * M_1_PI;
}  // namespace

struct Region {
    int north {-1};
    int south {-1};
    std::unique_ptr<array::Array> elems;
    int ntriags {0};
    int nquads {0};
    int nnodes {0};
    std::vector<idx_t> lat_begin;
    std::vector<idx_t> lat_end;
    std::vector<idx_t> nb_lat_elems;
};

StructuredMeshGenerator::StructuredMeshGenerator(const eckit::Parametrisation& p) {
    configure_defaults();

    bool include_pole;
    if (p.get("include_pole", include_pole)) {
        options.set("include_pole", include_pole);
    }

    bool patch_pole;
    if (p.get("patch_pole", patch_pole)) {
        options.set("patch_pole", patch_pole);
    }

    bool patch_quads;
    if (p.get("patch_quads", patch_quads)) {
        options.set("patch_quads", patch_quads);
    }

    bool unique_pole;
    if (p.get("unique_pole", unique_pole)) {
        options.set("unique_pole", unique_pole);
    }

    bool force_include_pole;
    if (p.get("force_include_north_pole", force_include_pole)) {
        options.set("force_include_north_pole", force_include_pole);
    }
    if (p.get("force_include_south_pole", force_include_pole)) {
        options.set("force_include_south_pole", force_include_pole);
    }

    bool three_dimensional;
    if (p.get("three_dimensional", three_dimensional) || p.get("3d", three_dimensional)) {
        options.set("three_dimensional", three_dimensional);
    }

    std::string mpi_comm_name = mpi::comm().name();
    p.get("mpi_comm", mpi_comm_name);
    options.set("mpi_comm",mpi_comm_name);
    auto& comm = mpi::comm(mpi_comm_name);

    // This option sets number of parts the mesh will be split in
    options.set("nb_parts", comm.size());

    // This option sets the part that will be generated
    options.set("part", comm.rank());

    size_t nb_parts;
    if (p.get("nb_parts", nb_parts)) {
        options.set("nb_parts", nb_parts);
    }

    size_t part;
    if (p.get("part", part)) {
        options.set("part", part);
    }

    double angle;
    if (p.get("angle", angle)) {
        options.set("angle", angle);
    }

    bool triangulate;
    if (p.get("triangulate", triangulate)) {
        options.set("triangulate", triangulate);
    }

    bool ghost_at_end;
    if (p.get("ghost_at_end", ghost_at_end)) {
        options.set("ghost_at_end", ghost_at_end);
    }

    std::string partitioner;
    if (p.get("partitioner", partitioner)) {
        if (not grid::Partitioner::exists(partitioner)) {
            Log::warning() << "Atlas does not have support for partitioner " << partitioner << ". "
                           << "Defaulting to use partitioner EqualRegions" << std::endl;
            partitioner = "equal_regions";
        }
    }
    else {
        partitioner = "equal_regions";
    }
    options.set("partitioner", partitioner);
}

void StructuredMeshGenerator::configure_defaults() {
    // This option creates a point at the pole when true
    options.set("include_pole", false);

    // This option sets the part that will be generated
    options.set("patch_pole", true);

    // This option disregards multiple poles in grid (e.g. lonlat up to poles) and
    // connects elements
    // to the first node only. Note this option will only be looked at in case
    // other option
    // "three_dimensional"==true
    options.set("unique_pole", true);

    // This option creates elements that connect east to west at greenwich
    // meridian
    // when true, instead of creating periodic ghost-points at east boundary when
    // false
    options.set("three_dimensional", false);

    // Experimental option. This is what makes tripolar grid patched with quads
    options.set("patch_quads", false);

    // This option sets the maximum angle deviation for a quadrilateral element
    // angle = 30  -->  minimises number of triangles
    // angle = 0   -->  maximises number of triangles
    options.set("angle", 0.);

    options.set("triangulate", false);

    options.set("ghost_at_end", true);
}

void StructuredMeshGenerator::generate(const Grid& grid, Mesh& mesh) const {
    ATLAS_TRACE();
    ATLAS_ASSERT(!mesh.generated());

    const StructuredGrid rg = StructuredGrid(grid);
    if (!rg) {
        throw_Exception("Structured can only work with a Structured grid", Here());
    }

    idx_t nb_parts = options.getInt("nb_parts");

    std::string partitioner_type = "equal_regions";
    options.get("partitioner", partitioner_type);

    if (partitioner_type == "ectrans") {
        if (rg.ny() % 2 == 1) {
            partitioner_type = "equal_regions";  // Odd number of latitudes
        }
        if (nb_parts != mpi::size()) {
            partitioner_type = "equal_regions";
        }
    }
    if (nb_parts == 1) {
        partitioner_type = "serial";
    }
    mpi::push(options.getString("mpi_comm"));
    grid::Partitioner partitioner(partitioner_type, nb_parts);
    grid::Distribution distribution(partitioner.partition(grid));
    mpi::pop();
    generate(grid, distribution, mesh);
}

void StructuredMeshGenerator::hash(eckit::Hash& h) const {
    h.add("Structured");
    options.hash(h);
}

void StructuredMeshGenerator::generate(const Grid& grid, const grid::Distribution& distribution, Mesh& mesh) const {
    ATLAS_TRACE("structuredmeshgenerator(grid,dist,mesh)");
    Log::debug() << "StructuredMeshGenerator generating mesh from " << grid.name() << std::endl;

    const StructuredGrid rg = StructuredGrid(grid);
    if (!rg) {
        throw_Exception("Grid could not be cast to a Structured", Here());
    }

    ATLAS_ASSERT(!mesh.generated());

    if (grid.size() != idx_t(distribution.size())) {
        std::stringstream msg;
        msg << "Number of points in grid (" << grid.size()
            << ") different from "
               "number of points in grid distribution ("
            << distribution.size() << ")";
        throw_AssertionFailed(msg.str(), Here());
    }

    idx_t mypart = options.getInt("part");

// show distribution
#if DEBUG_OUTPUT
    int inode                       = 0;
    const atlas::vector<int>& parts = distribution;
    Log::info() << "Partition : " << std::endl;
    for (size_t ilat = 0; ilat < rg.ny(); ilat++) {
        for (size_t ilon = 0; ilon < rg.nx(ilat); ilon++) {
            Log::info() << std::setw(3) << distribution.partition(inode);
            inode++;
        }
        Log::info() << std::endl;
    }
#endif

    // clone some grid properties
    setGrid(mesh, rg, distribution);

    Region region;
    generate_region(rg, distribution, mypart, region);


    mesh.metadata().set("nb_parts",options.getInt("nb_parts"));
    mesh.metadata().set("part",options.getInt("part"));
    mesh.metadata().set("mpi_comm",options.getString("mpi_comm"));
    generate_mesh(rg, distribution, region, mesh);
}

void StructuredMeshGenerator::generate_region(const StructuredGrid& rg, const grid::Distribution& distribution,
                                              int mypart, Region& region) const {
    ATLAS_TRACE();

    double max_angle        = options.getDouble("angle");
    bool triangulate_quads  = options.getBool("triangulate");
    bool three_dimensional  = options.getBool("three_dimensional");
    bool has_north_pole     = eckit::types::is_approximately_equal(rg.y().front(), 90.);
    bool has_south_pole     = eckit::types::is_approximately_equal(rg.y().back(), -90.);
    bool unique_pole        = options.getBool("unique_pole") && three_dimensional && has_north_pole && has_south_pole;
    bool periodic_east_west = rg.periodic();

    region.lat_begin.resize(rg.ny(), -1);
    region.lat_end.resize(rg.ny(), -1);
    region.nb_lat_elems.resize(rg.ny(), 0);

    idx_t lat_north = -1;
    idx_t lat_south = -1;

    if (distribution.nb_partitions() == 1) {
        lat_north = 0;
        lat_south = rg.ny() - 1;
    }
    else {
        ATLAS_TRACE_SCOPE("compute bounds") {
            // This part does not scale
            /*
Find min and max latitudes used by this part.
*/

            size_t num_threads = atlas_omp_get_max_threads();
            if (num_threads == 1) {
                int n = 0;
                for (idx_t jlat = 0; jlat < rg.ny(); ++jlat) {
                    for (idx_t jlon = 0; jlon < rg.nx(jlat); ++jlon) {
                        if (distribution.partition(n) == mypart) {
                            lat_north = jlat;
                            goto end_north;
                        }
                        ++n;
                    }
                }
            end_north:;

                n = rg.size() - 1;
                for (idx_t jlat = rg.ny() - 1; jlat >= 0; --jlat) {
                    for (idx_t jlon = rg.nx(jlat) - 1; jlon >= 0; --jlon) {
                        if (distribution.partition(n) == mypart) {
                            lat_south = jlat;
                            goto end_south;
                        }
                        --n;
                    }
                }
            end_south:;
            }
            else {
                std::vector<idx_t> thread_reduce_lat_north(num_threads, std::numeric_limits<idx_t>::max());
                std::vector<idx_t> thread_reduce_lat_south(num_threads, std::numeric_limits<idx_t>::min());

                atlas_omp_parallel {
                    const idx_t thread_num = atlas_omp_get_thread_num();
                    const idx_t jbegin = static_cast<int>(size_t(thread_num) * size_t(rg.ny()) / size_t(num_threads));
                    const idx_t jend = static_cast<int>(size_t(thread_num + 1) * size_t(rg.ny()) / size_t(num_threads));

                    auto& thread_lat_north = thread_reduce_lat_north[thread_num];
                    auto& thread_lat_south = thread_reduce_lat_south[thread_num];

                    for (idx_t jlat = jbegin; jlat < jend && thread_lat_north > jend; ++jlat) {
                        int n = rg.index(0, jlat);
                        for (idx_t jlon = 0; jlon < rg.nx(jlat); ++jlon, ++n) {
                            if (distribution.partition(n) == mypart) {
                                thread_lat_north = jlat;
                                break;
                            }
                        }
                    }

                    for (idx_t jlat = jend - 1; jlat >= jbegin && thread_lat_south < jbegin; --jlat) {
                        int n = rg.index(0, jlat);
                        for (idx_t jlon = 0; jlon < rg.nx(jlat); ++jlon, ++n) {
                            if (distribution.partition(n) == mypart) {
                                thread_lat_south = jlat;
                                break;
                            }
                        }
                    }
                }
                lat_north = *std::min_element(thread_reduce_lat_north.begin(), thread_reduce_lat_north.end());
                lat_south = *std::max_element(thread_reduce_lat_south.begin(), thread_reduce_lat_south.end());
            }
        }
    }

    if (lat_north == -1 && lat_south == -1 ) {
        return;
    }

    ATLAS_ASSERT(lat_north >= 0);
    ATLAS_ASSERT(lat_south < rg.ny() );

    std::vector<idx_t> offset(rg.ny(), 0);

    int n = 0;
    for (idx_t jlat = 0; jlat < rg.ny(); ++jlat) {
        offset.at(jlat) = n;
        n += rg.nx(jlat);
    }

    /*
We need to connect to next region
*/
    if (lat_north - 1 >= 0 && rg.nx(lat_north - 1) > 0) {
        --lat_north;
    }
    if (idx_t(lat_south + 1) <= rg.ny() - 1 && rg.nx(lat_south + 1) > 0) {
        ++lat_south;
    }

    region.north = lat_north;
    region.south = lat_south;

    array::ArrayShape shape = array::make_shape(region.south - region.north, 4 * rg.nxmax(), 4);

    region.elems.reset(array::Array::create<int>(shape));

    int nelems     = 0;
    region.nquads  = 0;
    region.ntriags = 0;

    array::ArrayView<int, 3> elemview = array::make_view<int, 3>(*region.elems);
    elemview.assign(-1);

    ATLAS_TRACE_SCOPE("generate elements") {
        eckit::Channel blackhole;

        eckit::ProgressTimer progress("Generating elements for " + std::to_string(lat_south - lat_north) + " rows",
                                      lat_south - lat_north, " row", 5.,
                                      lat_south - lat_north > 1280 ? Log::trace() : blackhole);

        for (idx_t jlat = lat_north; jlat < lat_south; ++jlat, ++progress) {
            //    std::stringstream filename; filename << "/tmp/debug/"<<jlat;

            idx_t ilat, latN, latS;
            idx_t ipN1, ipN2, ipS1, ipS2;
            double xN1, xN2, yN, xS1, xS2, yS;
            double dN1S2, dS1N2;  // dN2S2;
            bool try_make_triangle_up, try_make_triangle_down, try_make_quad;
            bool add_triag, add_quad;

            ilat = jlat - region.north;

            auto lat_elems_view = elemview.slice(ilat, Range::all(), Range::all());

            latN = jlat;
            latS = jlat + 1;
            yN   = rg.y(latN);
            yS   = rg.y(latS);

            idx_t beginN, beginS, endN, endS;

            beginN = 0;
            endN   = rg.nx(latN) - (periodic_east_west ? 0 : 1);
            if (eckit::types::is_approximately_equal(yN, 90.) && unique_pole) {
                endN = beginN;
            }

            beginS = 0;
            endS   = rg.nx(latS) - (periodic_east_west ? 0 : 1);
            if (eckit::types::is_approximately_equal(yS, -90.) && unique_pole) {
                endS = beginS;
            }

            ipN1 = beginN;
            ipS1 = beginS;
            ipN2 = std::min(ipN1 + 1, endN);
            ipS2 = std::min(ipS1 + 1, endS);

            idx_t jelem = 0;
            int pE      = distribution.partition(offset.at(latN));

#if DEBUG_OUTPUT
            Log::info() << "=================\n";
            Log::info() << "latN, latS : " << latN << ", " << latS << '\n';
#endif

            while (true) {
                if (ipN1 == endN && ipS1 == endS) {
                    // ATLAS_DEBUG("ipN1 == endN && ipS1 == endS");
                    break;
                }

#if DEBUG_OUTPUT
                Log::info() << "-------\n";
#endif

                // ATLAS_ASSERT(offset.at(latN)+ipN1 < parts.size());
                // ATLAS_ASSERT(offset.at(latS)+ipS1 < parts.size());

                int pN1, pS1, pN2, pS2;
                if (ipN1 != rg.nx(latN)) {
                    pN1 = distribution.partition(offset.at(latN) + ipN1);
                }
                else {
                    pN1 = distribution.partition(offset.at(latN));
                }
                if (ipS1 != rg.nx(latS)) {
                    pS1 = distribution.partition(offset.at(latS) + ipS1);
                }
                else {
                    pS1 = distribution.partition(offset.at(latS));
                }

                if (ipN2 == rg.nx(latN)) {
                    pN2 = distribution.partition(offset.at(latN));
                }
                else {
                    pN2 = distribution.partition(offset.at(latN) + ipN2);
                }
                if (ipS2 == rg.nx(latS)) {
                    pS2 = distribution.partition(offset.at(latS));
                }
                else {
                    pS2 = distribution.partition(offset.at(latS) + ipS2);
                }

                // Log::info()  << ipN1 << "("<<pN1<<") " << ipN2 <<"("<<pN2<<")" <<  std::endl;
                // Log::info()  << ipS1 << "("<<pS2<<") " << ipS2 <<"("<<pS2<<")" <<  std::endl;

#if DEBUG_OUTPUT
                Log::info() << ipN1 << "(" << pN1 << ") " << ipN2 << "(" << pN2 << ")" << std::endl;
                Log::info() << ipS1 << "(" << pS2 << ") " << ipS2 << "(" << pS2 << ")" << std::endl;
#endif

                xN1 = rg.x(ipN1, latN) * to_rad;
                xN2 = rg.x(ipN2, latN) * to_rad;
                xS1 = rg.x(ipS1, latS) * to_rad;
                xS2 = rg.x(ipS2, latS) * to_rad;

#if DEBUG_OUTPUT
                Log::info() << "-------\n";
#endif
                // Log::info()  << "  access  " <<
                // region.elems.stride(0)*(jlat-region.north) +
                // region.elems.stride(1)*jelem + 5 << std::endl;
                //      Log::info()  << ipN1 << "("<< xN1 << ")  " << ipN2 <<  "("<< xN2
                //      << ")  " << std::endl;
                //      Log::info()  << ipS1 << "("<< xS1 << ")  " << ipS2 <<  "("<< xS2
                //      << ")  " << std::endl;
                try_make_triangle_up   = false;
                try_make_triangle_down = false;
                try_make_quad          = false;

                // ------------------------------------------------
                // START RULES
                // ------------------------------------------------

                const double dxN    = std::abs(xN2 - xN1);
                const double dxS    = std::abs(xS2 - xS1);
                const double dx     = std::min(dxN, dxS);
                const double alpha1 = (dx == 0. ? 0. : std::atan2((xN1 - xS1), dx) * to_deg);
                const double alpha2 = (dx == 0. ? 0. : std::atan2((xN2 - xS2), dx) * to_deg);
                if (std::abs(alpha1) <= max_angle && std::abs(alpha2) <= max_angle) {
                    if (triangulate_quads) {
                        if (false)  // std::abs(alpha1) < 1 && std::abs(alpha2) < 1)
                        {
                            try_make_triangle_up   = (jlat + ipN1) % 2;
                            try_make_triangle_down = (jlat + ipN1 + 1) % 2;
                        }
                        else {
                            dN1S2 = std::abs(xN1 - xS2);
                            dS1N2 = std::abs(xS1 - xN2);
                            // dN2S2 = std::abs(xN2-xS2);
                            // Log::info()  << "  dN1S2 " << dN1S2 << "   dS1N2 " << dS1N2 << "
                            // dN2S2 " << dN2S2 << std::endl;
                            if (dN1S2 == dS1N2) {
                                try_make_triangle_up   = (jlat + ipN1) % 2;
                                try_make_triangle_down = (jlat + ipN1 + 1) % 2;
                            }
                            else if (dN1S2 < dS1N2) {
                                if (ipS1 != ipS2) {
                                    try_make_triangle_up = true;
                                }
                                else {
                                    try_make_triangle_down = true;
                                }
                            }
                            else if (dN1S2 > dS1N2) {
                                if (ipN1 != ipN2) {
                                    try_make_triangle_down = true;
                                }
                                else {
                                    try_make_triangle_up = true;
                                }
                            }
                            else {
                                throw_Exception("Should not be here", Here());
                            }
                        }
                    }
                    else {
                        if (ipN1 == ipN2) {
                            try_make_triangle_up = true;
                        }
                        else if (ipS1 == ipS2) {
                            try_make_triangle_down = true;
                        }
                        else {
                            try_make_quad = true;
                        }

                        //          try_make_quad          = true;
                    }
                }
                else {
                    dN1S2 = std::abs(xN1 - xS2);
                    dS1N2 = std::abs(xS1 - xN2);
                    // dN2S2 = std::abs(xN2-xS2);
                    // Log::info()  << "  dN1S2 " << dN1S2 << "   dS1N2 " << dS1N2 << "
                    // dN2S2 " << dN2S2 << std::endl;
                    if ((dN1S2 <= dS1N2) && (ipS1 != ipS2)) {
                        try_make_triangle_up = true;
                    }
                    else if ((dN1S2 >= dS1N2) && (ipN1 != ipN2)) {
                        try_make_triangle_down = true;
                    }
                    else {
                        if (ipN1 == ipN2) {
                            try_make_triangle_up = true;
                        }
                        else if (ipS1 == ipS2) {
                            try_make_triangle_down = true;
                        }
                        else {
                            ATLAS_DEBUG_VAR(dN1S2);
                            ATLAS_DEBUG_VAR(dS1N2);
                            ATLAS_DEBUG_VAR(jlat);
                            Log::info() << ipN1 << "(" << xN1 << ")  " << ipN2 << "(" << xN2 << ")  " << std::endl;
                            Log::info() << ipS1 << "(" << xS1 << ")  " << ipS2 << "(" << xS2 << ")  " << std::endl;
                            throw_Exception("Should not try to make a quadrilateral!", Here());
                        }
                    }
                }

                ATLAS_ASSERT( (ipN1!=ipN2) || (ipS1 != ipS2) );
                if( ipN1 == ipN2 ) {
                    try_make_triangle_up = true;
                    try_make_triangle_down = false;
                    try_make_quad = false;
                }
                if( ipS1 == ipS2 ) {
                    try_make_triangle_up = false;
                    try_make_triangle_down = true;
                    try_make_quad = false;
                }

                // ------------------------------------------------
                // END RULES
                // ------------------------------------------------

#if DEBUG_OUTPUT
                ATLAS_DEBUG_VAR(jelem);
#endif

                auto elem = lat_elems_view.slice(jelem, Range::all());

                if (try_make_quad) {
// add quadrilateral
#if DEBUG_OUTPUT
                    Log::info() << "          " << ipN1 << "  " << ipN2 << '\n';
                    Log::info() << "          " << ipS1 << "  " << ipS2 << '\n';
#endif
                    elem(0)  = ipN1;
                    elem(1)  = ipS1;
                    elem(2)  = ipS2;
                    elem(3)  = ipN2;
                    add_quad = false;
                    std::array<int, 4> np{pN1, pN2, pS1, pS2};
                    std::array<int, 4> pcnts;
                    for (int j = 0; j < 4; ++j) {
                        pcnts[j] = static_cast<int>(std::count(np.begin(), np.end(), np[j]));
                    }
                    if (pcnts[0] > 2) {  // 3 or more of pN1
                        pE = pN1;
                        if (latS == rg.ny() - 1) {
                            pE = pS1;
                        }
                    }
                    else if (pcnts[2] > 2) {  // 3 or more of pS1
                        pE = pS1;
                        if (latN == 0) {
                            pE = pN1;
                        }
                    }
                    else {
                        std::array<int, 4>::iterator p_max = std::max_element(pcnts.begin(), pcnts.end());
                        if (*p_max > 2) {  // 3 or 4 points belong to same part
                            pE = np[std::distance(np.begin(), p_max)];
                        }
                        else {  // 3 or 4 points don't belong to mypart
                            pE = pN1;
                            if (latS == rg.ny() - 1) {
                                pE = pS1;
                            }
                        }
                    }
                    add_quad = (pE == mypart);
                    if (add_quad) {
                        ++region.nquads;
                        ++jelem;
                        ++nelems;

                        if (region.lat_begin.at(latN) == -1) {
                            region.lat_begin.at(latN) = ipN1;
                        }
                        if (region.lat_begin.at(latS) == -1) {
                            region.lat_begin.at(latS) = ipS1;
                        }
                        region.lat_begin.at(latN) = std::min<int>(region.lat_begin.at(latN), ipN1);
                        region.lat_begin.at(latS) = std::min<int>(region.lat_begin.at(latS), ipS1);
                        region.lat_end.at(latN)   = std::max<int>(region.lat_end.at(latN), ipN2);
                        region.lat_end.at(latS)   = std::max<int>(region.lat_end.at(latS), ipS2);
                    }
                    else {
#if DEBUG_OUTPUT
                        Log::info() << "Quad belongs to other partition" << std::endl;
#endif
                    }
                    ipN1 = ipN2;
                    ipS1 = ipS2;
                }
                else if (try_make_triangle_down)  // make triangle down
                {
// triangle without ip3
#if DEBUG_OUTPUT
                    Log::info() << "v          " << ipN1 << "  " << ipN2 << '\n';
                    Log::info() << "           " << ipS1 << '\n';
#endif
                    elem(0) = ipN1;
                    elem(1) = ipS1;
                    elem(2) = -1;
                    elem(3) = ipN2;

                    pE = pN1;
                    if (latS == rg.ny() - 1) {
                        pE = pS1;
                    }
                    add_triag = (mypart == pE);

                    if (add_triag) {
                        ATLAS_ASSERT(ipN1 != ipN2, "Faulty triangle with latN = "+std::to_string(latN)+"("+std::to_string(rg.y(latN))+")");
                        ++region.ntriags;
                        ++jelem;
                        ++nelems;

                        if (region.lat_begin.at(latN) == -1) {
                            region.lat_begin.at(latN) = ipN1;
                        }
                        if (region.lat_begin.at(latS) == -1) {
                            region.lat_begin.at(latS) = ipS1;
                        }
                        region.lat_begin.at(latN) = std::min<int>(region.lat_begin.at(latN), ipN1);
                        region.lat_begin.at(latS) = std::min<int>(region.lat_begin.at(latS), ipS1);
                        region.lat_end.at(latN)   = std::max<int>(region.lat_end.at(latN), ipN2);
                        region.lat_end.at(latS)   = std::max<int>(region.lat_end.at(latS), ipS1);
                    }
                    else {
#if DEBUG_OUTPUT
                        Log::info() << "Downward Triag belongs to other partition" << std::endl;
#endif
                    }
                    ipN1 = ipN2;
                    // and ipS1=ipS1;
                }
                else if (try_make_triangle_up)  // make triangle up
                {
// triangle without ip4
#if DEBUG_OUTPUT
                    Log::info() << "^          " << ipN1 << " (" << pN1 << ")" << '\n';
                    Log::info() << "           " << ipS1 << " (" << pS1 << ")"
                                << "  " << ipS2 << " (" << pS2 << ")" << '\n';
#endif
                    elem(0) = ipN1;
                    elem(1) = ipS1;
                    elem(2) = ipS2;
                    elem(3) = -1;

                    if (pS1 == pE && pN1 != pE) {
                        if (xN1 < 0.5 * (xS1 + xS2)) {
                            pE = pN1;
                        }  // else pE of previous element
                    }
                    else {
                        pE = pN1;
                    }
                    if (ipN1 == rg.nx(latN)) {
                        pE = pS1;
                    }
                    if (latS == rg.ny() - 1) {
                        pE = pS1;
                    }

                    add_triag = (mypart == pE);

                    if (add_triag) {
                        ++region.ntriags;
                        ++jelem;
                        ++nelems;

                        if (region.lat_begin.at(latN) == -1) {
                            region.lat_begin.at(latN) = ipN1;
                        }
                        if (region.lat_begin.at(latS) == -1) {
                            region.lat_begin.at(latS) = ipS1;
                        }
                        region.lat_begin.at(latN) = std::min(region.lat_begin.at(latN), ipN1);
                        region.lat_begin.at(latS) = std::min(region.lat_begin.at(latS), ipS1);
                        region.lat_end.at(latN)   = std::max(region.lat_end.at(latN), ipN1);
                        region.lat_end.at(latS)   = std::max(region.lat_end.at(latS), ipS2);
                    }
                    else {
#if DEBUG_OUTPUT
                        Log::info() << "Upward Triag belongs to other partition" << std::endl;
#endif
                    }
                    ipS1 = ipS2;
                    // and ipN1=ipN1;
                }
                else {
                    throw_Exception("Could not detect which element to create", Here());
                }
                ipN2 = std::min(endN, ipN1 + 1);
                ipS2 = std::min(endS, ipS1 + 1);
            }
            region.nb_lat_elems.at(jlat) = jelem;
#if DEBUG_OUTPUT
            ATLAS_DEBUG_VAR(region.nb_lat_elems.at(jlat));
#endif
            if (region.nb_lat_elems.at(jlat) == 0 && latN == region.north) {
                ++region.north;
            }
            if (region.nb_lat_elems.at(jlat) == 0 && latS == region.south) {
                --region.south;
            }
            //    region.lat_end.at(latN) = std::min(region.lat_end.at(latN),
            //    int(rg.nx(latN)-1));
            //    region.lat_end.at(latS) = std::min(region.lat_end.at(latS),
            //    int(rg.nx(latS)-1));
            if (yN == 90 && unique_pole) {
                region.lat_end.at(latN) = rg.nx(latN) - 1;
            }
            if (yS == -90 && unique_pole) {
                region.lat_end.at(latS) = rg.nx(latS) - 1;
            }

            if (region.nb_lat_elems.at(jlat) > 0) {
                region.lat_end.at(latN) = std::max(region.lat_end.at(latN), region.lat_begin.at(latN));
                region.lat_end.at(latS) = std::max(region.lat_end.at(latS), region.lat_begin.at(latS));
            }
        }  // for jlat
    }

    //  Log::info()  << "nb_triags = " << region.ntriags << std::endl;
    //  Log::info()  << "nb_quads = " << region.nquads << std::endl;
    //  Log::info()  << "nb_elems = " << nelems << std::endl;

    int nb_region_nodes = 0;
    for (int jlat = region.north; jlat <= region.south; ++jlat) {
        n                         = offset.at(jlat);
        region.lat_begin.at(jlat) = std::max<idx_t>(0, region.lat_begin.at(jlat));
        for (idx_t jlon = 0; jlon < rg.nx(jlat); ++jlon) {
            if (distribution.partition(n) == mypart) {
                region.lat_begin.at(jlat) = std::min(region.lat_begin.at(jlat), jlon);
                region.lat_end.at(jlat)   = std::max(region.lat_end.at(jlat), jlon);
            }
            ++n;
        }
        nb_region_nodes += region.lat_end.at(jlat) - region.lat_begin.at(jlat) + 1;

        // Count extra periodic node
        // if( periodic_east_west && size_t(region.lat_end.at(jlat)) == rg.nx(jlat)
        // - 1) ++nb_region_nodes;
    }

    region.nnodes = nb_region_nodes;
    if (region.nnodes == 0) {
        throw_Exception(
            "Trying to generate mesh with too many partitions. Reduce "
            "the number of partitions.",
            Here());
    }
#if DEBUG_OUTPUT
    ATLAS_DEBUG("End of generate_region()");
#endif
}

namespace {
struct GhostNode {
    GhostNode(int _jlat, int _jlon, int _jnode) {
        jlat  = _jlat;
        jlon  = _jlon;
        jnode = _jnode;
    }
    int jlat;
    int jlon;
    int jnode;
};
}  // namespace

void StructuredMeshGenerator::generate_mesh(const StructuredGrid& rg, const grid::Distribution& distribution,
                                            const Region& region, Mesh& mesh) const {
    ATLAS_TRACE();

    ATLAS_ASSERT(!mesh.generated());

    int mypart = options.getInt("part");
    int nparts = options.getInt("nb_parts");
    int n, l;
    const int y_numbering = (rg.y().front() < rg.y().back()) ? +1 : -1;
    double y_north        = y_numbering < 0 ? rg.y().front() : rg.y().back();
    double y_south        = y_numbering < 0 ? rg.y().back() : rg.y().front();
    idx_t nx_north        = y_numbering < 0 ? rg.nx().front() : rg.nx().back();
    idx_t nx_south        = y_numbering < 0 ? rg.nx().back() : rg.nx().front();

    std::vector<int> part_north(nx_north);
    std::vector<int> part_south(nx_south);
    if (y_numbering < 0) {
        distribution.partition(0, nx_north, part_north);
        distribution.partition(rg.size() - nx_south, rg.size(), part_south);
    }
    else {
        distribution.partition(0, nx_south, part_south);
        distribution.partition(rg.size() - nx_north, rg.size(), part_north);
    }
    bool mypart_at_north = std::any_of(part_north.begin(), part_north.end(), [&](int p) { return p == mypart; });
    bool mypart_at_south = std::any_of(part_south.begin(), part_south.end(), [&](int p) { return p == mypart; });

    bool three_dimensional             = options.getBool("three_dimensional");
    bool periodic_east_west            = rg.periodic();
    bool include_periodic_ghost_points = periodic_east_west && !three_dimensional;
    bool remove_periodic_ghost_points  = periodic_east_west && three_dimensional;

    bool has_point_at_north_pole = y_north == 90 && nx_north > 0;
    bool has_point_at_south_pole = y_south == -90 && nx_south > 0;
    bool possible_north_pole     = !has_point_at_north_pole && rg.domain().containsNorthPole() && mypart_at_north;
    bool possible_south_pole     = !has_point_at_south_pole && rg.domain().containsSouthPole() && mypart_at_south;

    bool force_include_north_pole(options.has("force_include_north_pole") &&
                                  options.getBool("force_include_north_pole"));
    bool force_include_south_pole(options.has("force_include_south_pole") &&
                                  options.getBool("force_include_south_pole"));

    bool include_north_pole = (possible_north_pole && options.getBool("include_pole")) || force_include_north_pole;
    bool include_south_pole = (possible_south_pole && options.getBool("include_pole")) || force_include_south_pole;
    bool patch_north_pole   = possible_north_pole && options.getBool("patch_pole") && nx_north > 0;
    bool patch_south_pole   = possible_south_pole && options.getBool("patch_pole") && nx_south > 0;
    bool patch_quads        = options.getBool("patch_quads");

    int nnewnodes = (!has_point_at_north_pole && include_north_pole ? 1 : 0) +
                    (!has_point_at_south_pole && include_south_pole ? 1 : 0);

    if (three_dimensional && nparts != 1) {
        throw_Exception("Cannot generate three_dimensional mesh in parallel", Here());
    }
    int nnodes  = region.nnodes;
    int ntriags = region.ntriags;
    int nquads  = region.nquads;

    if (include_north_pole) {
        ++nnodes;
        ntriags += nx_north - (periodic_east_west ? 0 : 1);
    }
    else if (patch_north_pole) {
        if (patch_quads) {
            nquads += (nx_north + 1) / 2;
        }
        else {
            ntriags += nx_north - 2;
        }
    }
    if (include_south_pole) {
        ++nnodes;
        ntriags += nx_south - (periodic_east_west ? 0 : 1);
    }
    else if (patch_south_pole) {
        if (patch_quads) {
            nquads += (nx_south + 1) / 2;
        }
        else {
            ntriags += nx_south - 2;
        }
    }

    size_t node_numbering_size = nnodes;
    if (remove_periodic_ghost_points) {
        for (idx_t jlat = 0; jlat < rg.ny(); ++jlat) {
            if (region.lat_end[jlat] >= rg.nx(jlat)) {
                --nnodes;
            }
        }
    }
    ATLAS_ASSERT(nnodes >= nnewnodes);

#if DEBUG_OUTPUT
    ATLAS_DEBUG_VAR(include_periodic_ghost_points);
    ATLAS_DEBUG_VAR(include_north_pole);
    ATLAS_DEBUG_VAR(include_south_pole);
    ATLAS_DEBUG_VAR(three_dimensional);
    ATLAS_DEBUG_VAR(patch_north_pole);
    ATLAS_DEBUG_VAR(patch_south_pole);
    ATLAS_DEBUG_VAR(rg.size());
    ATLAS_DEBUG_VAR(nnodes);
    ATLAS_DEBUG_VAR(ntriags);
    ATLAS_DEBUG_VAR(nquads);
    ATLAS_DEBUG_VAR(options.getBool("ghost_at_end"));
#endif

    std::vector<int> offset_glb(rg.ny());
    std::vector<int> offset_loc(region.south - region.north + 1, 0);

    n = 0;
    for (idx_t jlat = 0; jlat < rg.ny(); ++jlat) {
        offset_glb.at(jlat) = n;
        n += rg.nx(jlat);
    }

    std::vector<int> periodic_glb(rg.ny());

    if (include_periodic_ghost_points) {
        for (idx_t jlat = 0; jlat < rg.ny(); ++jlat) {
            if (rg.nx(jlat) > 0) {
                periodic_glb.at(jlat) = n;
                ++n;
            }
        }
    }
    else {
        for (idx_t jlat = 0; jlat < rg.ny(); ++jlat) {
            if (rg.nx(jlat) > 0) {
                periodic_glb.at(jlat) = offset_glb.at(jlat) + rg.nx(jlat) - 1;
            }
        }
    }
    int max_glb_idx = n;

    mesh.nodes().resize(nnodes);
    mesh::Nodes& nodes = mesh.nodes();

    auto xy      = array::make_view<double, 2>(nodes.xy());
    auto lonlat  = array::make_view<double, 2>(nodes.lonlat());
    auto glb_idx = array::make_view<gidx_t, 1>(nodes.global_index());
    auto part    = array::make_view<int, 1>(nodes.partition());
    auto ghost   = array::make_view<int, 1>(nodes.ghost());
    auto flags   = array::make_view<int, 1>(nodes.flags());
    auto halo    = array::make_view<int, 1>(nodes.halo());

    idx_t jnorth = -1;
    idx_t jsouth = -1;

    std::vector<idx_t> node_numbering(node_numbering_size, -1);
    if (nnodes > 0) {
    if (options.getBool("ghost_at_end")) {
        std::vector<GhostNode> ghost_nodes;
        ghost_nodes.reserve(nnodes);
        idx_t node_number = 0;
        idx_t jnode       = 0;
        l                 = 0;
        ATLAS_ASSERT(region.south >= region.north);
        for (int jlat = region.north; jlat <= region.south; ++jlat) {
            int ilat            = jlat - region.north;
            offset_loc.at(ilat) = l;
            l += region.lat_end.at(jlat) - region.lat_begin.at(jlat) + 1;

            if (region.lat_end.at(jlat) < region.lat_begin.at(jlat)) {
                ATLAS_DEBUG_VAR(jlat);
                ATLAS_DEBUG_VAR(region.lat_begin[jlat]);
                ATLAS_DEBUG_VAR(region.lat_end[jlat]);
            }
            for (idx_t jlon = region.lat_begin.at(jlat); jlon <= region.lat_end.at(jlat); ++jlon) {
                if (jlon < rg.nx(jlat)) {
                    n = offset_glb.at(jlat) + jlon;
                    if (distribution.partition(n) == mypart) {
                        node_numbering.at(jnode) = node_number;
                        ++node_number;
                    }
                    else {
                        ghost_nodes.emplace_back(jlat, jlon, jnode);
                    }
                    ++jnode;
                }
                else if (include_periodic_ghost_points)  // add periodic point
                {
                    //#warning TODO: use commented approach
                    part(jnode) = mypart;
                    // part(jnode)      = parts.at( offset_glb.at(jlat) );
                    ghost(jnode) = 1;
                    halo(jnode)  = 0;
                    ghost_nodes.emplace_back(jlat, rg.nx(jlat), jnode);
                    ++jnode;
                }
                else {
                    --l;
                }
            }
        }
        for (size_t jghost = 0; jghost < ghost_nodes.size(); ++jghost) {
            node_numbering.at(ghost_nodes.at(jghost).jnode) = node_number;
            ++node_number;
        }
        if (include_north_pole) {
            node_numbering.at(jnode) = jnode;
            ++jnode;
        }
        if (include_south_pole) {
            node_numbering.at(jnode) = jnode;
            ++jnode;
        }
        ATLAS_ASSERT(jnode == nnodes);
    }
    else  // No renumbering
    {
        for (idx_t jnode = 0; jnode < nnodes; ++jnode) {
            node_numbering.at(jnode) = jnode;
        }
    }

    idx_t jnode = 0;
    l           = 0;
    for (idx_t jlat = region.north; jlat <= region.south; ++jlat) {
        idx_t ilat          = jlat - region.north;
        offset_loc.at(ilat) = l;
        l += region.lat_end.at(jlat) - region.lat_begin.at(jlat) + 1;

        double y = rg.y(jlat);
        for (idx_t jlon = region.lat_begin.at(jlat); jlon <= region.lat_end.at(jlat); ++jlon) {
            if (jlon < rg.nx(jlat)) {
                idx_t inode = node_numbering.at(jnode);
                n           = offset_glb.at(jlat) + jlon;

                double x = rg.x(jlon, jlat);
                // std::cout << "jlat = " << jlat << "; jlon = " << jlon << "; x = " <<
                // x << std::endl;

                xy(inode, XX) = x;
                xy(inode, YY) = y;

                // geographic coordinates by using projection
                double crd[] = {x, y};
                rg.projection().xy2lonlat(crd);
                lonlat(inode, LON) = crd[LON];
                lonlat(inode, LAT) = crd[LAT];

                glb_idx(inode) = n + 1;
                part(inode)    = distribution.partition(n);
                ghost(inode)   = 0;
                halo(inode)    = 0;
                Topology::reset(flags(inode));
                Topology::set(flags(inode), Topology::BC);
                if (jlat == 0) {
                    if (y_numbering < 0 && !include_north_pole) {
                        Topology::set(flags(inode), Topology::NORTH);
                    }
                    if (y_numbering > 0 && !include_south_pole) {
                        Topology::set(flags(inode), Topology::SOUTH);
                    }
                }
                if (jlat == rg.ny() - 1) {
                    if (y_numbering < 0 && !include_south_pole) {
                        Topology::set(flags(inode), Topology::SOUTH);
                    }
                    if (y_numbering > 0 && !include_north_pole) {
                        Topology::set(flags(inode), Topology::NORTH);
                    }
                }
                if (jlon == 0 && include_periodic_ghost_points) {
                    Topology::set(flags(inode), Topology::WEST);
                }
                if (part(inode) != mypart) {
                    Topology::set(flags(inode), Topology::GHOST);
                    ghost(inode) = 1;
                }
                ++jnode;
            }
            else if (include_periodic_ghost_points)  // add periodic point
            {
                idx_t inode = node_numbering.at(jnode);
                // int inode_left = node_numbering.at(jnode-1);
                double x = rg.x(rg.nx(jlat), jlat);

                xy(inode, XX) = x;
                xy(inode, YY) = y;

                // geographic coordinates by using projection
                double crd[] = {x, y};
                rg.projection().xy2lonlat(crd);
                lonlat(inode, LON) = crd[LON];
                lonlat(inode, LAT) = crd[LAT];

                glb_idx(inode) = periodic_glb.at(jlat) + 1;
                //#warning TODO: use commented approach
                //        part(inode)      = parts.at( offset_glb.at(jlat) );
                part(inode)  = mypart;  // The actual part will be fixed later
                ghost(inode) = 1;
                halo(inode)  = 0;
                Topology::reset(flags(inode));
                Topology::set(flags(inode), Topology::BC | Topology::EAST);
                Topology::set(flags(inode), Topology::GHOST);
                if (jlat == 0) {
                    if (y_numbering < 0 && !include_north_pole) {
                        Topology::set(flags(inode), Topology::NORTH);
                    }
                    if (y_numbering > 0 && !include_south_pole) {
                        Topology::set(flags(inode), Topology::SOUTH);
                    }
                }
                if (jlat == rg.ny() - 1) {
                    if (y_numbering < 0 && !include_south_pole) {
                        Topology::set(flags(inode), Topology::SOUTH);
                    }
                    if (y_numbering > 0 && !include_north_pole) {
                        Topology::set(flags(inode), Topology::NORTH);
                    }
                }
                ++jnode;
            }
            else {
                --l;
            }
        }
    }

    if (include_north_pole) {
        idx_t inode   = node_numbering.at(jnode);
        jnorth        = jnode;
        double y      = 90.;
        double x      = 180.;
        xy(inode, XX) = x;
        xy(inode, YY) = y;

        // geographic coordinates by using projection
        double crd[] = {x, y};
        rg.projection().xy2lonlat(crd);
        lonlat(inode, LON) = crd[LON];
        lonlat(inode, LAT) = crd[LAT];

        glb_idx(inode) = periodic_glb.at(rg.ny() - 1) + 2;
        part(inode)    = mypart;
        ghost(inode)   = 0;
        halo(inode)    = 0;
        Topology::reset(flags(inode));
        Topology::set(flags(inode), Topology::NORTH);
        ++jnode;
    }

    if (include_south_pole) {
        idx_t inode   = node_numbering.at(jnode);
        jsouth        = jnode;
        double y      = -90.;
        double x      = 180.;
        xy(inode, XX) = x;
        xy(inode, YY) = y;

        // geographic coordinates by using projection
        double crd[] = {x, y};
        rg.projection().xy2lonlat(crd);
        lonlat(inode, LON) = crd[LON];
        lonlat(inode, LAT) = crd[LAT];

        glb_idx(inode) = periodic_glb.at(rg.ny() - 1) + 3;
        part(inode)    = mypart;
        ghost(inode)   = 0;
        halo(inode)    = 0;
        Topology::reset(flags(inode));
        Topology::set(flags(inode), Topology::SOUTH);
        ++jnode;
    }
    }

    mesh.metadata().set<size_t>("nb_nodes_including_halo[0]", nodes.size());
    nodes.metadata().set<size_t>("NbRealPts", size_t(nnodes - nnewnodes));
    nodes.metadata().set<size_t>("NbVirtualPts", size_t(nnewnodes));
    nodes.global_index().metadata().set("human_readable", true);
    nodes.global_index().metadata().set("min", 1);
    nodes.global_index().metadata().set("max", max_glb_idx);

    // Now handle elements
    // -------------------

    mesh.cells().add(mesh::ElementType::create("Quadrilateral"), nquads);
    mesh.cells().add(mesh::ElementType::create("Triangle"), ntriags);

    mesh::HybridElements::Connectivity& node_connectivity = mesh.cells().node_connectivity();
    array::ArrayView<gidx_t, 1> cells_glb_idx             = array::make_view<gidx_t, 1>(mesh.cells().global_index());
    array::ArrayView<int, 1> cells_part                   = array::make_view<int, 1>(mesh.cells().partition());
    array::ArrayView<int, 1> cells_flags                  = array::make_view<int, 1>(mesh.cells().flags());

    /*
     * label all patch cells a non-patch
     */
    cells_flags.assign(0);

    /*
     * Fill in connectivity tables with global node indices first
     */
    idx_t jcell;
    idx_t jquad       = 0;
    idx_t jtriag      = 0;
    idx_t quad_begin  = mesh.cells().elements(0).begin();
    idx_t triag_begin = mesh.cells().elements(1).begin();
    idx_t quad_nodes[4];
    idx_t triag_nodes[3];

    auto fix_quad_orientation = [](idx_t nodes[]) {
        idx_t tmp;
        tmp      = nodes[0];
        nodes[0] = nodes[1];
        nodes[1] = tmp;
        tmp      = nodes[2];
        nodes[2] = nodes[3];
        nodes[3] = tmp;
    };
    auto fix_triag_orientation = [](idx_t nodes[]) {
        idx_t tmp;
        tmp      = nodes[0];
        nodes[0] = nodes[1];
        nodes[1] = tmp;
    };

    bool regular_cells_glb_idx = rg.regular();
    if (options.getBool("triangulate") || y_numbering > 0 || rg.y().front() != 90. || rg.y().back() != -90.) {
        regular_cells_glb_idx = false;
    }

    if ((region.nquads + region.ntriags) > 0) {
    for (idx_t jlat = region.north; jlat < region.south; ++jlat) {
        idx_t ilat  = jlat - region.north;
        idx_t jlatN = jlat;
        idx_t jlatS = jlat + 1;
        idx_t ilatN = ilat;
        idx_t ilatS = ilat + 1;
        for (idx_t jelem = 0; jelem < region.nb_lat_elems.at(jlat); ++jelem) {
            const auto elem = array::make_view<int, 3>(*region.elems).slice(ilat, jelem, Range::all());

            if (elem(2) >= 0 && elem(3) >= 0)  // This is a quad
            {
                quad_nodes[0] = node_numbering.at(offset_loc.at(ilatN) + elem(0) - region.lat_begin.at(jlatN));
                quad_nodes[1] = node_numbering.at(offset_loc.at(ilatS) + elem(1) - region.lat_begin.at(jlatS));
                quad_nodes[2] = node_numbering.at(offset_loc.at(ilatS) + elem(2) - region.lat_begin.at(jlatS));
                quad_nodes[3] = node_numbering.at(offset_loc.at(ilatN) + elem(3) - region.lat_begin.at(jlatN));

                if (three_dimensional && periodic_east_west) {
                    if (elem(2) == rg.nx(jlatS)) {
                        quad_nodes[2] = node_numbering.at(offset_loc.at(ilatS));
                    }
                    if (elem(3) == rg.nx(jlatN)) {
                        quad_nodes[3] = node_numbering.at(offset_loc.at(ilatN));
                    }
                }

                if (y_numbering > 0) {
                    fix_quad_orientation(quad_nodes);
                }

                jcell = quad_begin + jquad++;
                node_connectivity.set(jcell, quad_nodes);
                cells_glb_idx(jcell) = jcell + 1;
                cells_part(jcell)    = mypart;
                if( regular_cells_glb_idx ) {
                    gidx_t nx = rg.nx(jlatN) - 1;
                    if (periodic_east_west) {
                        ++nx;
                    }
                    cells_glb_idx(jcell) = glb_idx( quad_nodes[0] );
                }
            }
            else  // This is a triag
            {
                if (elem(3) < 0)  // This is a triangle pointing up
                {
                    triag_nodes[0] = node_numbering.at(offset_loc.at(ilatN) + elem(0) - region.lat_begin.at(jlatN));
                    triag_nodes[1] = node_numbering.at(offset_loc.at(ilatS) + elem(1) - region.lat_begin.at(jlatS));
                    triag_nodes[2] = node_numbering.at(offset_loc.at(ilatS) + elem(2) - region.lat_begin.at(jlatS));
                    if (three_dimensional && periodic_east_west) {
                        if (elem(0) == rg.nx(jlatN)) {
                            triag_nodes[0] = node_numbering.at(offset_loc.at(ilatN));
                        }
                        if (elem(2) == rg.nx(jlatS)) {
                            triag_nodes[2] = node_numbering.at(offset_loc.at(ilatS));
                        }
                    }
                    if (y_numbering > 0) {
                        fix_triag_orientation(triag_nodes);
                    }
                }
                else  // This is a triangle pointing down
                {
                    triag_nodes[0] = node_numbering.at(offset_loc.at(ilatN) + elem(0) - region.lat_begin.at(jlatN));
                    triag_nodes[1] = node_numbering.at(offset_loc.at(ilatS) + elem(1) - region.lat_begin.at(jlatS));
                    triag_nodes[2] = node_numbering.at(offset_loc.at(ilatN) + elem(3) - region.lat_begin.at(jlatN));
                    if (three_dimensional && periodic_east_west) {
                        if (elem(1) == rg.nx(jlatS)) {
                            triag_nodes[1] = node_numbering.at(offset_loc.at(ilatS));
                        }
                        if (elem(3) == rg.nx(jlatN)) {
                            triag_nodes[2] = node_numbering.at(offset_loc.at(ilatN));
                        }
                    }
                    if (y_numbering > 0) {
                        fix_triag_orientation(triag_nodes);
                    }
                }
                jcell = triag_begin + jtriag++;
                node_connectivity.set(jcell, triag_nodes);
                cells_glb_idx(jcell) = jcell + 1;
                cells_part(jcell)    = mypart;
            }
        }
    }

    if (include_north_pole) {
        idx_t ilat = 0;
        idx_t ip1  = 0;
        idx_t nlon = rg.nx(0) - (periodic_east_west ? 0 : 1);
        for (idx_t ip2 = 0; ip2 < nlon; ++ip2) {
            jcell     = triag_begin + jtriag++;
            idx_t ip3 = ip2 + 1;
            if (three_dimensional && ip3 == rg.nx(0)) {
                ip3 = 0;
            }
            triag_nodes[0] = node_numbering.at(jnorth + ip1);
            triag_nodes[1] = node_numbering.at(offset_loc.at(ilat) + ip2);
            triag_nodes[2] = node_numbering.at(offset_loc.at(ilat) + ip3);
            if (y_numbering > 0) {
                fix_triag_orientation(triag_nodes);
            }
            node_connectivity.set(jcell, triag_nodes);
            cells_glb_idx(jcell) = jcell + 1;
            cells_part(jcell)    = mypart;
        }
    }
    else if (patch_north_pole) {
        idx_t jlat = y_numbering < 0 ? 0 : rg.ny() - 1;
        idx_t ilat = y_numbering < 0 ? 0 : std::abs(region.south - region.north);

        if (patch_quads) {
            idx_t ip1, ip2, ip3, ip4;

            idx_t jforward  = 0;
            idx_t jbackward = rg.nx(jlat) - 1;

            while (true) {
                ip1 = jforward;
                ip2 = jforward + 1;
                ip3 = jbackward - 1;
                ip4 = jbackward;

                quad_nodes[0] = node_numbering.at(offset_loc.at(ilat) + ip1);
                quad_nodes[1] = node_numbering.at(offset_loc.at(ilat) + ip2);
                quad_nodes[2] = node_numbering.at(offset_loc.at(ilat) + ip3);
                quad_nodes[3] = node_numbering.at(offset_loc.at(ilat) + ip4);

                jcell = quad_begin + jquad++;
                node_connectivity.set(jcell, quad_nodes);

                cells_glb_idx(jcell) = jcell + 1;
                cells_part(jcell)    = mypart;
                Topology::set(cells_flags(jcell), Topology::PATCH);

                if (jcell == cells_glb_idx.size() - 1) {
                }
                if (jforward + 1 == jbackward) {
                    break;
                }
                ++jforward;
                --jbackward;
            }
        }
        else {
            idx_t ip1, ip2, ip3;

            idx_t jforward  = 0;
            idx_t jbackward = rg.nx(jlat) - 1;
            bool forward    = true;

            while (true) {
                if (forward) {
                    ip1 = jforward;
                    ip2 = jforward + 1;
                    ip3 = jbackward;
                }
                else {
                    ip1 = jforward;
                    ip2 = jbackward - 1;
                    ip3 = jbackward;
                }

                triag_nodes[0] = node_numbering.at(offset_loc.at(ilat) + ip1);
                triag_nodes[1] = node_numbering.at(offset_loc.at(ilat) + ip2);
                triag_nodes[2] = node_numbering.at(offset_loc.at(ilat) + ip3);
                //            if ( y_numbering > 0 ) {
                //                fix_triag_orientation( triag_nodes );
                //            }

                jcell = triag_begin + jtriag++;
                node_connectivity.set(jcell, triag_nodes);

                cells_glb_idx(jcell) = jcell + 1;
                cells_part(jcell)    = mypart;
                Topology::set(cells_flags(jcell), Topology::PATCH);

                if (jbackward == jforward + 2) {
                    break;
                }

                if (forward) {
                    ++jforward;
                    forward = false;
                }
                else {
                    --jbackward;
                    forward = true;
                }
            }
        }
    }

    if (include_south_pole) {
        idx_t jlat = y_numbering > 0 ? 0 : rg.ny() - 1;
        idx_t ilat = y_numbering > 0 ? 0 : std::abs(region.south - region.north);
        idx_t ip1  = 0;
        idx_t nlon = rg.nx(jlat) + 1 - (periodic_east_west ? 0 : 1);
        for (idx_t ip2 = 1; ip2 < nlon; ++ip2) {
            jcell          = triag_begin + jtriag++;
            int ip3        = ip2 - 1;
            triag_nodes[0] = node_numbering.at(jsouth + ip1);
            triag_nodes[1] = node_numbering.at(offset_loc.at(ilat) + ip2);
            triag_nodes[2] = node_numbering.at(offset_loc.at(ilat) + ip3);
            if (three_dimensional && ip2 == rg.nx(jlat)) {
                triag_nodes[1] = node_numbering.at(offset_loc.at(ilat) + 0);
            }
            if (y_numbering > 0) {
                fix_triag_orientation(triag_nodes);
            }

            node_connectivity.set(jcell, triag_nodes);
            cells_glb_idx(jcell) = jcell + 1;
            cells_part(jcell)    = mypart;
        }
    }
    else if (patch_south_pole) {
        idx_t jlat = y_numbering > 0 ? 0 : rg.ny() - 1;
        idx_t ilat = y_numbering > 0 ? 0 : std::abs(region.south - region.north);

        if (patch_quads) {
            idx_t ip1, ip2, ip3, ip4;

            idx_t jforward  = 0;
            idx_t jbackward = rg.nx(jlat) - 1;

            while (true) {
                ip1 = jbackward;
                ip2 = jbackward - 1;
                ip3 = jforward + 1;
                ip4 = jforward;

                quad_nodes[0] = node_numbering.at(offset_loc.at(ilat) + ip1);
                quad_nodes[1] = node_numbering.at(offset_loc.at(ilat) + ip2);
                quad_nodes[2] = node_numbering.at(offset_loc.at(ilat) + ip3);
                quad_nodes[3] = node_numbering.at(offset_loc.at(ilat) + ip4);

                jcell = quad_begin + jquad++;
                node_connectivity.set(jcell, quad_nodes);

                cells_glb_idx(jcell) = jcell + 1;
                cells_part(jcell)    = mypart;
                Topology::set(cells_flags(jcell), Topology::PATCH);

                if (jcell == cells_glb_idx.size() - 1) {
                }
                if (jforward + 1 == jbackward) {
                    break;
                }
                ++jforward;
                --jbackward;
            }
        }
        else {
            idx_t ip1, ip2, ip3;

            idx_t jforward  = 0;
            idx_t jbackward = rg.nx(jlat) - 1;
            bool forward    = true;

            while (true) {
                if (forward) {
                    ip1 = jbackward;
                    ip2 = jforward + 1;
                    ip3 = jforward;
                }
                else {
                    ip1 = jbackward;
                    ip2 = jbackward - 1;
                    ip3 = jforward;
                }

                triag_nodes[0] = node_numbering.at(offset_loc.at(ilat) + ip1);
                triag_nodes[1] = node_numbering.at(offset_loc.at(ilat) + ip2);
                triag_nodes[2] = node_numbering.at(offset_loc.at(ilat) + ip3);
                //            if ( y_numbering > 0 ) {
                //                fix_triag_orientation( triag_nodes );
                //            }

                jcell = triag_begin + jtriag++;
                node_connectivity.set(jcell, triag_nodes);

                cells_glb_idx(jcell) = jcell + 1;
                cells_part(jcell)    = mypart;
                Topology::set(cells_flags(jcell), Topology::PATCH);

                if (jbackward == jforward + 2) {
                    break;
                }

                if (forward) {
                    ++jforward;
                    forward = false;
                }
                else {
                    --jbackward;
                    forward = true;
                }
            }
        }
    }
    }
    if (regular_cells_glb_idx) {
        mesh.cells().global_index().metadata().set("min", 1);
        mesh.cells().global_index().metadata().set("max", rg.nx(0) * (rg.ny()-1) );
    }
    else {
        generateGlobalElementNumbering(mesh);
    }
}

namespace {
static MeshGeneratorBuilder<StructuredMeshGenerator> __Structured("structured");
}  // namespace

}  // namespace meshgenerator
}  // namespace atlas
