/**********************************************************************
 *
 * GEOS - Geometry Engine Open Source
 * http://geos.osgeo.org
 *
 * Copyright (C) 2022 ISciences LLC
 * Copyright (C) 2006 Refractions Research Inc.
 *
 * This is free software; you can redistribute and/or modify it under
 * the terms of the GNU Lesser General Public Licence as published
 * by the Free Software Foundation.
 * See the COPYING file for more information.
 *
 **********************************************************************/

#pragma once

#include <geos/export.h>

#include <geos/geom/Coordinate.h> // for applyCoordinateFilter
#include <geos/geom/CoordinateSequenceIterator.h>

#include <cassert>
#include <vector>
#include <iostream>
#include <iosfwd> // ostream
#include <memory> // for unique_ptr typedef

// Forward declarations
namespace geos {
namespace geom {
class Envelope;
class CoordinateFilter;
}
}

namespace geos {
namespace geom { // geos::geom

/**
 * \class CoordinateSequence geom.h geos.h
 *
 * \brief
 * The internal representation of a list of coordinates inside a Geometry.
 *
 * A CoordinateSequence is capable of storing XY, XYZ, XYM, or XYZM Coordinates. For efficient
 * storage, the dimensionality of the CoordinateSequence should be specified at creation using
 * the constructor with `hasz` and `hasm` arguments. Currently most of the GEOS code base
 * stores 2D Coordinates and accesses using the Coordinate type. Sequences used by these parts
 * of the code must be created with constructors without `hasz` and `hasm` arguments.
 *
 * If a high-dimension Coordinate coordinate is read from a low-dimension CoordinateSequence,
 * the higher dimensions will be populated with incorrect values or a segfault may occur.
 *
 */
class GEOS_DLL CoordinateSequence {

public:

    /// Standard ordinate index values
    enum { X, Y, Z, M };

    using iterator = CoordinateSequenceIterator<CoordinateSequence, Coordinate>;
    using const_iterator = CoordinateSequenceIterator<const CoordinateSequence, const Coordinate>;

    typedef std::unique_ptr<CoordinateSequence> Ptr;

    /// \defgroup construct Constructors
    /// @{

    /**
     *  Create an CoordinateSequence capable of storing XY or XYZ coordinates.
     */
    CoordinateSequence();

    /**
     * Create a CoordinateSequence capable of storing XY, XYZ or XYZM coordinates.
     *
     * @param size size of the sequence to create.
     * @param dim 2 for 2D, 3 for XYZ, 4 for XYZM, or 0 to determine
     *            this based on the first coordinate in the sequence
     */
    CoordinateSequence(std::size_t size, std::size_t dim = 0);

    /**
     * Create a CoordinateSequence that packs coordinates of any dimension.
     * Code using a CoordinateSequence constructed in this way must not
     * attempt to access references to coordinates with dimensions that
     * are not actually stored in the sequence.
     *
     * @param size size of the sequence to create
     * @param hasz true if the stored
     * @param hasm
     * @param initialize
     */
    CoordinateSequence(std::size_t size, bool hasz, bool hasm, bool initialize = true);

    /**
     * Create a CoordinateSequence from a list of XYZ coordinates.
     * Code using the sequence may only access references to CoordinateXY
     * or Coordinate objects.
     */
    CoordinateSequence(const std::initializer_list<Coordinate>&);

    /**
     * Create a CoordinateSequence from a list of XY coordinates.
     * Code using the sequence may only access references to CoordinateXY objects.
     */
    CoordinateSequence(const std::initializer_list<CoordinateXY>&);

    /**
     * Create a CoordinateSequence from a list of XYM coordinates.
     * Code using the sequence may only access references to CoordinateXY
     * or CoordinateXYM objects.
     */
    CoordinateSequence(const std::initializer_list<CoordinateXYM>&);

    /**
     * Create a CoordinateSequence from a list of XYZM coordinates.
     */
    CoordinateSequence(const std::initializer_list<CoordinateXYZM>&);

    /**
     * Create a CoordinateSequence storing XY values only.
     *
     * @param size size of the sequence to create
     */
    static CoordinateSequence XY(std::size_t size)  {
        return CoordinateSequence(size, false, false);
    }

    /**
     * Create a CoordinateSequence storing XYZ values only.
     *
     * @param size size of the sequence to create
     */
    static CoordinateSequence XYZ(std::size_t size)  {
        return CoordinateSequence(size, true, false);
    }

    /**
     * Create a CoordinateSequence storing XYZM values only.
     *
     * @param size size of the sequence to create
     */
    static CoordinateSequence XYZM(std::size_t size)  {
        return CoordinateSequence(size, true, true);
    }

    /**
     * Create a CoordinateSequence storing XYM values only.
     *
     * @param size size of the sequence to create
     */
    static CoordinateSequence XYM(std::size_t size)  {
        return CoordinateSequence(size, false, true);
    }

    /** \brief
     * Returns a heap-allocated deep copy of this CoordinateSequence.
     */
    std::unique_ptr<CoordinateSequence> clone() const;

    /// @}
    /// \defgroup prop Properties
    /// @{

    /**
     * Return the Envelope containing all points in this sequence.
     * The Envelope is not cached and is computed each time the
     * method is called.
     */
    Envelope getEnvelope() const;

    /** \brief
     * Returns the number of Coordinates
     */
    std::size_t getSize() const {
        return size();
    }

    /** \brief
     * Returns the number of Coordinates
     */
    size_t size() const
    {
        assert(stride() == 2 || stride() == 3 || stride() == 4);
        switch(stride()) {
            case 2: return m_vect.size() / 2;
            case 4: return m_vect.size() / 4;
            default : return m_vect.size() / 3;
        }
    }

    /// Returns <code>true</code> if list contains no coordinates.
    bool isEmpty() const {
        return m_vect.empty();
    }

    /** \brief
    * Tests whether an a {@link CoordinateSequence} forms a ring,
    * by checking length and closure. Self-intersection is not checked.
    *
    * @return true if the coordinate form a ring.
    */
    bool isRing() const;

    /**
     * Returns the dimension (number of ordinates in each coordinate)
     * for this sequence.
     *
     * @return the dimension of the sequence.
     */
    std::size_t getDimension() const;

    bool hasZ() const {
        return m_hasdim ? m_hasz : (m_vect.empty() || !std::isnan(m_vect[2]));
    }

    bool hasM() const {
        return m_hasm;
    }

    /// Returns true if contains any two consecutive points
    bool hasRepeatedPoints() const;

    /// Returns true if contains any NaN/Inf coordinates
    bool hasRepeatedOrInvalidPoints() const;

    /// Get the backing type of this CoordinateSequence. This is not necessarily
    /// consistent with the dimensionality of the stored Coordinates; 2D Coordinates
    /// may be stored as a XYZ coordinates.
    CoordinateType getCoordinateType() const {
        switch(stride()) {
            case 4: return CoordinateType::XYZM;
            case 2: return CoordinateType::XY;
            default: return hasM() ? CoordinateType::XYM : CoordinateType::XYZ;
        }
    }

    /// @}
    /// \defgroup access Accessors
    /// @{

    /** \brief
     * Returns a read-only reference to Coordinate at position i.
     */
    template<typename T=Coordinate>
    const T& getAt(std::size_t i) const {
        static_assert(std::is_base_of<CoordinateXY, T>::value, "Must be a Coordinate class");
        assert(sizeof(T) <= sizeof(double) * stride());
        assert(i*stride() < m_vect.size());
        const T* orig = reinterpret_cast<const T*>(&m_vect[i*stride()]);
        return *orig;
    }

    /** \brief
     * Returns a reference to Coordinate at position i.
     */
    template<typename T=Coordinate>
    T& getAt(std::size_t i) {
        static_assert(std::is_base_of<CoordinateXY, T>::value, "Must be a Coordinate class");
        assert(sizeof(T) <= sizeof(double) * stride());
        assert(i*stride() < m_vect.size());
        T* orig = reinterpret_cast<T*>(&m_vect[i*stride()]);
        return *orig;
    }

    /** \brief
     * Write Coordinate at position i to given Coordinate.
     */
    template<typename T>
    void getAt(std::size_t i, T& c) const {
        switch(getCoordinateType()) {
            case CoordinateType::XY: c = getAt<CoordinateXY>(i); break;
            case CoordinateType::XYZ: c = getAt<Coordinate>(i); break;
            case CoordinateType::XYZM: c = getAt<CoordinateXYZM>(i); break;
            case CoordinateType::XYM: c = getAt<CoordinateXYM>(i); break;
            default: getAt<Coordinate>(i);
        }
    }

    void getAt(std::size_t i, CoordinateXY& c) const {
        c = getAt<CoordinateXY>(i);
    }

    // TODO: change to return CoordinateXY
    /**
     * Returns a read-only reference to Coordinate at i
     */
    const Coordinate& operator[](std::size_t i) const
    {
        return getAt(i);
    }

    // TODO: change to return CoordinateXY
    /**
     * Returns a reference to Coordinate at i
     */
    Coordinate&
    operator[](std::size_t i)
    {
        return getAt(i);
    }

    /**
     * Returns the ordinate of a coordinate in this sequence.
     * Ordinate indices 0 and 1 are assumed to be X and Y.
     * Ordinates indices greater than 1 have user-defined semantics
     * (for instance, they may contain other dimensions or measure values).
     *
     * @param index  the coordinate index in the sequence
     * @param ordinateIndex the ordinate index in the coordinate
     *                      (in range [0, dimension-1])
     */
    double getOrdinate(std::size_t index, std::size_t ordinateIndex) const;

    /**
     * Returns ordinate X (0) of the specified coordinate.
     *
     * @param index
     * @return the value of the X ordinate in the index'th coordinate
     */
    double getX(std::size_t index) const
    {
        return m_vect[index * stride()];
    }

    /**
     * Returns ordinate Y (1) of the specified coordinate.
     *
     * @param index
     * @return the value of the Y ordinate in the index'th coordinate
     */
    double getY(std::size_t index) const
    {
        return m_vect[index * stride() + 1];
    }

    /// Return last Coordinate in the sequence
    template<typename T=Coordinate>
    const T& back() const
    {
        return getAt<T>(size() - 1);
    }

    /// Return last Coordinate in the sequence
    template<typename T=Coordinate>
    T& back()
    {
        return getAt<T>(size() - 1);
    }

    /// Return first Coordinate in the sequence
    template<typename T=Coordinate>
    const T& front() const
    {
        return *(reinterpret_cast<const T*>(m_vect.data()));
    }

    /// Return first Coordinate in the sequence
    template<typename T=Coordinate>
    T& front()
    {
        return *(reinterpret_cast<T*>(m_vect.data()));
    }

    /// Pushes all Coordinates of this sequence into the provided vector.
    void toVector(std::vector<Coordinate>& coords) const;

    void toVector(std::vector<CoordinateXY>& coords) const;


    /// @}
    /// \defgroup mutate Mutators
    /// @{

    /// Copy Coordinate c to position pos
    template<typename T>
    void setAt(const T& c, std::size_t pos) {
        switch(getCoordinateType()) {
            case CoordinateType::XY: setAtImpl<CoordinateXY>(c, pos); break;
            case CoordinateType::XYZ: setAtImpl<Coordinate>(c, pos); break;
            case CoordinateType::XYZM: setAtImpl<CoordinateXYZM>(c, pos); break;
            case CoordinateType::XYM: setAtImpl<CoordinateXYM>(c, pos); break;
            default: setAtImpl<Coordinate>(c, pos);
        }
    }

    /**
     * Sets the value for a given ordinate of a coordinate in this sequence.
     *
     * @param index  the coordinate index in the sequence
     * @param ordinateIndex the ordinate index in the coordinate
     *                      (in range [0, dimension-1])
     * @param value  the new ordinate value
     */
    void setOrdinate(std::size_t index, std::size_t ordinateIndex, double value);

    /// Substitute Coordinate list with a copy of the given vector
    void setPoints(const std::vector<Coordinate>& v);

    /// @}
    /// \defgroup add Adding methods
    /// @{

    /// Adds the specified coordinate to the end of the sequence. Dimensions
    /// present in the coordinate but not in the sequence will be ignored.
    /// If multiple coordinates are to be added, a multiple-insert method should
    /// be used for best performance.
    template<typename T=Coordinate>
    void add(const T& c) {
        add(c, size());
    }

    /// Adds the specified coordinate to the end of the sequence. Dimensions
    /// present in the coordinate but not in the sequence will be ignored. If
    /// allowRepeated is false, the coordinate will not be added if it is the
    /// same as the last coordinate in the sequence.
    /// If multiple coordinates are to be added, a multiple-insert method should
    /// be used for best performance.
    template<typename T>
    void add(const T& c, bool allowRepeated)
    {
        if(!allowRepeated && !isEmpty()) {
            const CoordinateXY& last = back<CoordinateXY>();
            if(last.equals2D(c)) {
                return;
            }
        }

        add(c);
    }

    /** \brief
     * Inserts the specified coordinate at the specified position in
     * this sequence. If multiple coordinates are to be added, a multiple-
     * insert method should be used for best performance.
     *
     * @param c the coordinate to insert
     * @param pos the position at which to insert
     */
    template<typename T>
    void add(const T& c, std::size_t pos)
    {
        static_assert(std::is_base_of<CoordinateXY, T>::value, "Must be a Coordinate class");

        // c may be a reference inside m_vect, so we make sure it will not
        // grow before adding it
        if (m_vect.size() + stride() <= m_vect.capacity()) {
            make_space(pos, 1);
            setAt(c, static_cast<std::size_t>(pos));
        } else {
            T tmp{c};
            make_space(pos, 1);
            setAt(tmp, static_cast<std::size_t>(pos));
        }
    }

    /** \brief
     * Inserts the specified coordinate at the specified position in
     * this list.
     *
     * @param i the position at which to insert
     * @param coord the coordinate to insert
     * @param allowRepeated if set to false, repeated coordinates are
     *                      collapsed
     */
    template<typename T>
    void add(std::size_t i, const T& coord, bool allowRepeated)
    {
        // don't add duplicate coordinates
        if(! allowRepeated) {
            std::size_t sz = size();
            if(sz > 0) {
                if(i > 0) {
                    const CoordinateXY& prev = getAt<CoordinateXY>(i - 1);
                    if(prev.equals2D(coord)) {
                        return;
                    }
                }
                if(i < sz) {
                    const CoordinateXY& next = getAt<CoordinateXY>(i);
                    if(next.equals2D(coord)) {
                        return;
                    }
                }
            }
        }

        add(coord, i);
    }

    void add(double x, double y) {
        CoordinateXY c(x, y);
        add(c);
    }

    void add(const CoordinateSequence& cs);

    void add(const CoordinateSequence& cs, bool allowRepeated);

    void add(const CoordinateSequence& cl, bool allowRepeated, bool forwardDirection);

    void add(const CoordinateSequence& cs, std::size_t from, std::size_t to);

    void add(const CoordinateSequence& cs, std::size_t from, std::size_t to, bool allowRepeated);

    template<typename T, typename... Args>
    void add(T begin, T end, Args... args) {
        for (auto it = begin; it != end; ++it) {
            add(*it, args...);
        }
    }

    template<typename T>
    void add(std::size_t i, T from, T to) {
        auto npts = static_cast<std::size_t>(std::distance(from, to));
        make_space(i, npts);

        for (auto it = from; it != to; ++it) {
            setAt(*it, i);
            i++;
        }
    }

    /// @}
    /// \defgroup util Utilities
    /// @{

    void clear() {
        m_vect.clear();
    }

    void reserve(std::size_t capacity) {
        m_vect.reserve(capacity * stride());
    }

    void resize(std::size_t capacity) {
        m_vect.resize(capacity * stride());
    }

    void pop_back();

    /// Get a string representation of CoordinateSequence
    std::string toString() const;

    /// Returns lower-left Coordinate in list
    const CoordinateXY* minCoordinate() const;

    /** \brief
     *  Returns either the given CoordinateSequence if its length
     *  is greater than the given amount, or an empty CoordinateSequence.
     */
    static CoordinateSequence* atLeastNCoordinatesOrNothing(std::size_t n,
            CoordinateSequence* c);

    /// Return position of a Coordinate
    //
    /// or numeric_limits<std::size_t>::max() if not found
    ///
    static std::size_t indexOf(const CoordinateXY* coordinate,
                               const CoordinateSequence* cl);

    /**
     * \brief
     * Returns true if the two arrays are identical, both null,
     * or pointwise equal in two dimensions
     */
    static bool equals(const CoordinateSequence* cl1,
                       const CoordinateSequence* cl2);

    /**
     * \brief
     * Returns true if the two sequences are identical (pointwise
     * equal in all dimensions, with NaN == NaN).
     */
    bool equalsIdentical(const CoordinateSequence& other) const;

    /// Scroll given CoordinateSequence so to start with given Coordinate.
    static void scroll(CoordinateSequence* cl, const CoordinateXY* firstCoordinate);

    /** \brief
     * Determines which orientation of the {@link Coordinate} array
     * is (overall) increasing.
     *
     * In other words, determines which end of the array is "smaller"
     * (using the standard ordering on {@link Coordinate}).
     * Returns an integer indicating the increasing direction.
     * If the sequence is a palindrome, it is defined to be
     * oriented in a positive direction.
     *
     * @param pts the array of Coordinates to test
     * @return <code>1</code> if the array is smaller at the start
     * or is a palindrome,
     * <code>-1</code> if smaller at the end
     *
     * NOTE: this method is found in CoordinateArrays class for JTS
     */
    static int increasingDirection(const CoordinateSequence& pts);

    /// Reverse Coordinate order in given CoordinateSequence
    void reverse();

    void sort();


    /**
     * Expands the given Envelope to include the coordinates in the
     * sequence.
     * @param env the envelope to expand
     */
    void expandEnvelope(Envelope& env) const;

    void closeRing(bool allowRepeated = false);

    /// @}
    /// \defgroup iterate Iteration
    /// @{

    template<typename Filter>
    void apply_rw(const Filter* filter) {
        switch(getCoordinateType()) {
            case CoordinateType::XY:
                for (auto& c : items<CoordinateXY>()) {
                    if (filter->isDone()) break;
                    filter->filter_rw(&c);
                }
                break;
            case CoordinateType::XYZ:
                for (auto& c : items<Coordinate>()) {
                    if (filter->isDone()) break;
                    filter->filter_rw(&c);
                }
                break;
            case CoordinateType::XYM:
                for (auto& c : items<CoordinateXYM>()) {
                    if (filter->isDone()) break;
                    filter->filter_rw(&c);
                }
                break;
            case CoordinateType::XYZM:
                for (auto& c : items<CoordinateXYZM>()) {
                    if (filter->isDone()) break;
                    filter->filter_rw(&c);
                }
                break;
        }
        m_hasdim = m_hasz = false; // re-check (see http://trac.osgeo.org/geos/ticket/435)
    }

    template<typename Filter>
    void apply_ro(Filter* filter) const {
        switch(getCoordinateType()) {
            case CoordinateType::XY:
                for (const auto& c : items<CoordinateXY>()) {
                    if (filter->isDone()) break;
                    filter->filter_ro(&c);
                }
                break;
            case CoordinateType::XYZ:
                for (const auto& c : items<Coordinate>()) {
                    if (filter->isDone()) break;
                    filter->filter_ro(&c);
                }
                break;
            case CoordinateType::XYM:
                for (const auto& c : items<CoordinateXYM>()) {
                    if (filter->isDone()) break;
                    filter->filter_ro(&c);
                }
                break;
            case CoordinateType::XYZM:
                for (const auto& c : items<CoordinateXYZM>()) {
                    if (filter->isDone()) break;
                    filter->filter_ro(&c);
                }
                break;
        }
    }

    template<typename F>
    void forEach(F&& fun) const {
        switch(getCoordinateType()) {
            case CoordinateType::XY:    for (const auto& c : items<CoordinateXY>())   { fun(c); } break;
            case CoordinateType::XYZ:   for (const auto& c : items<Coordinate>())     { fun(c); } break;
            case CoordinateType::XYM:   for (const auto& c : items<CoordinateXYM>())  { fun(c); } break;
            case CoordinateType::XYZM:  for (const auto& c : items<CoordinateXYZM>()) { fun(c); } break;
        }
    }

    template<typename T, typename F>
    void forEach(F&& fun) const
    {
        for (std::size_t i = 0; i < size(); i++) {
            fun(getAt<T>(i));
        }
    }

    template<typename T, typename F>
    void forEach(std::size_t from, std::size_t to, F&& fun) const
    {
        for (std::size_t i = from; i <= to; i++) {
            fun(getAt<T>(i));
        }
    }

    template<typename T>
    class Coordinates {
    public:
        using SequenceType = typename std::conditional<std::is_const<T>::value, const CoordinateSequence, CoordinateSequence>::type;

        explicit Coordinates(SequenceType* seq) : m_seq(seq) {}

        CoordinateSequenceIterator<SequenceType, T> begin() {
            return {m_seq};
        }

        CoordinateSequenceIterator<SequenceType, T> end() {
            return {m_seq, m_seq->getSize()};
        }

        CoordinateSequenceIterator<const SequenceType, typename std::add_const<T>::type>
        begin() const {
            return CoordinateSequenceIterator<const SequenceType, typename std::add_const<T>::type>{m_seq};
        }

        CoordinateSequenceIterator<const SequenceType, typename std::add_const<T>::type>
        end() const {
            return CoordinateSequenceIterator<const SequenceType, typename std::add_const<T>::type>{m_seq, m_seq->getSize()};
        }

        CoordinateSequenceIterator<const SequenceType, typename std::add_const<T>::type>
        cbegin() const {
            return CoordinateSequenceIterator<const SequenceType, typename std::add_const<T>::type>{m_seq};
        }

        CoordinateSequenceIterator<const SequenceType, typename std::add_const<T>::type>
        cend() const {
            return CoordinateSequenceIterator<const SequenceType, typename std::add_const<T>::type>{m_seq, m_seq->getSize()};
        }

    private:
        SequenceType* m_seq;
    };

    template<typename T>
    Coordinates<typename std::add_const<T>::type> items() const {
        return Coordinates<typename std::add_const<T>::type>(this);
    }

    template<typename T>
    Coordinates<T> items() {
        return Coordinates<T>(this);
    }


    /// @}

    double* data() {
        return m_vect.data();
    }

    const double* data() const {
        return m_vect.data();
    }

private:
    std::vector<double> m_vect; // Vector to store values

    uint8_t m_stride;           // Stride of stored values, corresponding to underlying type

    mutable bool m_hasdim;      // Has the dimension of this sequence been determined? Or was it created with no
                                // explicit dimensionality, and we're waiting for getDimension() to be called
                                // after some coordinates have been added?
    mutable bool m_hasz;
    bool m_hasm;

    void initialize();

    template<typename T1, typename T2>
    void setAtImpl(const T2& c, std::size_t pos) {
        auto& orig = getAt<T1>(pos);
        orig = c;
    }

    void make_space(std::size_t pos, std::size_t n) {
        m_vect.insert(std::next(m_vect.begin(), static_cast<std::ptrdiff_t>(pos * stride())),
                      m_stride * n,
                      DoubleNotANumber);
    }

    std::uint8_t stride() const {
        return m_stride;
    }

};

GEOS_DLL std::ostream& operator<< (std::ostream& os, const CoordinateSequence& cs);

GEOS_DLL bool operator== (const CoordinateSequence& s1, const CoordinateSequence& s2);

GEOS_DLL bool operator!= (const CoordinateSequence& s1, const CoordinateSequence& s2);

} // namespace geos::geom
} // namespace geos