"""
Created on Tue Mar 19 11:09:30 2013

@author: pietro
"""
import grass.lib.vector as libvect

from grass.pygrass.errors import must_be_open

from grass.pygrass.vector.basic import Ilist, BoxList
from grass.pygrass.vector.geometry import read_line, Isle, Area, Node

# For test purposes
test_vector_name = "find_doctest_map"


class AbstractFinder(object):
    def __init__(self, c_mapinfo, table=None, writeable=False):
        """Abstract finder
        -----------------

        Find geometry feature around a point.
        """
        self.c_mapinfo = c_mapinfo
        self.table = table
        self.writeable = writeable
        self.vtype = {
            "point": libvect.GV_POINT,  # 1
            "line": libvect.GV_LINE,  # 2
            "boundary": libvect.GV_BOUNDARY,  # 3
            "centroid": libvect.GV_CENTROID,  # 4
            "all": -1,
        }

    def is_open(self):
        """Check if the vector map is open or not"""
        from . import abstract

        return abstract.is_open(self.c_mapinfo)


class PointFinder(AbstractFinder):
    """Point finder

    This class provides an interface to search geometry features
    of a vector map that are close to a point. The PointFinder class
    is part of a topological vector map object.
    """

    def __init__(self, c_mapinfo, table=None, writeable=False):
        """Find geometry feature(s) around a point.

        :param c_mapinfo: Pointer to the vector layer mapinfo structure
        :type c_mapinfo: ctypes pointer to mapinfo structure

        :param table: Attribute table of the vector layer
        :type table: Class Table from grass.pygrass.table

        :param writable: True or False
        :type writeable: boolean
        """
        super(PointFinder, self).__init__(c_mapinfo, table, writeable)

    @must_be_open
    def node(self, point, maxdist):
        """Find the nearest node around a specific point.

        :param point: The point to search
        :type point: grass.pygrass.vector.geometry.Point

        :param maxdist: The maximum search distance around the point
        :type maxdist: float

        :return: A grass.pygrass.vector.geometry.Node if found or None

        This methods uses libvect.Vect_find_node()()

        Examples:

        >>> from grass.pygrass.vector import VectorTopo
        >>> from grass.pygrass.vector.geometry import Point
        >>> test_vect = VectorTopo(test_vector_name)
        >>> test_vect.open('r')

        # Find nearest node
        >>> points = (Point(10,0), Point(10,4), Point(14,0))
        >>> result = []
        >>> for point in points:
        ...     f = test_vect.find_by_point.node(point=point, maxdist=1)
        ...     if f:
        ...         result.append(f)
        >>> result
        [Node(2), Node(1), Node(6)]

        >>> test_vect.find_by_point.node(point=Point(20,20), maxdist=0)

        >>> test_vect.close()
        """
        node_id = libvect.Vect_find_node(
            self.c_mapinfo,
            point.x,
            point.y,
            point.z if point.z else 0,
            float(maxdist),
            int(not point.is2D),
        )
        if node_id:
            return Node(
                v_id=node_id,
                c_mapinfo=self.c_mapinfo,
                table=self.table,
                writeable=self.writeable,
            )

    @must_be_open
    def geo(self, point, maxdist, type="all", exclude=0):
        """Find the nearest vector feature around a specific point.

        :param point: The point to search
        :type point: grass.pygrass.vector.geometry.Point

        :param maxdist: The maximum search distance around the point
        :type maxdist: float

        :param type: The type of feature to search for
                     Valid type are all the keys in find.vtype dictionary
        :type type: string

        :param exclude: if > 0 number of lines which should be
                        excluded from selection

        :return: A grass.pygrass.vector.geometry.Node if found or None

        This methods uses libvect.Vect_find_line()()

        Examples:

        >>> from grass.pygrass.vector import VectorTopo
        >>> from grass.pygrass.vector.geometry import Point
        >>> test_vect = VectorTopo(test_vector_name)
        >>> test_vect.open('r')

        # Find single features
        >>> points = (Point(10,0), Point(10,6), Point(14,2))
        >>> result = []
        >>> for point in points:
        ...     f = test_vect.find_by_point.geo(point=point, maxdist=1)
        ...     if f:
        ...         result.append(f)
        >>> for f in result:
        ...     print(f.to_wkt_p())    #doctest: +NORMALIZE_WHITESPACE
        LINESTRING(10.000000 4.000000,
                   10.000000 2.000000,
                   10.000000 0.000000)
        POINT(10.000000 6.000000)
        LINESTRING(14.000000 4.000000,
                   14.000000 2.000000,
                   14.000000 0.000000)

        >>> test_vect.find_by_point.geo(point=Point(20,20), maxdist=0)

        >>> test_vect.close()
        """
        feature_id = libvect.Vect_find_line(
            self.c_mapinfo,
            point.x,
            point.y,
            point.z if point.z else 0,
            self.vtype[type],
            float(maxdist),
            int(not point.is2D),
            exclude,
        )
        if feature_id:
            return read_line(feature_id, self.c_mapinfo, self.table, self.writeable)

    @must_be_open
    def geos(self, point, maxdist, type="all", exclude=None):
        """Find the nearest vector features around a specific point.

        :param point: The point to search
        :type point: grass.pygrass.vector.geometry.Point

        :param maxdist: The maximum search distance around the point
        :type maxdist: float

        :param type: The type of feature to search for
                     Valid type are all the keys in find.vtype dictionary
        :type type: string

        :param exclude: if > 0 number of lines which should be
                        excluded from selection

        :return: A list of grass.pygrass.vector.geometry
                 (Line, Point, Boundary, Centroid) if found or None

        This methods uses libvect.Vect_find_line_list()()

        Examples:

        >>> from grass.pygrass.vector import VectorTopo
        >>> from grass.pygrass.vector.geometry import Point
        >>> test_vect = VectorTopo(test_vector_name)
        >>> test_vect.open('r')

        # Find multiple features
        >>> points = (Point(10,0), Point(10,5), Point(14,2))
        >>> result = []
        >>> for point in points:
        ...     f = test_vect.find_by_point.geos(point=point,
        ...                                      maxdist=1.5)
        ...     if f:
        ...         result.append(f)
        >>> for f in result:
        ...     print(f)             #doctest: +NORMALIZE_WHITESPACE
        [Line([Point(10.000000, 4.000000),
               Point(10.000000, 2.000000),
               Point(10.000000, 0.000000)])]
        [Line([Point(10.000000, 4.000000),
               Point(10.000000, 2.000000),
               Point(10.000000, 0.000000)]),
         Point(10.000000, 6.000000)]
        [Line([Point(14.000000, 4.000000),
               Point(14.000000, 2.000000),
               Point(14.000000, 0.000000)])]

        # Find multiple boundaries
        >>> point = Point(3,3)
        >>> result = test_vect.find_by_point.geos(point=Point(3,3),
        ...                                          type="boundary",
        ...                                          maxdist=1.5)
        >>> result                   #doctest: +NORMALIZE_WHITESPACE
        [Boundary([Point(0.000000, 4.000000), Point(4.000000, 4.000000)]),
         Boundary([Point(4.000000, 4.000000), Point(4.000000, 0.000000)]),
         Boundary([Point(1.000000, 1.000000), Point(1.000000, 3.000000),
                   Point(3.000000, 3.000000), Point(3.000000, 1.000000),
                   Point(1.000000, 1.000000)]),
         Boundary([Point(4.000000, 4.000000), Point(6.000000, 4.000000)])]

        # Find multiple centroids
        >>> point = Point(3,3)
        >>> result = test_vect.find_by_point.geos(point=Point(3,3),
        ...                                          type="centroid",
        ...                                          maxdist=1.5)
        >>> result                   #doctest: +NORMALIZE_WHITESPACE
        [Centroid(2.500000, 2.500000),
         Centroid(3.500000, 3.500000)]

        >>> test_vect.find_by_point.geos(point=Point(20,20), maxdist=0)

        >>> test_vect.close()
        """
        excl = Ilist(exclude) if exclude else Ilist([])
        found = Ilist()
        if libvect.Vect_find_line_list(
            self.c_mapinfo,
            point.x,
            point.y,
            point.z if point.z else 0,
            self.vtype[type],
            float(maxdist),
            int(not point.is2D),
            excl.c_ilist,
            found.c_ilist,
        ):
            return [
                read_line(f_id, self.c_mapinfo, self.table, self.writeable)
                for f_id in found
            ]

    @must_be_open
    def area(self, point):
        """Find the nearest area around a specific point.

        :param point: The point to search
        :type point: grass.pygrass.vector.geometry.Point

        :return: A grass.pygrass.vector.geometry.Area if found or None

        This methods uses libvect.Vect_find_area()

        Examples:

        >>> from grass.pygrass.vector import VectorTopo
        >>> from grass.pygrass.vector.geometry import Point
        >>> test_vect = VectorTopo(test_vector_name)
        >>> test_vect.open('r')

        # Find AREAS
        >>> points = (Point(0.5,0.5), Point(5,1), Point(7,1))
        >>> result = []
        >>> for point in points:
        ...     area = test_vect.find_by_point.area(point)
        ...     result.append(area)
        >>> result
        [Area(1), Area(2), Area(4)]
        >>> for area in result:
        ...     print(area.to_wkt())         #doctest: +NORMALIZE_WHITESPACE
        POLYGON ((0.0000000000000000 0.0000000000000000,
                  0.0000000000000000 4.0000000000000000,
                  0.0000000000000000 4.0000000000000000,
                  4.0000000000000000 4.0000000000000000,
                  4.0000000000000000 4.0000000000000000,
                  4.0000000000000000 0.0000000000000000,
                  4.0000000000000000 0.0000000000000000,
                  0.0000000000000000 0.0000000000000000),
                 (1.0000000000000000 1.0000000000000000,
                  3.0000000000000000 1.0000000000000000,
                  3.0000000000000000 3.0000000000000000,
                  1.0000000000000000 3.0000000000000000,
                  1.0000000000000000 1.0000000000000000))
        POLYGON ((4.0000000000000000 0.0000000000000000,
                  4.0000000000000000 4.0000000000000000,
                  4.0000000000000000 4.0000000000000000,
                  6.0000000000000000 4.0000000000000000,
                  6.0000000000000000 4.0000000000000000,
                  6.0000000000000000 0.0000000000000000,
                  6.0000000000000000 0.0000000000000000,
                  4.0000000000000000 0.0000000000000000))
        POLYGON ((6.0000000000000000 0.0000000000000000,
                  6.0000000000000000 4.0000000000000000,
                  6.0000000000000000 4.0000000000000000,
                  8.0000000000000000 4.0000000000000000,
                  8.0000000000000000 4.0000000000000000,
                  8.0000000000000000 0.0000000000000000,
                  8.0000000000000000 0.0000000000000000,
                  6.0000000000000000 0.0000000000000000))

        >>> test_vect.find_by_point.area(Point(20,20))

        >>> test_vect.close()
        """
        area_id = libvect.Vect_find_area(self.c_mapinfo, point.x, point.y)
        if area_id:
            return Area(
                v_id=area_id,
                c_mapinfo=self.c_mapinfo,
                table=self.table,
                writeable=self.writeable,
            )

    @must_be_open
    def island(self, point):
        """Find the nearest island around a specific point.

        :param point: The point to search
        :type point: grass.pygrass.vector.geometry.Point

        :return: A grass.pygrass.vector.geometry.Isle if found or None

        This methods uses Vect_find_island.Vect_find_area()

        Examples:

        >>> from grass.pygrass.vector import VectorTopo
        >>> from grass.pygrass.vector.geometry import Point
        >>> test_vect = VectorTopo(test_vector_name)
        >>> test_vect.open('r')

        # Find ISLANDS
        >>> points = (Point(2,2), Point(5,1))
        >>> result = []
        >>> for point in points:
        ...     area = test_vect.find_by_point.island(point)
        ...     result.append(area)
        >>> result
        [Isle(2), Isle(1)]
        >>> for isle in result:
        ...     print(isle.to_wkt())         #doctest: +NORMALIZE_WHITESPACE
        Polygon((1.000000 1.000000, 3.000000 1.000000,
                 3.000000 3.000000, 1.000000 3.000000, 1.000000 1.000000))
        Polygon((0.000000 4.000000, 0.000000 0.000000, 4.000000 0.000000,
                 6.000000 0.000000, 8.000000 0.000000, 8.000000 4.000000,
                 6.000000 4.000000, 4.000000 4.000000, 0.000000 4.000000))

        >>> test_vect.find_by_point.island(Point(20,20))

        >>> test_vect.close()
        """
        isle_id = libvect.Vect_find_island(self.c_mapinfo, point.x, point.y)
        if isle_id:
            return Isle(
                v_id=isle_id,
                c_mapinfo=self.c_mapinfo,
                table=self.table,
                writeable=self.writeable,
            )


class BboxFinder(AbstractFinder):
    """Bounding Box finder

    This class provides an interface to search geometry features
    of a vector map that are inside or intersect a boundingbox.
    The BboxFinder class is part of a topological vector map object.

    """

    def __init__(self, c_mapinfo, table=None, writeable=False):
        """Find geometry feature(s)that are insider or intersect
        with a boundingbox.

         :param c_mapinfo: Pointer to the vector layer mapinfo structure
         :type c_mapinfo: ctypes pointer to mapinfo structure

         :param table: Attribute table of the vector layer
         :type table: Class Table from grass.pygrass.table

         :param writable: True or False
         :type writeable: boolean
        """
        super(BboxFinder, self).__init__(c_mapinfo, table, writeable)

    @must_be_open
    def geos(self, bbox, type="all", bboxlist_only=False):
        """Find vector features inside a boundingbox.

        :param bbox: The boundingbox to search in
        :type bbox: grass.pygrass.vector.basic.Bbox

        :param type: The type of feature to search for
                     Valid type are all the keys in find.vtype dictionary
        :type type: string

        :param bboxlist_only: If true the BoxList will be returned,
                              no features are generated
        :type bboxlist_only: boolean

        :return: A list of grass.pygrass.vector.geometry
                 (Line, Point, Boundary, Centroid) if found,
                 or None if nothing was found.
                 If bboxlist_only is True a BoxList
                 object will be returned, or None if nothing was found.

        This methods uses libvect.Vect_select_lines_by_box()

        Examples:

        >>> from grass.pygrass.vector import VectorTopo
        >>> from grass.pygrass.vector.basic import Bbox
        >>> test_vect = VectorTopo(test_vector_name)
        >>> test_vect.open('r')

        >>> bbox = Bbox(north=5, south=-1, east=3, west=-1)
        >>> result = test_vect.find_by_bbox.geos(bbox=bbox)
        >>> [bbox for bbox in result] #doctest: +NORMALIZE_WHITESPACE
        [Boundary([Point(4.000000, 0.000000), Point(0.000000, 0.000000)]),
         Boundary([Point(0.000000, 0.000000), Point(0.000000, 4.000000)]),
         Boundary([Point(0.000000, 4.000000), Point(4.000000, 4.000000)]),
         Boundary([Point(1.000000, 1.000000), Point(1.000000, 3.000000),
                   Point(3.000000, 3.000000), Point(3.000000, 1.000000),
                   Point(1.000000, 1.000000)]),
         Centroid(2.500000, 2.500000)]

        >>> bbox = Bbox(north=5, south=-1, east=3, west=-1)
        >>> result = test_vect.find_by_bbox.geos(bbox=bbox,
        ...                                      bboxlist_only=True)
        >>> result                   #doctest: +NORMALIZE_WHITESPACE
        Boxlist([Bbox(0.0, 0.0, 4.0, 0.0),
                 Bbox(4.0, 0.0, 0.0, 0.0),
                 Bbox(4.0, 4.0, 4.0, 0.0),
                 Bbox(3.0, 1.0, 3.0, 1.0),
                 Bbox(2.5, 2.5, 2.5, 2.5)])

        >>> bbox = Bbox(north=7, south=-1, east=15, west=9)
        >>> result = test_vect.find_by_bbox.geos(bbox=bbox)
        >>> [bbox for bbox in result] #doctest: +NORMALIZE_WHITESPACE
        [Line([Point(10.000000, 4.000000), Point(10.000000, 2.000000),
               Point(10.000000, 0.000000)]),
         Point(10.000000, 6.000000),
         Line([Point(12.000000, 4.000000), Point(12.000000, 2.000000),
               Point(12.000000, 0.000000)]),
         Point(12.000000, 6.000000),
         Line([Point(14.000000, 4.000000), Point(14.000000, 2.000000),
               Point(14.000000, 0.000000)]),
         Point(14.000000, 6.000000)]

        >>> bbox = Bbox(north=20, south=18, east=20, west=18)
        >>> test_vect.find_by_bbox.geos(bbox=bbox)

        >>> bbox = Bbox(north=20, south=18, east=20, west=18)
        >>> test_vect.find_by_bbox.geos(bbox=bbox, bboxlist_only=True)

        >>> test_vect.close()
        """
        found = BoxList()
        if libvect.Vect_select_lines_by_box(
            self.c_mapinfo, bbox.c_bbox, self.vtype[type], found.c_boxlist
        ):
            if bboxlist_only:
                return found
            else:
                return (
                    read_line(f_id, self.c_mapinfo, self.table, self.writeable)
                    for f_id in found.ids
                )

    @must_be_open
    def nodes(self, bbox):
        """Find nodes inside a boundingbox.

        :param bbox: The boundingbox to search in
        :type bbox: grass.pygrass.vector.basic.Bbox

        :return: A list of nodes or None if nothing was found

        This methods uses libvect.Vect_select_nodes_by_box()

        Examples:

        >>> from grass.pygrass.vector import VectorTopo
        >>> from grass.pygrass.vector.basic import Bbox
        >>> test_vect = VectorTopo(test_vector_name)
        >>> test_vect.open('r')

        # Find nodes in box
        >>> bbox = Bbox(north=5, south=-1, east=15, west=9)
        >>> result = test_vect.find_by_bbox.nodes(bbox=bbox)
        >>> [node for node in result]
        [Node(2), Node(1), Node(4), Node(3), Node(5), Node(6)]

        >>> bbox = Bbox(north=20, south=18, east=20, west=18)
        >>> test_vect.find_by_bbox.nodes(bbox=bbox)

        >>> test_vect.close()
        """
        found = Ilist()
        if libvect.Vect_select_nodes_by_box(self.c_mapinfo, bbox.c_bbox, found.c_ilist):
            if len(found) > 0:
                return (
                    Node(
                        v_id=n_id,
                        c_mapinfo=self.c_mapinfo,
                        table=self.table,
                        writeable=self.writeable,
                    )
                    for n_id in found
                )

    @must_be_open
    def areas(self, bbox, boxlist=None, bboxlist_only=False):
        """Find areas inside a boundingbox.

        :param bbox: The boundingbox to search in
        :type bbox: grass.pygrass.vector.basic.Bbox

        :param boxlist: An existing BoxList to be filled with
        :type_boxlist: grass.pygrass.vector.basic.BoxList

        :param bboxlist_only: If true the BoxList will be returned,
                              no features are generated
        :type bboxlist_only: boolean

        :return: A list of areas or None if nothing was found

        This methods uses libvect.Vect_select_areas_by_box()

        Examples:

        >>> from grass.pygrass.vector import VectorTopo
        >>> from grass.pygrass.vector.basic import Bbox
        >>> test_vect = VectorTopo(test_vector_name)
        >>> test_vect.open('r')

        # Find areas in box
        >>> bbox = Bbox(north=5, south=-1, east=9, west=-1)
        >>> result = test_vect.find_by_bbox.areas(bbox=bbox)
        >>> [area for area in result]
        [Area(1), Area(2), Area(3), Area(4)]

        >>> bbox = Bbox(north=5, south=-1, east=9, west=-1)
        >>> result = test_vect.find_by_bbox.areas(bbox=bbox,
        ...                                       bboxlist_only=True)
        >>> result                   #doctest: +NORMALIZE_WHITESPACE
        Boxlist([Bbox(4.0, 0.0, 4.0, 0.0),
                 Bbox(4.0, 0.0, 6.0, 4.0),
                 Bbox(3.0, 1.0, 3.0, 1.0),
                 Bbox(4.0, 0.0, 8.0, 6.0)])

        >>> bbox = Bbox(north=20, south=18, east=20, west=18)
        >>> test_vect.find_by_bbox.areas(bbox=bbox)

        >>> test_vect.find_by_bbox.areas(bbox=bbox,
        ...                              bboxlist_only=True)

        >>> test_vect.close()
        """
        boxlist = boxlist if boxlist else BoxList()
        if libvect.Vect_select_areas_by_box(
            self.c_mapinfo, bbox.c_bbox, boxlist.c_boxlist
        ):
            if bboxlist_only:
                return boxlist
            else:
                return (
                    Area(
                        v_id=a_id,
                        c_mapinfo=self.c_mapinfo,
                        table=self.table,
                        writeable=self.writeable,
                    )
                    for a_id in boxlist.ids
                )

    @must_be_open
    def islands(self, bbox, bboxlist_only=False):
        """Find isles inside a boundingbox.

        :param bbox: The boundingbox to search in
        :type bbox: grass.pygrass.vector.basic.Bbox

        :param bboxlist_only: If true the BoxList will be returned,
                              no features are generated
        :type bboxlist_only: boolean

        :return: A list of isles or None if nothing was found

        This methods uses libvect.Vect_select_isles_by_box()

        Examples:

        >>> from grass.pygrass.vector import VectorTopo
        >>> from grass.pygrass.vector.basic import Bbox
        >>> test_vect = VectorTopo(test_vector_name)
        >>> test_vect.open('r')

        # Find isles in box
        >>> bbox = Bbox(north=5, south=-1, east=9, west=-1)
        >>> result = test_vect.find_by_bbox.islands(bbox=bbox)
        >>> [isle for isle in result]
        [Isle(1), Isle(2)]

        >>> bbox = Bbox(north=5, south=-1, east=9, west=-1)
        >>> result = test_vect.find_by_bbox.islands(bbox=bbox,
        ...                                       bboxlist_only=True)
        >>> result                   #doctest: +NORMALIZE_WHITESPACE
        Boxlist([Bbox(4.0, 0.0, 8.0, 0.0),
                 Bbox(3.0, 1.0, 3.0, 1.0)])

        >>> bbox = Bbox(north=20, south=18, east=20, west=18)
        >>> test_vect.find_by_bbox.islands(bbox=bbox)

        >>> test_vect.find_by_bbox.islands(bbox=bbox,
        ...                              bboxlist_only=True)

        >>> test_vect.close()
        """
        found = BoxList()
        if libvect.Vect_select_isles_by_box(
            self.c_mapinfo, bbox.c_bbox, found.c_boxlist
        ):
            if bboxlist_only:
                return found
            else:
                return (
                    Isle(
                        v_id=i_id,
                        c_mapinfo=self.c_mapinfo,
                        table=self.table,
                        writeable=self.writeable,
                    )
                    for i_id in found.ids
                )


class PolygonFinder(AbstractFinder):
    def __init__(self, c_mapinfo, table=None, writeable=False):
        super(PolygonFinder, self).__init__(c_mapinfo, table, writeable)

    def lines(self, polygon, isles=None):
        pass

    def areas(self, polygon, isles=None):
        pass


if __name__ == "__main__":
    import doctest
    from grass.pygrass import utils

    utils.create_test_vector_map(test_vector_name)
    doctest.testmod()

    """Remove the generated vector map, if exist"""
    from grass.pygrass.utils import get_mapset_vector
    from grass.script.core import run_command

    mset = get_mapset_vector(test_vector_name, mapset="")
    if mset:
        run_command("g.remove", flags="f", type="vector", name=test_vector_name)
