/*
 * (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 "Structured.h"

#include <algorithm>
#include <iomanip>
#include <limits>
#include <numeric>

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

#include "atlas/domain/Domain.h"
#include "atlas/grid/StructuredGrid.h"
#include "atlas/grid/detail/grid/GridBuilder.h"
#include "atlas/grid/detail/grid/GridFactory.h"
#include "atlas/grid/detail/spacing/CustomSpacing.h"
#include "atlas/grid/detail/spacing/LinearSpacing.h"
#include "atlas/parallel/mpi/mpi.h"
#include "atlas/runtime/Exception.h"
#include "atlas/runtime/Log.h"
#include "atlas/runtime/Trace.h"
#include "atlas/util/NormaliseLongitude.h"
#include "atlas/util/Point.h"
#include "atlas/util/UnitSphere.h"

namespace atlas {
namespace grid {
namespace detail {
namespace grid {

std::string Structured::static_type() {
    return "structured";
}

std::string Structured::name() const {
    return name_;
}

Structured::Structured(XSpace xspace, YSpace yspace, Projection p, Domain domain):
    Structured(Structured::static_type(), xspace, yspace, p, domain) {}

Structured::Structured(const std::string& name, XSpace xspace, YSpace yspace, Projection projection, Domain domain):
    Grid(), name_(name), xspace_(xspace), yspace_(yspace) {
    // Copy members
    projection_ = projection ? projection : Projection();

    y_.assign(yspace_.begin(), yspace_.end());
    idx_t ny{static_cast<idx_t>(y_.size())};

    if (xspace_.ny() == 1 && yspace_.size() > 1) {
        nx_.resize(ny, xspace_.nx()[0]);
        dx_.resize(ny, xspace_.dx()[0]);
        xmin_.resize(ny, xspace_.xmin()[0]);
        xmax_.resize(ny, xspace_.xmax()[0]);
    }
    else {
        nx_   = xspace_.nx();
        dx_   = xspace_.dx();
        xmin_ = xspace_.xmin();
        xmax_ = xspace_.xmax();
    }

    ATLAS_ASSERT(static_cast<idx_t>(nx_.size()) == ny);

    // Further setup
    nxmin_ = nxmax_ = nx_.front();
    for (idx_t j = 1; j < ny; ++j) {
        nxmin_ = std::min(nx_[j], nxmin_);
        nxmax_ = std::max(nx_[j], nxmax_);
    }
    size_t npts_size_t = 0;
    for (auto& nx : nx_) {
        npts_size_t += size_t(nx);
    }
    npts_ = std::accumulate(nx_.begin(), nx_.end(), idx_t{0});
    if (size_t(npts_) != npts_size_t) {
        ATLAS_THROW_EXCEPTION(
            "Cannot represent grid.size() with idx_t of "
            << ATLAS_BITS_LOCAL
            << " bits.\n"
               "Recompile atlas with idx_t 64bits, or wait for pending development where grid index to become gidx_t");
    }
    crop(domain);

    computeTruePeriodicity();

    jglooff_.resize(ny + 1);
    jglooff_[0] = 0;
    for (idx_t j = 1; j < ny + 1; j++) {
        jglooff_[j] = jglooff_[j - 1] + nx_[j - 1];
    }
}

Domain Structured::computeDomain() const {
    if (periodic()) {
        return ZonalBandDomain({yspace().min(), yspace().max()}, xspace().min());
    }
    return RectangularDomain({xspace().min(), xspace().max()}, {yspace().min(), yspace().max()}, projection_.units());
}

Structured::~Structured() = default;

Structured::XSpace::XSpace(): impl_(nullptr) {}

Structured::XSpace::XSpace(const XSpace& xspace) = default;

template <typename NVector>
Structured::XSpace::XSpace(const std::array<double, 2>& interval, const NVector& N, bool endpoint):
    impl_(new Implementation(interval, N, endpoint)) {}
template Structured::XSpace::XSpace(const std::array<double, 2>& interval, const std::vector<int>& N, bool endpoint);
template Structured::XSpace::XSpace(const std::array<double, 2>& interval, const std::vector<long>& N, bool endpoint);

Structured::XSpace::XSpace(const std::array<double, 2>& interval, std::initializer_list<int>&& N, bool endpoint):
    XSpace(interval, std::vector<int>{N}, endpoint) {}

Structured::XSpace::XSpace(const Spacing& spacing, idx_t ny): impl_(new Implementation(spacing, ny)) {}

Structured::XSpace::XSpace(const std::vector<Spacing>& spacings): impl_(new Implementation(spacings)) {}

Structured::XSpace::XSpace(const std::vector<spacing::LinearSpacing::Params>& spacings):
    impl_(new Implementation(spacings)) {}

Structured::XSpace::XSpace(const Config& config): impl_(new Implementation(config)) {}

Structured::XSpace::XSpace(const std::vector<Config>& config): impl_(new Implementation(config)) {}

Grid::Spec Structured::XSpace::spec() const {
    return impl_->spec();
}

Structured::XSpace::Implementation::Implementation(const Config& config) {
    Config config_xspace(config);

    std::string xspace_type;
    config_xspace.get("type", xspace_type);
    ATLAS_ASSERT(xspace_type == "linear");

    std::vector<idx_t> v_N;
    std::vector<double> v_start;
    std::vector<double> v_end;
    std::vector<double> v_length;
    config_xspace.get("N[]", v_N);
    config_xspace.get("start[]", v_start);
    config_xspace.get("end[]", v_end);
    config_xspace.get("length[]", v_length);

    idx_t ny = std::max(v_N.size(), std::max(v_start.size(), std::max(v_end.size(), std::max(v_length.size(), 1ul))));
    reserve(ny);

    if (not v_N.empty()) {
        ATLAS_ASSERT(static_cast<idx_t>(v_N.size()) == ny);
    }
    if (not v_start.empty()) {
        ATLAS_ASSERT(static_cast<idx_t>(v_start.size()) == ny);
    }
    if (not v_end.empty()) {
        ATLAS_ASSERT(static_cast<idx_t>(v_end.size()) == ny);
    }
    if (not v_length.empty()) {
        ATLAS_ASSERT(static_cast<idx_t>(v_length.size()) == ny);
    }

    nxmin_ = std::numeric_limits<idx_t>::max();
    nxmax_ = 0;
    min_   = std::numeric_limits<double>::max();
    max_   = -std::numeric_limits<double>::max();

    for (idx_t j = 0; j < ny; ++j) {
        if (not v_N.empty()) {
            config_xspace.set("N", v_N[j]);
        }
        if (not v_start.empty()) {
            config_xspace.set("start", v_start[j]);
        }
        if (not v_end.empty()) {
            config_xspace.set("end", v_end[j]);
        }
        if (not v_length.empty()) {
            config_xspace.set("length", v_length[j]);
        }
        spacing::LinearSpacing::Params xspace(config_xspace);
        xmin_.push_back(xspace.start);
        xmax_.push_back(xspace.end);
        nx_.push_back(xspace.N);
        dx_.push_back(xspace.step);
        nxmin_ = std::min(nxmin_, nx_[j]);
        nxmax_ = std::max(nxmax_, nx_[j]);
        min_   = std::min(min_, xspace.start);
        max_   = std::max(max_, xspace.end);
    }
}

Structured::XSpace::Implementation::Implementation(const std::vector<Config>& config_list) {
    reserve(config_list.size());

    nxmin_ = std::numeric_limits<idx_t>::max();
    nxmax_ = 0;
    min_   = std::numeric_limits<double>::max();
    max_   = -std::numeric_limits<double>::max();

    std::string xspace_type;
    for (idx_t j = 0; j < ny(); ++j) {
        config_list[j].get("type", xspace_type);
        ATLAS_ASSERT(xspace_type == "linear");
        spacing::LinearSpacing::Params xspace(config_list[j]);
        xmin_.push_back(xspace.start);
        xmax_.push_back(xspace.end);
        nx_.push_back(xspace.N);
        dx_.push_back(xspace.step);
        nxmin_ = std::min(nxmin_, nx_[j]);
        nxmax_ = std::max(nxmax_, nx_[j]);
        min_   = std::min(min_, xspace.start);
        max_   = std::max(max_, xspace.end);
    }
}

Structured::XSpace::Implementation::Implementation(const std::vector<spacing::LinearSpacing::Params>& spacings) {
    reserve(spacings.size());

    nxmin_ = std::numeric_limits<idx_t>::max();
    nxmax_ = 0;
    min_   = std::numeric_limits<double>::max();
    max_   = -std::numeric_limits<double>::max();

    for (idx_t j = 0; j < ny(); ++j) {
        const auto& xspace = spacings[j];
        xmin_.push_back(xspace.start);
        xmax_.push_back(xspace.end);
        nx_.push_back(xspace.N);
        dx_.push_back(xspace.step);
        nxmin_ = std::min(nxmin_, nx_[j]);
        nxmax_ = std::max(nxmax_, nx_[j]);
        min_   = std::min(min_, xspace.start);
        max_   = std::max(max_, xspace.end);
    }
}


void Structured::XSpace::Implementation::Implementation::reserve(idx_t ny) {
    ny_ = ny;
    nx_.reserve(ny);
    xmin_.reserve(ny);
    xmax_.reserve(ny);
    dx_.reserve(ny);
}

template <typename NVector>
Structured::XSpace::Implementation::Implementation(const std::array<double, 2>& interval, const NVector& N,
                                                   bool endpoint):
    ny_(N.size()), nx_(N.begin(), N.end()), xmin_(ny_, interval[0]), xmax_(ny_, interval[1]), dx_(ny_) {
    nxmin_        = std::numeric_limits<idx_t>::max();
    nxmax_        = 0;
    min_          = std::numeric_limits<double>::max();
    max_          = -std::numeric_limits<double>::max();
    double length = interval[1] - interval[0];
    for (idx_t j = 0; j < ny_; ++j) {
        nxmin_ = std::min(nxmin_, nx_[j]);
        nxmax_ = std::max(nxmax_, nx_[j]);
        dx_[j] = endpoint ? length / double(nx_[j] - 1) : length / double(nx_[j]);
        min_   = std::min(min_, xmin_[j]);
        max_   = std::max(max_, xmax_[j]);
    }
}
template Structured::XSpace::Implementation::Implementation(const std::array<double, 2>& interval,
                                                            const std::vector<int>& N, bool endpoint);
template Structured::XSpace::Implementation::Implementation(const std::array<double, 2>& interval,
                                                            const std::vector<long>& N, bool endpoint);

Structured::XSpace::Implementation::Implementation(const std::array<double, 2>& interval,
                                                   std::initializer_list<int>&& N, bool endpoint):
    Implementation(interval, std::vector<int>{N}, endpoint) {}


Structured::XSpace::Implementation::Implementation(const Spacing& spacing, idx_t ny):
    ny_(ny), nx_(ny_, spacing.size()), xmin_(ny_, spacing.min()), xmax_(ny_, spacing.max()), dx_(ny_) {
    const spacing::LinearSpacing& linspace = dynamic_cast<const spacing::LinearSpacing&>(*spacing.get());
    dx_[0]                                 = linspace.step();
    for (idx_t j = 1; j < ny_; ++j) {
        dx_[j] = dx_[0];
    }
    nxmax_ = nx_[0];
    nxmin_ = nx_[0];
    min_   = spacing.min();
    max_   = spacing.max();
}

Structured::XSpace::Implementation::Implementation(const std::vector<Spacing>& spacings):
    ny_(spacings.size()),
    nxmax_(0),
    nxmin_(std::numeric_limits<idx_t>::max()),
    nx_(ny_),
    xmin_(ny_),
    xmax_(ny_),
    dx_(ny_),
    min_(std::numeric_limits<double>::max()),
    max_(-std::numeric_limits<double>::max()) {
    for (idx_t j = 0; j < ny_; ++j) {
        const spacing::LinearSpacing& linspace = dynamic_cast<const spacing::LinearSpacing&>(*spacings[j].get());

        nx_[j]   = linspace.size();
        xmin_[j] = linspace.min();
        xmax_[j] = linspace.max();
        dx_[j]   = linspace.step();
        nxmin_   = std::min(nxmin_, nx_[j]);
        nxmax_   = std::max(nxmax_, nx_[j]);
        min_     = std::min(min_, xmin_[j]);
        max_     = std::max(max_, xmax_[j]);
    }
}


std::string Structured::XSpace::Implementation::type() const {
    return "linear";
}

Grid::Spec Structured::XSpace::Implementation::spec() const {
    Grid::Spec spec;

    bool same_xmin = true;
    bool same_xmax = true;
    bool same_nx   = true;

    double xmin = xmin_[0];
    double xmax = xmax_[0];
    idx_t nx    = nx_[0];
    double dx   = dx_[0];

    ATLAS_ASSERT(static_cast<idx_t>(xmin_.size()) == ny_);
    ATLAS_ASSERT(static_cast<idx_t>(xmax_.size()) == ny_);
    ATLAS_ASSERT(static_cast<idx_t>(nx_.size()) == ny_);

    for (idx_t j = 1; j < ny_; ++j) {
        same_xmin = same_xmin && (eckit::types::is_approximately_equal(xmin_[j], xmin));
        same_xmax = same_xmax && (eckit::types::is_approximately_equal(xmax_[j], xmax));
        same_nx   = same_nx && (nx_[j] == nx);
    }

    bool endpoint = std::abs((xmax - xmin) - (nx - 1) * dx) < 1.e-10;

    spec.set("type", "linear");
    if (same_xmin) {
        spec.set("start", xmin);
    }
    else {
        spec.set("start[]", xmin_);
    }
    if (same_xmax) {
        spec.set("end", xmax);
    }
    else {
        spec.set("end[]", xmax_);
    }
    if (same_nx) {
        spec.set("N", nx);
    }
    else {
        spec.set("N[]", nx_);
    }
    spec.set("endpoint", endpoint);

    return spec;
}

namespace {
class Normalise {
public:
    Normalise(const RectangularDomain& domain): degrees_(domain.units() == "degrees"), normalise_(domain.xmin()) {}

    double operator()(double x) const {
        if (degrees_) {
            x = normalise_(x);
        }
        return x;
    }

private:
    const bool degrees_;
    util::NormaliseLongitude normalise_;
};
}  // namespace

void Structured::crop(const Domain& dom) {
    using eckit::types::is_approximately_equal;

    if (!dom) {
        domain_ =
            RectangularDomain({xspace_.min(), xspace_.max()}, {yspace_.min(), yspace_.max()}, projection_.units());
        return;
    }

    ATLAS_ASSERT(dom.units() == projection().units());
    auto rect_domain = RectangularDomain(dom);

    if (!rect_domain) {
        std::stringstream errmsg;
        errmsg << "Cannot crop the grid with domain " << dom;
        throw_Exception(errmsg.str(), Here());
    }

    const double cropped_ymin = rect_domain.ymin();
    const double cropped_ymax = rect_domain.ymax();

    // Cropping in Y
    idx_t jmin = ny();
    idx_t jmax = 0;
    for (idx_t j = 0; j < ny(); ++j) {
        if (rect_domain.contains_y(y(j))) {
            jmin = std::min(j, jmin);
            jmax = std::max(j, jmax);
        }
    }
    ATLAS_ASSERT(jmax >= jmin);

    idx_t cropped_ny = jmax - jmin + 1;
    ATLAS_ASSERT(cropped_ny <= ny());

    std::vector<double> cropped_y(y_.begin() + jmin, y_.begin() + jmin + cropped_ny);
    std::vector<double> cropped_dx(dx_.begin() + jmin, dx_.begin() + jmin + cropped_ny);

    std::vector<double> cropped_xmin(cropped_ny, std::numeric_limits<double>::max());
    std::vector<double> cropped_xmax(cropped_ny, -std::numeric_limits<double>::max());
    std::vector<idx_t> cropped_nx(cropped_ny);

    Normalise normalise(rect_domain);

    auto index_of_smallest_normalised_x = [&](idx_t j) {
        // Compute point of normalisation with bisection method
        idx_t imin        = 0;
        idx_t imax        = nx(j) - 1;
        auto normalised_x = [&](idx_t i) { return normalise(x(i, j)); };
        double xmin       = normalised_x(imin);
        double xmax       = normalised_x(imax);
        idx_t max_iter    = nx(j);
        idx_t iter        = 0;
        while (xmax < xmin && imax != imin + 1) {
            idx_t imid = (imin + imax) / 2;
            if (normalised_x(imid) > xmin) {
                imin = imid;
                xmin = normalised_x(imin);
            }
            else {
                imax = imid;
                xmax = normalised_x(imax);
            }
            if (iter == max_iter) {
                ATLAS_THROW_EXCEPTION("Could not converge");
            }
            ++iter;
        }
        return (xmin < xmax ? imin : imax);
    };

    bool is_regular = [&]() {
        if (reduced()) {
            return false;
        }
        long _nx     = nx(jmin);
        double _dx   = _nx > 0 ? dx(jmin) : std::numeric_limits<double>::max();
        double _xmin = _nx > 0 ? xmin(jmin) : std::numeric_limits<double>::max();

        for (idx_t j = jmin; j <= jmax; ++j) {
            if (nx(j) != _nx) {
                return false;
            }
            if (dx(j) != _dx) {
                return false;
            }
            if (xmin(j) != _xmin) {
                return false;
            }
        }
        return true;
    }();

    // Cropping in X
    bool endpoint = true;
    {
        if (rect_domain.zonal_band()) {
            for (idx_t j = jmin, jcropped = 0; j <= jmax; ++j, ++jcropped) {
                if (is_regular && jcropped > 0) {
                    cropped_xmin[jcropped] = cropped_xmin[0];
                    cropped_xmax[jcropped] = cropped_xmax[0];
                    cropped_nx[jcropped]   = cropped_nx[0];
                    continue;
                }
                idx_t i_xmin           = index_of_smallest_normalised_x(j);
                idx_t i_xmax           = i_xmin == 0 ? nx(j) - 1 : i_xmin - 1;
                cropped_xmin[jcropped] = normalise(x(i_xmin, j));
                cropped_xmax[jcropped] = normalise(x(i_xmax, j));
                cropped_nx[jcropped]   = nx(j);
                if (is_approximately_equal(cropped_xmax[jcropped] + cropped_dx[jcropped], cropped_xmin[jcropped] + 360.,
                                           1.e-10)) {
                    cropped_xmax[jcropped] = cropped_xmin[jcropped] + 360.;
                    endpoint               = false;
                }
            }
        }
        else {
            for (idx_t j = jmin, jcropped = 0; j <= jmax; ++j, ++jcropped) {
                if (is_regular && jcropped > 0) {
                    cropped_xmin[jcropped] = cropped_xmin[0];
                    cropped_xmax[jcropped] = cropped_xmax[0];
                    cropped_nx[jcropped]   = cropped_nx[0];
                    continue;
                }

                idx_t i_xmin = index_of_smallest_normalised_x(j);

                for (idx_t n = 0; n < nx(j); ++n) {
                    idx_t i = i_xmin + n;
                    if (i >= nx(j)) {
                        i -= nx(j);
                    }
                    double xmin = normalise(x(i, j));
                    if (rect_domain.contains_x(xmin)) {
                        i_xmin                 = i;
                        cropped_xmin[jcropped] = xmin;
                        break;
                    }
                }
                double xmin    = normalise(x(i_xmin, j));
                idx_t i_xmax   = i_xmin;
                idx_t max_iter = nx(j);
                idx_t n        = 1;
                ATLAS_ASSERT(rect_domain.contains_x(xmin));

                while (true) {
                    idx_t i_xmax_next = i_xmax + 1;
                    if (i_xmax_next == nx(j)) {
                        i_xmax_next -= nx(j);
                    }
                    if (i_xmax_next == i_xmin) {
                        break;
                    }
                    double xmax_next = normalise(x(i_xmax_next, j));

                    if (not rect_domain.contains_x(xmax_next)) {
                        break;
                    }
                    if (n == max_iter) {
                        ATLAS_THROW_EXCEPTION("Could not converge");
                    }
                    i_xmax++;
                    n++;
                }

                cropped_xmin[jcropped] = normalise(x(i_xmin, j));
                cropped_xmax[jcropped] = normalise(x(i_xmax, j));
                cropped_nx[jcropped]   = std::max<idx_t>(1, n);
            }
        }
    }

    // Complete structures

    idx_t cropped_nxmin, cropped_nxmax;
    cropped_nxmin = cropped_nxmax = cropped_nx.front();

    for (idx_t j = 1; j < cropped_ny; ++j) {
        cropped_nxmin = std::min(cropped_nx[j], cropped_nxmin);
        cropped_nxmax = std::max(cropped_nx[j], cropped_nxmax);
        if (is_regular) {
            break;
        }
    }
    idx_t cropped_npts = std::accumulate(cropped_nx.begin(), cropped_nx.end(), idx_t{0});


    XSpace cropped_xspace;
    if (is_regular) {
        cropped_xspace = XSpace(LinearSpacing{cropped_xmin[0], cropped_xmax[0], cropped_nx[0], endpoint}, cropped_ny);
    }
    else {
        std::vector<spacing::LinearSpacing::Params> cropped_xspacing_params(cropped_ny);
        for (idx_t j = 0; j < cropped_ny; ++j) {
            cropped_xspacing_params[j] = {cropped_xmin[j], cropped_xmax[j], cropped_nx[j], endpoint};
        }
        cropped_xspace = XSpace(cropped_xspacing_params);
    }


    for (idx_t j = 0; j < cropped_ny; ++j) {
        ATLAS_ASSERT(eckit::types::is_approximately_equal(cropped_xspace.xmin()[j], cropped_xmin[j]));
        if (cropped_nx[j] > 1) {
            ATLAS_ASSERT(eckit::types::is_approximately_equal(cropped_xspace.dx()[j], cropped_dx[j], 1.e-10));
        }
        ATLAS_ASSERT(cropped_xspace.nx()[j] == cropped_nx[j]);
        ATLAS_ASSERT(cropped_xspace.nxmin() == cropped_nxmin);
        ATLAS_ASSERT(cropped_xspace.nxmax() == cropped_nxmax);
    }

    // Modify grid
    if (ny() != cropped_ny) {
        // keep specialised spacing (Gaussian, Linear, ...) unless cropping happens
        yspace_ = new spacing::CustomSpacing(cropped_ny, cropped_y.data(), {cropped_ymin, cropped_ymax});
    }

    xspace_ = cropped_xspace;
    xmin_   = cropped_xmin;
    xmax_   = cropped_xmax;
    dx_     = cropped_dx;
    nx_     = cropped_nx;
    nxmin_  = cropped_nxmin;
    nxmax_  = cropped_nxmax;
    npts_   = cropped_npts;
    y_      = cropped_y;

    domain_ = dom;
}

void Structured::computeTruePeriodicity() {
    if (projection_.strictlyRegional()) {
        periodic_x_ = false;
        return;
    }
    if (not ZonalBandDomain(domain_)) {
        periodic_x_ = false;
        return;
    }

    idx_t j                  = ny() / 2;
    const PointLonLat Pllmin = projection().lonlat(PointXY(xmin_[j], y_[j]));
    const PointLonLat Pllmax = projection().lonlat(PointXY(xmax_[j], y_[j]));

    Point3 Pxmin;
    util::UnitSphere::convertSphericalToCartesian(Pllmin, Pxmin);

    Point3 Pxmax;
    util::UnitSphere::convertSphericalToCartesian(Pllmax, Pxmax);

    periodic_x_ = points_equal(Pxmin, Pxmax);
}

void Structured::print(std::ostream& os) const {
    os << "Structured(Name:" << name() << ")";
}

std::string Structured::type() const {
    return static_type();
}

Grid::Config Structured::meshgenerator() const {
    return Config("type", "structured");
}

Grid::Config Structured::partitioner() const {
    Config config;
    if (mpi::size() == 1) {
        config.set("type", "serial");
    }
    else if (reduced()) {
        config.set("type", "equal_regions");
    }
    else {
        config.set("type", "checkerboard");
    }
    return config;
}

void Structured::hash(eckit::Hash& h) const {
    double multiplier      = projection().units() == "meters" ? 1e2 : 1e8;
    auto add_double        = [&](const double& x) { h.add(std::round(x * multiplier)); };
    auto add_double_vector = [&](const std::vector<double>& xvec) {
        for (auto& x : xvec) {
            add_double(x);
        }
    };
    auto add_long        = [&](const idx_t& x) { h.add(long(x)); };
    auto add_long_vector = [&](const std::vector<idx_t>& xvec) {
        for (auto& x : xvec) {
            add_long(x);
        }
    };

    add_double_vector(y());
    add_long_vector(nx());

    add_double_vector(xmin_);
    add_double_vector(dx_);

    // also add projection information
    projection().hash(h);

    // also add domain information, even though already encoded in grid.
    domain().hash(h);
}

RectangularLonLatDomain Structured::lonlatBoundingBox() const {
    return projection_ ? projection_.lonlatBoundingBox(computeDomain()) : domain();
}

Grid::Spec Structured::spec() const {
    Grid::Spec grid_spec;

    if (name() == "structured") {
        grid_spec.set("type", type());
        grid_spec.set("xspace", xspace().spec());
        grid_spec.set("yspace", yspace().spec());
    }
    else {
        grid_spec.set("name", name());
    }
    grid_spec.set("domain", domain().spec());
    grid_spec.set("projection", projection().spec());
    return grid_spec;
}

// --------------------------------------------------------------------

#if 1
namespace {  // anonymous

static class structured : public GridBuilder {
    using Implementation = atlas::Grid::Implementation;
    using Config         = Grid::Config;
    using XSpace         = StructuredGrid::XSpace;

public:
    structured(): GridBuilder("structured") {}

    void print(std::ostream& os) const override {
        os << std::left << std::setw(20) << " "
           << "Structured grid";
    }

    const Implementation* create(const std::string& /* name */, const Config&) const override {
        throw_NotImplemented("Cannot create structured grid from name", Here());
    }

    const Implementation* create(const Config& config) const override {
        Projection projection;
        Spacing yspace;
        Domain domain;

        Config config_proj;
        if (config.get("projection", config_proj)) {
            projection = Projection(config_proj);
        }

        Config config_domain;
        if (config.get("domain", config_domain)) {
            domain = Domain(config_domain);
        }

        Config config_yspace;
        if (not config.get("yspace", config_yspace)) {
            throw_Exception("yspace missing in configuration", Here());
        }
        yspace = Spacing(config_yspace);

        XSpace xspace;

        Config config_xspace;
        std::vector<Config> config_xspace_list;

        if (config.get("xspace[]", config_xspace_list)) {
            xspace = XSpace(config_xspace_list);
        }
        else if (config.get("xspace", config_xspace)) {
            xspace = XSpace(config_xspace);
        }
        else {
            throw_Exception("xspace missing in configuration", Here());
        }

        return new StructuredGrid::grid_t(xspace, yspace, projection, domain);
    }

} structured_;

}  // anonymous namespace
#endif

// --------------------------------------------------------------------

extern "C" {

idx_t atlas__grid__Structured__ny(Structured* This) {
    ATLAS_ASSERT(This != nullptr, "Cannot access uninitialised atlas_StructuredGrid");
    return This->ny();
}

idx_t atlas__grid__Structured__nx(Structured* This, idx_t jlat) {
    ATLAS_ASSERT(This != nullptr, "Cannot access uninitialised atlas_StructuredGrid");
    return This->nx(jlat);
}

gidx_t atlas__grid__Structured__index(Structured* This, idx_t i, idx_t j) {
    ATLAS_ASSERT(This != nullptr, "Cannot access uninitialised atlas_StructuredGrid");
    return This->index(i, j);
}

void atlas__grid__Structured__index2ij(Structured* This, gidx_t gidx, idx_t& i, idx_t& j) {
    ATLAS_ASSERT(This != nullptr, "Cannot access uninitialised atlas_StructuredGrid");
    This->index2ij(gidx, i, j);
}

void atlas__grid__Structured__nx_array(Structured* This, const idx_t*& nx_array, idx_t& size) {
    ATLAS_ASSERT(This != nullptr, "Cannot access uninitialised atlas_StructuredGrid");
    nx_array = This->nx().data();
    size     = idx_t(This->nx().size());
}

idx_t atlas__grid__Structured__nxmax(Structured* This) {
    ATLAS_ASSERT(This != nullptr, "Cannot access uninitialised atlas_StructuredGrid");
    return This->nxmax();
}

idx_t atlas__grid__Structured__nxmin(Structured* This) {
    ATLAS_ASSERT(This != nullptr, "Cannot access uninitialised atlas_StructuredGrid");
    return This->nxmin();
}

double atlas__grid__Structured__y(Structured* This, idx_t j) {
    ATLAS_ASSERT(This != nullptr, "Cannot access uninitialised atlas_StructuredGrid");
    return This->y(j);
}

double atlas__grid__Structured__x(Structured* This, idx_t i, idx_t j) {
    ATLAS_ASSERT(This != nullptr, "Cannot access uninitialised atlas_StructuredGrid");
    return This->x(i, j);
}

void atlas__grid__Structured__xy(Structured* This, idx_t i, idx_t j, double crd[]) {
    ATLAS_ASSERT(This != nullptr, "Cannot access uninitialised atlas_StructuredGrid");
    This->xy(i, j, crd);
}

void atlas__grid__Structured__lonlat(Structured* This, idx_t i, idx_t j, double crd[]) {
    ATLAS_ASSERT(This != nullptr, "Cannot access uninitialised atlas_StructuredGrid");
    This->lonlat(i, j, crd);
}

void atlas__grid__Structured__y_array(Structured* This, const double*& y_array, idx_t& size) {
    ATLAS_ASSERT(This != nullptr, "Cannot access uninitialised atlas_StructuredGrid");
    y_array = This->y().data();
    size    = idx_t(This->y().size());
}

int atlas__grid__Structured__reduced(Structured* This) {
    ATLAS_ASSERT(This != nullptr, "Cannot access uninitialised atlas_StructuredGrid");
    return This->reduced();
}

const Structured* atlas__grid__Structured(char* identifier) {
    const Structured* grid = dynamic_cast<const Structured*>(Grid::create(std::string(identifier)));
    ATLAS_ASSERT(grid != nullptr);
    return grid;
}

const Structured* atlas__grid__Structured__config(util::Config* conf) {
    ATLAS_ASSERT(conf != nullptr);
    const Structured* grid = dynamic_cast<const Structured*>(Grid::create(*conf));
    ATLAS_ASSERT(grid != nullptr);
    return grid;
}

void atlas__grid__Structured__delete(Structured* This) {
    ATLAS_ASSERT(This != nullptr, "Cannot access uninitialised atlas_StructuredGrid");
    delete This;
}

Structured* atlas__grid__regular__RegularGaussian(long N) {
    ATLAS_NOTIMPLEMENTED;
}
Structured* atlas__grid__regular__RegularLonLat(long nlon, long nlat) {
    ATLAS_NOTIMPLEMENTED;
}
Structured* atlas__grid__regular__ShiftedLonLat(long nlon, long nlat) {
    ATLAS_NOTIMPLEMENTED;
}
Structured* atlas__grid__regular__ShiftedLon(long nlon, long nlat) {
    ATLAS_NOTIMPLEMENTED;
}
Structured* atlas__grid__regular__ShiftedLat(long nlon, long nlat) {
    ATLAS_NOTIMPLEMENTED;
}

idx_t atlas__grid__Gaussian__N(Structured* This) {
    ATLAS_ASSERT(This != nullptr, "Cannot access uninitialised atlas_StructuredGrid");
    GaussianGrid gaussian(This);
    ATLAS_ASSERT(gaussian, "This grid is not a Gaussian grid");
    return gaussian.N();
}
}

namespace {
GridFactoryBuilder<Structured> __register_Structured(Structured::static_type());
}

}  // namespace grid
}  // namespace detail
}  // namespace grid
}  // namespace atlas
