# -*- 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 fill query
"""

from ctypes import byref, c_void_p, c_uint32

from . import library
from . import utilities


class QueryFill:
    """
    Point fill query

    This class can be used to set attributes of existing points to a (one)
    specific value (e.g. set "riegl.selected" to "1" for all points). This
    query is similar to using select() and update() except that it only accepts
    one value per point attribute and this value is copied to all 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,
        nodes=None
    ):
        """
        Constructor

        Creates a query prepared for modifying points.
        """
        self.context = pointcloud.context
        self.pointcloud = pointcloud
        self.handle = c_void_p(None)
        self.buffers = list()  # used by value()

        try:
            if nodes is None:
                node = c_uint32(0)  # = all nodes
                self.context.check(
                    library.handle.rdb_pointcloud_query_fill_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_fill_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 __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 modifying points.
        """
        if self.valid:
            library.handle.rdb_pointcloud_query_fill_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).

        You can but don't need to define a buffer for each attribute. If
        no buffer is defined for an attribute, it remains unchanged.

        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_fill_bind(
                        self.context.handle,
                        self.handle,
                        utilities.to_rdb_string(buffer.point_attribute.name),
                        c_uint32(buffer.data_type.value),
                        buffer.raw_data()
                    )
                )
            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 value(self, attribute, constant):
        from . import attributebuffer
        buffer = attributebuffer.AttributeBuffer(attribute, 1)
        buffer[0] = constant
        self.buffers.append(buffer)
        self.bind(buffer)

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

        Use this function to actually read the point attributes from
        all defined buffers and update the points in the database.

        Afterwards you may re-fill the buffers or define new buffers
        with bind() and call next() again until all points have been
        updated.

        Note: IEEE-754 "NaN" values contained in floating point source
              buffers are ignored and the attribute's default value is
              used instead. Furthermore IEEE-754 "Infinity" values will
              always cause next() to fail with error code 10414, i.e.
              riegl::rdb::Error::QueryAttributeValueOutOfRange.

        Returns the number of points processed
        """
        if not self.valid:
            return 0
        count = c_uint32(count)
        result = c_uint32(0)
        self.context.check(
            library.handle.rdb_pointcloud_query_fill_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.
        """
        if not self.valid:
            return 0
        result = c_uint32(0)
        self.context.check(
            library.handle.rdb_pointcloud_query_fill_progress(
                self.context.handle,
                self.handle,
                byref(result)
            )
        )
        return result.value
