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

#pragma once

#include <array>
#include <memory>

#include "atlas/grid/Spacing.h"
#include "atlas/grid/detail/grid/Grid.h"
#include "atlas/grid/detail/spacing/LinearSpacing.h"
#include "atlas/library/config.h"
#include "atlas/runtime/Exception.h"
#include "atlas/util/Object.h"
#include "atlas/util/ObjectHandle.h"
#include "atlas/util/Point.h"

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

/**
 * @brief Structured Grid
 *
 * This class is a base class for all grids that can be described by
 * constant latitudes with a uniform distribution of points per latitude
 * in zonal direction.
 * This means any full grid and reduced grid, both regular, gaussian or other
 * such distribution can be represented with this class
 */
class Structured : public Grid {
private:
    struct ComputePointXY {
        ComputePointXY(const Structured& grid): grid_(grid), ny_(grid_.ny()) {}
        void operator()(idx_t i, idx_t j, PointXY& point) {
            if (j < ny_) {  // likely
                grid_.xy(i, j, point.data());
            }
        }
        const Structured& grid_;
        idx_t ny_;
    };
    struct ComputePointLonLat {
        ComputePointLonLat(const Structured& grid): grid_(grid), ny_(grid_.ny()) {}
        void operator()(idx_t i, idx_t j, PointLonLat& point) {
            if (j < ny_) {  // likely
                grid_.lonlat(i, j, point.data());
            }
        }
        const Structured& grid_;
        idx_t ny_;
    };

    template <typename Base, typename ComputePoint>
    class StructuredIterator : public Base {
    public:
        StructuredIterator(const Structured& grid, bool begin = true):
            grid_(grid), ny_(grid_.ny()), i_(0), j_(begin ? 0 : grid_.ny()), compute_point{grid_} {
            if (j_ != ny_ && grid_.size()) {
                compute_point(i_, j_, point_);
            }
        }
        virtual bool next(typename Base::value_type& point) {
            if (j_ < ny_ && i_ < grid_.nx(j_)) {
                compute_point(i_++, j_, point);

                if (i_ == grid_.nx(j_)) {
                    j_++;
                    i_ = 0;
                }
                return true;
            }
            return false;
        }

        virtual const typename Base::reference operator*() const { return point_; }

        virtual const Base& operator++() {
            ++i_;
            if (i_ == grid_.nx(j_)) {
                ++j_;
                i_ = 0;
            }
            compute_point(i_, j_, point_);
            return *this;
        }

        virtual const Base& operator+=(typename Base::difference_type distance) {
            idx_t d = distance;
            while (j_ != ny_ && d >= (grid_.nx(j_) - i_)) {
                d -= (grid_.nx(j_) - i_);
                ++j_;
                i_ = 0;
            }
            i_ += d;
            compute_point(i_, j_, point_);
            return *this;
        }

        virtual typename Base::difference_type distance(const Base& other) const {
            const auto& _other               = static_cast<const StructuredIterator&>(other);
            typename Base::difference_type d = 0;
            idx_t j                          = j_;
            idx_t i                          = i_;
            while (j < _other.j_) {
                d += grid_.nx(j) - i;
                ++j;
                i = 0;
            }
            d += _other.i_;
            return d;
        }

        virtual bool operator==(const Base& other) const {
            return j_ == static_cast<const StructuredIterator&>(other).j_ &&
                   i_ == static_cast<const StructuredIterator&>(other).i_;
        }

        virtual bool operator!=(const Base& other) const {
            return i_ != static_cast<const StructuredIterator&>(other).i_ ||
                   j_ != static_cast<const StructuredIterator&>(other).j_;
        }

        virtual std::unique_ptr<Base> clone() const {
            auto result    = new StructuredIterator(grid_, false);
            result->i_     = i_;
            result->j_     = j_;
            result->point_ = point_;
            return std::unique_ptr<Base>(result);
        }

    public:
        const Structured& grid_;
        idx_t ny_;
        idx_t i_;
        idx_t j_;
        typename Base::value_type point_;
        ComputePoint compute_point;
    };

public:
    using IteratorXY     = StructuredIterator<Grid::IteratorXY, ComputePointXY>;
    using IteratorLonLat = StructuredIterator<Grid::IteratorLonLat, ComputePointLonLat>;

public:
    class XSpace {
        class Implementation : public util::Object {
        public:
            // Constructor NVector can be either std::vector<int> or std::vector<long>
            template <typename NVector>
            Implementation(const std::array<double, 2>& interval, const NVector& N, bool endpoint = true);

            Implementation(const std::array<double, 2>& interval, std::initializer_list<int>&& N, bool endpoint = true);

            Implementation(const Spacing&, idx_t ny = 1);

            Implementation(const std::vector<Spacing>&);

            Implementation(const Config&);

            Implementation(const std::vector<Config>&);

            Implementation(const std::vector<spacing::LinearSpacing::Params>&);

            idx_t ny() const { return ny_; }

            // Minimum number of points across parallels (constant y)
            idx_t nxmin() const { return nxmin_; }

            // Maximum number of points across parallels (constant y)
            idx_t nxmax() const { return nxmax_; }

            /// Number of points per latitude
            const std::vector<idx_t>& nx() const { return nx_; }

            /// Value of minimum longitude per latitude [default=0]
            const std::vector<double>& xmin() const { return xmin_; }

            /// Value of maximum longitude per latitude [default=0]
            const std::vector<double>& xmax() const { return xmax_; }

            /// Value of longitude increment
            const std::vector<double>& dx() const { return dx_; }

            /// Value of minimum x over entire grid
            double min() const { return min_; }

            /// Value of maximum x over entire grid
            double max() const { return max_; }

            Spec spec() const;

            std::string type() const;

        private:
            void reserve(idx_t ny);

        private:
            idx_t ny_;
            idx_t nxmin_;
            idx_t nxmax_;
            std::vector<idx_t> nx_;
            std::vector<double> xmin_;
            std::vector<double> xmax_;
            std::vector<double> dx_;
            double min_;
            double max_;
        };

    public:
        XSpace();

        XSpace(const XSpace&);

        XSpace(const Spacing&, idx_t ny = 1);

        XSpace(const std::vector<Spacing>&);

        XSpace(const std::vector<spacing::LinearSpacing::Params>&);

        // Constructor NVector can be either std::vector<int> or std::vector<long> or initializer list
        template <typename NVector>
        XSpace(const std::array<double, 2>& interval, const NVector& N, bool endpoint = true);

        XSpace(const std::array<double, 2>& interval, std::initializer_list<int>&& N, bool endpoint = true);

        XSpace(const Config&);

        XSpace(const std::vector<Config>&);

        idx_t ny() const { return impl_->ny(); }

        // Minimum number of points across parallels (constant y)
        idx_t nxmin() const { return impl_->nxmin(); }

        // Maximum number of points across parallels (constant y)
        idx_t nxmax() const { return impl_->nxmax(); }

        /// Number of points per latitude
        const std::vector<idx_t>& nx() const { return impl_->nx(); }

        /// Value of minimum longitude per latitude [default=0]
        const std::vector<double>& xmin() const { return impl_->xmin(); }

        /// Value of maximum longitude per latitude [default=0]
        const std::vector<double>& xmax() const { return impl_->xmax(); }

        /// Value of longitude increment
        const std::vector<double>& dx() const { return impl_->dx(); }

        /// Value of minimum x over entire grid
        double min() const { return impl_->min(); }

        /// Value of maximum x over entire grid
        double max() const { return impl_->max(); }

        Spec spec() const;

        std::string type() const { return impl_->type(); }

    private:
        util::ObjectHandle<XSpace::Implementation> impl_;
    };

    using YSpace = Spacing;

public:
    static std::string static_type();

public:
    Structured(const std::string&, XSpace, YSpace, Projection, Domain);
    Structured(XSpace, YSpace, Projection, Domain);
    Structured(const Structured&, Domain);

    virtual ~Structured() override;

    virtual idx_t size() const override { return npts_; }

    virtual Spec spec() const override;

    /**
   * Human readable name
   * Either the name is the one given at construction as a canonical named grid,
   * or the name "structured"
   */
    virtual std::string name() const override;

    virtual std::string type() const override;

    inline idx_t ny() const { return static_cast<idx_t>(y_.size()); }

    inline idx_t nx(idx_t j) const { return static_cast<idx_t>(nx_[j]); }

    inline idx_t nxmax() const { return nxmax_; }

    inline idx_t nxmin() const { return nxmin_; }

    inline const std::vector<idx_t>& nx() const { return nx_; }

    inline const std::vector<double>& y() const { return y_; }

    inline double dx(idx_t j) const { return dx_[j]; }

    inline double xmin(idx_t j) const { return xmin_[j]; }

    inline double x(idx_t i, idx_t j) const { return xmin_[j] + static_cast<double>(i) * dx_[j]; }

    inline double y(idx_t j) const { return y_[j]; }

    inline void xy(idx_t i, idx_t j, double crd[]) const {
        crd[0] = x(i, j);
        crd[1] = y(j);
    }

    PointXY xy(idx_t i, idx_t j) const { return PointXY(x(i, j), y(j)); }

    PointLonLat lonlat(idx_t i, idx_t j) const { return projection_.lonlat(xy(i, j)); }

    void lonlat(idx_t i, idx_t j, double crd[]) const {
        xy(i, j, crd);
        projection_.xy2lonlat(crd);
    }

    inline bool reduced() const { return nxmax() != nxmin(); }

    bool periodic() const { return periodic_x_; }

    const XSpace& xspace() const { return xspace_; }
    const YSpace& yspace() const { return yspace_; }

    virtual std::unique_ptr<Grid::IteratorXY> xy_begin() const override {
        return std::unique_ptr<Grid::IteratorXY>(new IteratorXY(*this));
    }
    virtual std::unique_ptr<Grid::IteratorXY> xy_end() const override {
        return std::unique_ptr<Grid::IteratorXY>(new IteratorXY(*this, false));
    }
    virtual std::unique_ptr<Grid::IteratorLonLat> lonlat_begin() const override {
        return std::unique_ptr<Grid::IteratorLonLat>(new IteratorLonLat(*this));
    }
    virtual std::unique_ptr<Grid::IteratorLonLat> lonlat_end() const override {
        return std::unique_ptr<Grid::IteratorLonLat>(new IteratorLonLat(*this, false));
    }

    gidx_t index(idx_t i, idx_t j) const { return jglooff_[j] + i; }

    void index2ij(gidx_t gidx, idx_t& i, idx_t& j) const {
        if ((gidx < 0) || (gidx >= jglooff_.back())) {
            throw_Exception("Structured::index2ij: gidx out of bounds", Here());
        }
        idx_t ja = 0, jb = jglooff_.size();
        while (jb - ja > 1) {
            idx_t jm = (ja + jb) / 2;
            if (gidx < jglooff_[jm]) {
                jb = jm;
            }
            else {
                ja = jm;
            }
        }
        i = gidx - jglooff_[ja];
        j = ja;
    }

    Config meshgenerator() const override;
    Config partitioner() const override;


protected:  // methods
    virtual void print(std::ostream&) const override;

    virtual void hash(eckit::Hash&) const override;

    virtual RectangularLonLatDomain lonlatBoundingBox() const override;

    void computeTruePeriodicity();

    Domain computeDomain() const;

    void crop(const Domain&);

protected:
    // Minimum number of points across parallels (constant y)
    idx_t nxmin_;

    // Maximum number of points across parallels (constant y)
    idx_t nxmax_;

    /// Total number of unique points in the grid
    idx_t npts_;

    /// Latitude values
    std::vector<double> y_;

    /// Number of points per latitude
    std::vector<idx_t> nx_;

    /// Value of minimum longitude per latitude [default=0]
    std::vector<double> xmin_;

    /// Value of maximum longitude per latitude [default=0]
    std::vector<double> xmax_;

    /// Value of longitude increment
    std::vector<double> dx_;

    /// Periodicity in x-direction
    bool periodic_x_;

    /// Per-row offset
    std::vector<gidx_t> jglooff_;

private:
    std::string name_ = {"structured"};
    XSpace xspace_;
    YSpace yspace_;
    mutable std::string type_;
};

extern "C" {
void atlas__grid__Structured__delete(Structured* This);
const Structured* atlas__grid__Structured(char* identifier);
const Structured* atlas__grid__Structured__config(util::Config* conf);
Structured* atlas__grid__regular__RegularGaussian(long N);
Structured* atlas__grid__reduced__ReducedGaussian_int(int nx[], long ny);
Structured* atlas__grid__reduced__ReducedGaussian_long(long nx[], long ny);
Structured* atlas__grid__reduced__ReducedGaussian_int_projection(int nx[], long ny,
                                                                 const Projection::Implementation* projection);
Structured* atlas__grid__reduced__ReducedGaussian_long_projection(long nx[], long ny,
                                                                  const Projection::Implementation* projection);
Structured* atlas__grid__regular__RegularLonLat(long nx, long ny);
Structured* atlas__grid__regular__ShiftedLonLat(long nx, long ny);
Structured* atlas__grid__regular__ShiftedLon(long nx, long ny);
Structured* atlas__grid__regular__ShiftedLat(long nx, long ny);

void atlas__grid__Structured__nx_array(Structured* This, const idx_t*& nx, idx_t& size);
idx_t atlas__grid__Structured__nx(Structured* This, idx_t j);
idx_t atlas__grid__Structured__ny(Structured* This);
gidx_t atlas__grid__Structured__index(Structured* This, idx_t i, idx_t j);
void atlas__grid__Structured__index2ij(Structured* This, gidx_t gidx, idx_t& i, idx_t& j);
idx_t atlas__grid__Structured__nxmin(Structured* This);
idx_t atlas__grid__Structured__nxmax(Structured* This);
double atlas__grid__Structured__y(Structured* This, idx_t j);
double atlas__grid__Structured__x(Structured* This, idx_t i, idx_t j);
void atlas__grid__Structured__xy(Structured* This, idx_t i, idx_t j, double crd[]);
void atlas__grid__Structured__lonlat(Structured* This, idx_t i, idx_t j, double crd[]);
void atlas__grid__Structured__y_array(Structured* This, const double*& lats, idx_t& size);
int atlas__grid__Structured__reduced(Structured* This);

idx_t atlas__grid__Gaussian__N(Structured* This);
}

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