/*
 * (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 <algorithm>
#include <iterator>
#include <memory>
#include <string>
#include <vector>

#include "atlas/domain/Domain.h"
#include "atlas/library/config.h"
#include "atlas/projection/Projection.h"
#include "atlas/util/Object.h"

namespace eckit {
class Hash;
}
namespace atlas {
class PointXY;
class PointLonLat;
namespace util {
class Config;
};
}  // namespace atlas

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

class GridObserver;

class Grid : public util::Object {
public:  // types
    using Projection = atlas::Projection;
    using Domain     = atlas::Domain;
    using Config     = atlas::util::Config;
    using Spec       = atlas::util::Config;
    using uid_t      = std::string;
    using hash_t     = std::string;

    template <typename Derived, typename Point>
    class IteratorT {
    public:
        using difference_type   = size_t;
        using iterator_category = std::random_access_iterator_tag;
        using value_type        = Point;
        using pointer           = const Point*;
        using reference         = const Point&;

    public:
        virtual bool next( value_type& )                         = 0;
        virtual reference operator*() const                      = 0;
        virtual const Derived& operator++()                      = 0;
        virtual const Derived& operator+=( difference_type )     = 0;
        virtual bool operator==( const Derived& ) const          = 0;
        virtual bool operator!=( const Derived& ) const          = 0;
        virtual difference_type distance( const Derived& ) const = 0;
        virtual std::unique_ptr<Derived> clone() const           = 0;
        virtual ~IteratorT() {}
    };
    class IteratorXY : public IteratorT<IteratorXY, PointXY> {};
    class IteratorLonLat : public IteratorT<IteratorLonLat, PointLonLat> {};

public:  // methods
    static const Grid* create( const Config& );

    static const Grid* create( const std::string& name );

    static const Grid* create( const std::string& name, const Config& );

    static const Grid* create( const Grid&, const Domain& );

    /// ctor (default)
    Grid();

    /// dtor
    virtual ~Grid();

    /// Human readable name (may not be unique)
    virtual std::string name() const = 0;
    virtual std::string type() const = 0;

    /// Unique grid id
    /// Computed from the hash. Can be used to compare 2 grids.
    uid_t uid() const;

    /// Adds to the hash the information that makes this Grid unique
    virtual void hash( eckit::Hash& ) const = 0;

    /// @returns the hash of the information that makes this Grid unique
    std::string hash() const;

    /// @return area represented by the grid
    const Domain& domain() const { return domain_; }

    /// @return parallel/meridian limits containing the grid
    virtual RectangularLonLatDomain lonlatBoundingBox() const = 0;

    virtual size_t footprint() const { return 0; }

    /// @return projection (mapping between geographic coordinates and grid
    /// coordinates)
    const Projection& projection() const { return projection_; }

    /// @return number of grid points
    /// @note This methods should have constant access time, if necessary derived
    //        classes should compute it at construction
    virtual idx_t size() const = 0;

    virtual Spec spec() const = 0;

    virtual std::unique_ptr<IteratorXY> xy_begin() const         = 0;
    virtual std::unique_ptr<IteratorXY> xy_end() const           = 0;
    virtual std::unique_ptr<IteratorLonLat> lonlat_begin() const = 0;
    virtual std::unique_ptr<IteratorLonLat> lonlat_end() const   = 0;

    void attachObserver( GridObserver& ) const;
    void detachObserver( GridObserver& ) const;

    virtual Config meshgenerator() const;
    virtual Config partitioner() const;

protected:  // methods
    /// Fill provided me
    virtual void print( std::ostream& ) const = 0;

private:  // methods
    friend std::ostream& operator<<( std::ostream& s, const Grid& p ) {
        p.print( s );
        return s;
    }

private:  // members
    /// Cache the unique ID
    mutable uid_t uid_;

    /// Cache the hash
    mutable hash_t hash_;

protected:  // members
    Projection projection_;
    Domain domain_;

    mutable std::vector<GridObserver*> grid_observers_;
};

class GridObserver {
private:
    std::vector<const Grid*> registered_grids_;

public:
    void registerGrid( const Grid& grid ) {
        if ( std::find( registered_grids_.begin(), registered_grids_.end(), &grid ) == registered_grids_.end() ) {
            registered_grids_.push_back( &grid );
            grid.attachObserver( *this );
        }
    }
    void unregisterGrid( const Grid& grid ) {
        auto found = std::find( registered_grids_.begin(), registered_grids_.end(), &grid );
        if ( found != registered_grids_.end() ) {
            registered_grids_.erase( found );
            grid.detachObserver( *this );
        }
    }
    virtual ~GridObserver() {
        for ( auto grid : registered_grids_ ) {
            grid->detachObserver( *this );
        }
    }
    virtual void onGridDestruction( Grid& ) = 0;
};

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

extern "C" {
idx_t atlas__grid__Grid__size( Grid* This );
Grid::Spec* atlas__grid__Grid__spec( Grid* This );
void atlas__grid__Grid__uid( const Grid* This, char*& uid, int& size );
Grid::Domain::Implementation* atlas__grid__Grid__lonlat_bounding_box( const Grid* This );
}

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