/*
 * (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 <iomanip>
#include <ostream>

#include "LonLat.h"

#include "eckit/utils/Translator.h"

#include "atlas/grid/StructuredGrid.h"
#include "atlas/grid/detail/grid/GridBuilder.h"
#include "atlas/runtime/Exception.h"

namespace atlas {
namespace grid {
namespace {  // anonymous

static eckit::Translator<std::string, int> to_int;

static Domain domain(const Grid::Config& grid) {
    Grid::Config config;
    if (grid.get("domain", config)) {
        return Domain(config);
    }
    return Domain();
}

struct Shift {
    enum Bits
    {
        NONE = 0,
        LAT  = (1 << 1),
        LON  = (1 << 2)
    };

    Shift(int bits = NONE): bits_(bits) {}

    Shift(bool shift_lon, bool shift_lat): bits_((shift_lon ? LON : NONE) | (shift_lat ? LAT : NONE)) {}

    bool operator()(int bits) const { return (bits_ & bits) == bits; }

    const int bits_;
};

using XSpace = StructuredGrid::XSpace;

StructuredGrid::grid_t* create_lonlat(long nlon, long nlat, Shift shift, const Grid::Config& config = Grid::Config()) {
    bool shifted_x = shift(Shift::LON);
    bool shifted_y = shift(Shift::LAT);

    double start_x                   = (shifted_x ? 0.5 : 0.0) * 360.0 / double(nlon);
    std::array<double, 2> interval_x = {start_x, start_x + 360.};
    bool no_endpoint                 = false;
    XSpace xspace(interval_x, std::vector<idx_t>(nlat, nlon), no_endpoint);

    // spacing is uniform in y
    // If shifted_y, the whole interval is shifted by -dy/2, and last latitude
    // would be -90-dy/2 (below -90!!!), if endpoint=true.
    // Instead, we set endpoint=false so that last latitude is -90+dy/2 instead.
    Spacing yspace([&] {
        Grid::Config config_spacing;
        config_spacing.set("type", "linear");
        config_spacing.set("start", 90.0 - (shifted_y ? 90.0 / double(nlat) : 0.0));
        config_spacing.set("end", -90.0 - (shifted_y ? 90.0 / double(nlat) : 0.0));
        config_spacing.set("endpoint", shifted_y ? false : true);
        config_spacing.set("N", nlat);
        return config_spacing;
    }());

    Projection projection;
    Grid::Config config_projection;
    if (config.get("projection", config_projection)) {
        projection = Projection(config_projection);
    }

    std::string name;

    if (shifted_x and shifted_y) {
        name = "S";
    }
    else if (shifted_x and not shifted_y) {
        name = "Slon";
    }
    else if (not shifted_x and shifted_y) {
        name = "Slat";
    }
    else {
        name = "L";
    }

    name += std::to_string(nlon) + "x" + std::to_string(nlat);

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

StructuredGrid::grid_t* create_lonlat(const Grid::Config& config, Shift shift) {
    bool shifted_y = shift(Shift::LAT);

    long N, nx, ny;
    // dimensions
    if (config.get("N", N)) {
        nx = 4 * N;
        ny = shifted_y ? 2 * N : 2 * N + 1;
    }
    else if (config.get("nx", nx) && config.get("ny", ny)) {
    }
    else {
        throw_Exception("Configuration requires either N, or (nx,ny)", Here());
    }

    return create_lonlat(nx, ny, shift, config);
}

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

static class regular_lonlat : public GridBuilder {
public:
    regular_lonlat():
        GridBuilder("regular_lonlat", {"^[Ll]([0-9]+)x([0-9]+)$", "^[Ll]([0-9]+)$"}, {"L<nx>x<ny>", "L<gauss>"}) {}

    void print(std::ostream& os) const override {
        os << std::left << std::setw(20) << "L<nx>x<ny> / L<gauss>"
           << "Regular longitude-latitude grid";
    }

    const Grid::Implementation* create(const std::string& name, const Grid::Config& config) const override {
        int id;
        std::vector<std::string> matches;
        if (match(name, matches, id)) {
            util::Config grid(config);
            grid.set("type", type());

            if (id == 0) {
                grid.set("nx", to_int(matches[0]));
                grid.set("ny", to_int(matches[1]));
                return create(grid);
            }

            if (id == 1) {
                grid.set("N", to_int(matches[0]));
                return create(grid);
            }
        }
        return nullptr;
    }

    const Grid::Implementation* create(const Grid::Config& config) const override {
        return create_lonlat(config, Shift(false, false));
    }

    void force_link() {}

} regular_lonlat_;

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

static class shifted_lonlat : public GridBuilder {
public:
    shifted_lonlat():
        GridBuilder("shifted_lonlat", {"^[Ss]([0-9]+)x([0-9]+)$", "^[Ss]([0-9]+)$"}, {"S<nx>x<ny>", "S<gauss>"}) {}

    void print(std::ostream& os) const override {
        os << std::left << std::setw(20) << "S<nx>x<ny> / S<gauss>"
           << "Shifted longitude-latitude grid";
    }

    const Grid::Implementation* create(const std::string& name, const Grid::Config& config) const override {
        int id;
        std::vector<std::string> matches;
        if (match(name, matches, id)) {
            util::Config grid(config);
            grid.set("type", type());

            if (id == 0) {
                grid.set("nx", to_int(matches[0]));
                grid.set("ny", to_int(matches[1]));
                return create(grid);
            }

            if (id == 1) {
                grid.set("N", to_int(matches[0]));
                return create(grid);
            }
        }
        return nullptr;
    }

    const Grid::Implementation* create(const Grid::Config& config) const override {
        return create_lonlat(config, Shift(true, true));
    }

    void force_link() {}

} shifted_lonlat_;

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

static class shifted_lon : public GridBuilder {
public:
    shifted_lon():
        GridBuilder("shifted_lon", {"^[Ss][Ll][Oo][Nn]([0-9]+)x([0-9]+)$", "^[Ss][Ll][Oo][Nn]([0-9]+)$"},
                    {"Slon<nx>x<ny>", "Slon<gauss>"}) {}

    void print(std::ostream& os) const override {
        os << std::left << std::setw(20) << "Slon<nx>x<ny> / Slon<gauss>"
           << "Shifted longitude grid";
    }

    const Grid::Implementation* create(const std::string& name, const Grid::Config& config) const override {
        int id;
        std::vector<std::string> matches;
        if (match(name, matches, id)) {
            util::Config grid(config);
            grid.set("type", type());

            if (id == 0) {
                grid.set("nx", to_int(matches[0]));
                grid.set("ny", to_int(matches[1]));
                return create(grid);
            }

            if (id == 1) {
                grid.set("N", to_int(matches[0]));
                return create(grid);
            }
        }
        return nullptr;
    }

    const Grid::Implementation* create(const Grid::Config& config) const override {
        return create_lonlat(config, Shift(true, false));
    }

    void force_link() {}

} shifted_lon_;

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

static class shifted_lat : public GridBuilder {
public:
    shifted_lat():
        GridBuilder("shifted_lat", {"^[Ss][Ll][Aa][Tt]([0-9]+)x([0-9]+)$", "^[Ss][Ll][Aa][Tt]([0-9]+)$"},
                    {"Slat<nx>x<ny>", "Slat<gauss>"}) {}

    void print(std::ostream& os) const override {
        os << std::left << std::setw(20) << "Slat<nx>x<ny> / Slat<gauss>"
           << "Shifted latitude grid";
    }

    const Grid::Implementation* create(const std::string& name, const Grid::Config& config) const override {
        int id;
        std::vector<std::string> matches;
        if (match(name, matches, id)) {
            util::Config grid(config);
            grid.set("type", type());

            if (id == 0) {
                grid.set("nx", to_int(matches[0]));
                grid.set("ny", to_int(matches[1]));
                return create(grid);
            }

            if (id == 1) {
                grid.set("N", to_int(matches[0]));
                return create(grid);
            }
        }
        return nullptr;
    }

    const Grid::Implementation* create(const Grid::Config& config) const override {
        return create_lonlat(config, Shift(false, true));
    }

    void force_link() {}

} shifted_lat_;

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

}  // anonymous namespace

namespace detail {
namespace grid {

void force_link_LonLat() {
    regular_lonlat_.force_link();
    shifted_lonlat_.force_link();
    shifted_lon_.force_link();
    shifted_lat_.force_link();
}

}  // namespace grid
}  // namespace detail

}  // namespace grid
}  // namespace atlas
