# -*- coding: utf-8 -*-
#
#*******************************************************************************
#
#  Copyright 2022 RIEGL Laser Measurement Systems
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.
#
#  SPDX-License-Identifier: Apache-2.0
#
#*******************************************************************************
#
"""
Point select query
"""

from ctypes import byref, c_void_p, c_uint32

from . import library
from . import utilities


class QuerySelect:
    """
    Point select query

    This class can be used to select (read) attributes of existing points.

    Note: You either must delete the query object or call close()
          __before__ the parent Pointcloud instance is closed/deleted!
    """

    def __init__(
        self,
        pointcloud,
        selection=None,
        attributes=None,
        nodes=None,
        chunk_size=100000
    ):
        """
        Constructor

        Creates a query prepared for reading points.
        """
        self.context = pointcloud.context
        self.pointcloud = pointcloud
        self.attributes = attributes
        self.chunk_size = chunk_size
        self.handle = c_void_p(None)

        try:
            if nodes is None:
                node = c_uint32(0)  # = all nodes
                self.context.check(
                    library.handle.rdb_pointcloud_query_select_new(
                        self.context.handle,
                        self.pointcloud.handle,
                        node,
                        utilities.to_rdb_string(selection),
                        byref(self.handle)
                    )
                )
                return
            else:
                list_size = len(nodes)
                if list_size == 0:
                    return
                NodeArrayType = c_uint32 * list_size
                list_data = NodeArrayType(*nodes)
                list_size = c_uint32(list_size)
        except TypeError:  # 'nodes' doesn't seem to be iterable, assume scalar
            if nodes == 0:
                return
            list_data = c_uint32(nodes)
            list_size = c_uint32(1)

        self.context.check(
            library.handle.rdb_pointcloud_query_select_nodes_new(
                self.context.handle,
                self.pointcloud.handle,
                byref(list_data), list_size,
                utilities.to_rdb_string(selection),
                byref(self.handle)
            )
        )

    def __del__(self):
        self.close()

    def __iter__(self):
        return self

    def __next__(self):
        """
        Load next point chunk

        Creates a new point buffer, loads the points into the buffer
        and returns the buffer.
        """
        from . import pointbuffer
        result = self.bind(pointbuffer.PointBuffer(
            self.pointcloud, self.attributes, self.chunk_size
        ))
        result.point_count_total = self.next(self.chunk_size)
        if result.point_count_total > 0:
            result.resize(result.point_count_total)
            return result
        else:
            raise StopIteration()

    def __enter__(self):
        return self

    # noinspection PyUnusedLocal
    def __exit__(self, exc_type, exc_value, traceback):
        self.close()

    @property
    def valid(self):
        """"
        Check if query is not null

        A null query cannot be used to read points.

        Returns True if the query is not null
        """
        return self.handle != c_void_p(None)

    def close(self):
        """
        Finish query

        Call this function when done with reading points.
        """
        if self.valid:
            library.handle.rdb_pointcloud_query_select_delete(
                self.context.handle,
                byref(self.handle)
            )
            self.handle = c_void_p(None)

    def bind(self, buffer):
        """
        Bind attribute buffer

        Use this function to define a target buffer for a point attribute.
        Exactly one buffer can be defined for an attribute (i.e. only the
        most recently defined buffer will be used).
        The buffer is expected to be n*d bytes large, where

        __n__ is the number of points defined in next() and
        __d__ is the number of attribute dimensions (elements).

        Note: This function just stores the buffer pointer - it does
              __NOT__ copy the data contained in the buffer. So make
              sure that the buffer remains valid until you call next().

        Returns the given buffer object
        """
        from . import attributebuffer
        from . import pointbuffer
        if isinstance(buffer, attributebuffer.AttributeBuffer):
            if self.valid:
                self.context.check(
                    library.handle.rdb_pointcloud_query_select_bind(
                        self.context.handle,
                        self.handle,
                        utilities.to_rdb_string(buffer.point_attribute.name),
                        c_uint32(buffer.data_type.value),
                        buffer.raw_data(),
                        0  # no stride
                    )
                )
            return buffer
        elif isinstance(buffer, pointbuffer.PointBuffer):
            for item in buffer.buffers.values():
                self.bind(item)
            return buffer
        else:
            raise TypeError(
                "Buffer must be of type {0} or {1}.".format(
                    "riegl.rdb.attributebuffer.AttributeBuffer",
                    "riegl.rdb.pointbuffer.PointBuffer"
                )
            )

    def next(self, count):
        """
        Select points

        Use this function to actually read the selected points from
        database and copy the attributes to the defined buffers.

        Afterwards you may re-use the buffers or define new buffers
        with bind() and call next() again until all points have been
        read (i.e. next() returns zero).

        If no buffers were bound, calling next() is still valid. This can be
        used for counting points - either all or just those that meet some
        filter criteria. If no filter is specified the total point count is
        equal to what you get with riegl::rdb::Pointcloud::stat().

        Returns the number of points read
        """
        if not self.valid:
            return 0
        count = c_uint32(count)
        result = c_uint32(0)
        self.context.check(
            library.handle.rdb_pointcloud_query_select_next(
                self.context.handle,
                self.handle,
                count,
                byref(result)
            )
        )
        return result.value

    def progress(self):
        """
        Progress

        This function returns a coarse progress information in percent (0-100%).
        Since the total number of returned points is not known in advance, this
        value just reflects the progress of the (internal) index traversal.

        Note: When the select query is used to count points (i.e. no buffers
              were bound), then this function always returns 0%.
        """
        if not self.valid:
            return 0
        result = c_uint32(0)
        self.context.check(
            library.handle.rdb_pointcloud_query_select_progress(
                self.context.handle,
                self.handle,
                byref(result)
            )
        )
        return result.value
