// Generic Geometry Library
//
// Copyright Barend Gehrels 1995-2009, Geodan Holding B.V. Amsterdam, the Netherlands.
// Copyright Bruno Lalande 2008, 2009
// Use, modification and distribution is subject to the Boost Software License,
// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at
// http://www.boost.org/LICENSE_1_0.txt)

#ifndef GGL_ALGORITHMS_SELECTED_HPP
#define GGL_ALGORITHMS_SELECTED_HPP

#include <cmath>
#include <cstddef>

#include <boost/concept/requires.hpp>
#include <boost/range/functions.hpp>
#include <boost/range/metafunctions.hpp>

#include <ggl/algorithms/within.hpp>
#include <ggl/core/access.hpp>
#include <ggl/core/topological_dimension.hpp>
#include <ggl/core/concepts/nsphere_concept.hpp>
#include <ggl/core/concepts/point_concept.hpp>

/*!
\defgroup selected selection: check if a geometry is "selected" by a point

Checks if one geometry is selected by a point lying within or in the neighborhood of that geometry

\par Geometries:
- POINT: checks if points are CLOSE TO each other (< search_radius)
- LINESTRING: checks if selection point is CLOSE TO linestring (< search_radius)
- RING: checks if selection point is INSIDE the ring, search radius is ignored
- POLYGON: checks if selection point is INSIDE the polygon, but not inside any of its holes

*/

namespace ggl
{

/*!
    \ingroup impl
 */
#ifndef DOXYGEN_NO_DETAIL
namespace detail { namespace selected {

/*!
\details Checks, per dimension, if d[I] not larger than search distance. If true for all
dimensions then returns true. If larger stops immediately and returns false.
Calculate during this process the sum, which is only valid if returning true
*/
template <typename P1, typename P2, typename T, std::size_t D, std::size_t N>
struct differences_loop
{
    static inline bool apply(P1 const& p1, P2 const& p2, T const& distance, T& sum)
    {
        typedef typename select_coordinate_type<P1, P2>::type coordinate_type;

        coordinate_type const c1 = boost::numeric_cast<coordinate_type>(get<D>(p1));
        coordinate_type const c2 = boost::numeric_cast<coordinate_type>(get<D>(p2));

        T const d = std::abs(c1 - c2);
        if (d > distance)
        {
            return false;
        }
        sum += d * d;
        return differences_loop<P1, P2, T, D + 1, N>::apply(p1, p2, distance, sum);
    }
};

template <typename P1, typename P2, typename T, std::size_t N>
struct differences_loop<P1, P2, T, N, N>
{
    static inline bool apply(P1 const&, P2 const&, T const&, T&)
    {
        return true;
    }
};



template <typename S, typename P, typename T, std::size_t D, std::size_t N>
struct outside_loop
{
    static inline bool apply(S const& seg, P const& point, T const& distance)
    {
        typedef typename select_coordinate_type<S, P>::type coordinate_type;

        coordinate_type const v = boost::numeric_cast<coordinate_type>(get<D>(point));
        coordinate_type const s1 = get<0, D>(seg);
        coordinate_type const s2 = get<1, D>(seg);

        // Out of reach if left/bottom or right/top of both points making up the segment
        // I know and currently accept that these comparisons/calculations are done twice per point

        if ((v < s1 - distance && v < s2 - distance) || (v > s1 + distance && v > s2 + distance))
        {
            return true;
        }
        return outside_loop<S, P, T, D + 1, N>::apply(seg, point, distance);
    }
};

template <typename S, typename P, typename T, std::size_t N>
struct outside_loop<S, P, T, N, N>
{
    static inline bool apply(S const&, P const&, T const&)
    {
        return false;
    }
};


template <typename P1, typename P2, typename T>
struct close_to_point
{
    static inline bool apply(P1 const& point, P1 const& selection_point, T const& search_radius)
    {
        assert_dimension_equal<P1, P2>();

        T sum = 0;
        if (differences_loop
                <
                    P1, P2, T, 0, dimension<P1>::type::value
                >::apply(point, selection_point, search_radius, sum))
        {
            return sum <= search_radius * search_radius;
        }

        return false;
    }
};

template <typename S, typename P, typename T>
struct close_to_segment
{
    static inline bool apply(S const& seg, P const& selection_point, T const& search_radius)
    {
        assert_dimension_equal<S, P>();

        if (! outside_loop
                <
                    S, P, T, 0, dimension<P>::type::value
                >::apply(seg, selection_point, search_radius))
        {
            // Not outside, calculate dot product/square distance to segment.
            // Call corresponding strategy
            typedef typename strategy_distance_segment
                <
                    typename cs_tag<P>::type,
                    typename cs_tag<S>::type,
                    P,
                    S
                >::type strategy_type;
            typedef typename strategy_type::return_type return_type;

            strategy_type strategy;
            return_type result = strategy(selection_point, seg);
            return result < search_radius;
        }

        return false;
    }
};

template <typename R, typename P, typename T>
struct close_to_range
{
    static inline bool apply(R const& range, P const& selection_point, T const& search_radius)
    {
        assert_dimension_equal<R, P>();

        std::size_t const n = boost::size(range);
        if (n == 0)
        {
            // Line with zero points, never close
            return false;
        }

        typedef typename point_type<R>::type point_type;
        typedef typename boost::range_const_iterator<R>::type iterator_type;

        iterator_type it = boost::begin(range);
        if (n == 1)
        {
            // Line with one point ==> close to point
            return close_to_point<P, point_type, T>::apply(*it, selection_point, search_radius);
        }

        iterator_type previous = it++;
        while(it != boost::end(range))
        {
            typedef segment<const point_type> segment_type;
            segment_type s(*previous, *it);
            if (close_to_segment<segment_type, P, T>::apply(s, selection_point, search_radius))
            {
                return true;
            }
            previous = it++;
        }

        return false;
    }
};

template <typename Tag, typename G, typename P, typename T>
struct use_within
{
    static inline bool apply(G const& geometry, P const& selection_point, T const& search_radius)
    {
        // Note the reversion, point-in-poly -> first point, then poly
        // Selected-at-point -> first geometry, then point
        return dispatch::within<point_tag, Tag, P, G>::apply(selection_point, geometry);
    }
};

}} // namespace detail::selected
#endif // DOXYGEN_NO_DETAIL


#ifndef DOXYGEN_NO_DISPATCH
namespace dispatch
{

/*!
    \tparam TD topological dimension
 */
template <typename Tag, typename G, std::size_t D, typename P, typename T>
struct selected
{
};

template <typename P1, typename P2, typename T>
struct selected<point_tag, P1, 0, P2, T> : detail::selected::close_to_point<P1, P2, T> { };

// SEGMENT, TODO HERE (close_to_segment)

template <typename L, typename P, typename T>
struct selected<linestring_tag, L, 1, P, T> : detail::selected::close_to_range<L, P, T> { };

template <typename Tag, typename G, typename P, typename T>
struct selected<Tag, G, 2, P, T> : detail::selected::use_within<Tag, G, P, T> { };

} // namespace dispatch
#endif // DOXYGEN_NO_DISPATCH


/*!
    \brief Checks if one geometry is selected by a point lying within or in the neighborhood of that geometry
    \ingroup selected
    \tparam G type of geometry to check
    \tparam P type of point to check
    \tparam T type of search radius
    \param geometry geometry which might be located in the neighborhood
    \param selection_point point to select the geometry
    \param search_radius for points/linestrings: defines radius of "neighborhood" to find things in
    \return true if point is within or close to the other geometry

 */
template<typename G, typename P, typename T>
inline bool selected(G const& geometry, P const& selection_point, T const& search_radius)
{
    typedef dispatch::selected
        <
        typename tag<G>::type,
        G,
        topological_dimension<G>::value,
        P,
        T
        > selector_type;

    return selector_type::apply(geometry, selection_point, search_radius);
}

} // namespace ggl

#endif // GGL_ALGORITHMS_SELECTED_HPP
