# -*- 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
#
#*******************************************************************************
#
"""
Manage point cloud meta data
"""

from ctypes import POINTER, byref, c_char, c_char_p, c_uint32, c_void_p
from warnings import warn

from . import library
from . import utilities


class PointAttributes:
    """
    Manage point attributes

    This class allows to manage point attributes (dimensions).

    When creating a new database, each point has at least two attributes:

     - an unique point identifier (ID)
     - the primary attribute specified in CreateSettings::primaryAttribute

    Both attributes cannot be deleted or modified in any way.

    The point identifier attribute is always named "id" and is an unsigned 64
    bit integer number (see DataTypes::UINT64, pointIDType() and pointIDName()).
    """

    def __init__(self, pointcloud):
        self.context = pointcloud.context
        self.pointcloud = pointcloud

    def list(self):
        """
        Query attribute names

        This function returns the names of the attributes defined in the
        database in the order defined by the point attribute group table
        (details see group() function).
        """
        list_size = c_uint32(0)
        list_data = POINTER(c_char)()
        self.context.check(
            library.handle.rdb_pointcloud_point_attributes_list(
                self.context.handle,
                self.pointcloud.handle,
                byref(list_size),
                byref(list_data)
            )
        )
        return utilities.to_std_strings(list_data, int(list_size.value))

    @staticmethod
    def list_default(context=None):
        """Return list of built-in default attribute names"""
        if context is None:
            from . import context as ctx
            context = ctx.Context()
        list_size = c_uint32(0)
        list_data = POINTER(c_char)()
        context.check(
            library.handle.rdb_pointcloud_point_attributes_list_default(
                context.handle,
                byref(list_size),
                byref(list_data)
            )
        )
        return utilities.to_std_strings(list_data, int(list_size.value))

    @staticmethod
    def list_filtered(filter, context=None):
        """Query attribute names of filter"""
        if context is None:
            from . import context as ctx
            context = ctx.Context()
        list_size = c_uint32(0)
        list_data = POINTER(c_char)()
        context.check(
            library.handle.rdb_pointcloud_point_attributes_list_filtered(
                context.handle,
                utilities.to_rdb_string(filter),
                byref(list_size),
                byref(list_data)
            )
        )
        return utilities.to_std_strings(list_data, int(list_size.value))

    def exists(self, name):
        """
        Check if attribute exists

        Returns True if an attribute with given name exists

        Args:
            name: attribute name
        """
        result = c_uint32(0)
        self.context.check(
            library.handle.rdb_pointcloud_point_attributes_exists(
                self.context.handle,
                self.pointcloud.handle,
                utilities.to_rdb_string(name),
                byref(result)
            )
        )
        return result.value != 0

    def get(self, name):
        """
        Query attribute details

        If the given attribute name could not be found, the function fails.

        Args:
            name: attribute name (str)
        """
        from . import pointattribute
        result = pointattribute.PointAttribute(self.context)
        self.context.check(
            library.handle.rdb_pointcloud_point_attributes_get(
                self.context.handle,
                self.pointcloud.handle,
                utilities.to_rdb_string(name),
                result.handle
            )
        )
        return result

    def group(self, name):
        """
        Query attribute group and index

        Since RDB version 2.2 the point attributes are grouped and sorted for
        a better overview. The grouping and order is defined by a table which
        is stored in the meta data entry "riegl.point_attribute_groups". Each
        database is filled with a default table with RIEGL point attributes.

        The list() function returns the names of the attributes actually used
        in the order defined by the table. Function group() returns the group
        name as well as the table index of a point attribute (index starts at
        1 for the first attribute in the first group and goes up to N for the
        last attribute in the last group).

        So unless you want to change the order of the point attributes or add
        your own, it is not necessary to access or change the attribute table.
        """
        group = c_char_p(None)
        index = c_uint32(0)
        self.context.check(
            library.handle.rdb_pointcloud_point_attributes_group(
                self.context.handle,
                self.pointcloud.handle,
                utilities.to_rdb_string(name),
                byref(group), byref(index)
            )
        )
        return (utilities.to_std_string(group), index.value)

    @staticmethod
    def get_default(name, context=None):
        """
        Query default attribute details

        This function is similar to get() but instead of returning the attribute
        details defined in the database, it returns the details of the built-in
        RIEGL default point attribute.

        If the given attribute name could not be found, the function fails.

        Args:
            name: attribute name (str)
        """
        if context is None:
            from . import context as ctx
            context = ctx.Context()
        from . import pointattribute
        result = pointattribute.PointAttribute(context)
        context.check(
            library.handle.rdb_pointcloud_point_attributes_get_default(
                context.handle,
                utilities.to_rdb_string(name),
                result.handle
            )
        )
        return result

    @staticmethod
    def group_default(name, context=None):
        """
        Query default attribute group and index

        This function works like group() but returns the group and index from
        the built-in RIEGL default attribute table.
        """
        if context is None:
            from . import context as ctx
            context = ctx.Context()
        group = c_char_p(None)
        index = c_uint32(0)
        context.check(
            library.handle.rdb_pointcloud_point_attributes_group_default(
                context.handle,
                utilities.to_rdb_string(name),
                byref(group), byref(index)
            )
        )
        return (utilities.to_std_string(group), index.value)

    @staticmethod
    def get_merged(pointclouds, name, context=None):
        """
        Merge attribute details

        This function returns the details (description) of a point attribute by
        analyzing two or more point clouds. If the attribute details are not the
        same in all point clouds (e.g. a typo was fixed or the minimum, maximum,
        resolution or default value has changed), merged details that cover all
        variants are returned. However, some changes can not be merged, e.g.
        changes to the vector length, the unit symbol or changes to the minimum,
        maximum and resolution values that would require unsupported data types
        (see riegl::rdb::pointcloud::DataType). In this case an exception with
        error code riegl::rdb::Error::PointAttributeNotMergeable is thrown.
        It is not required that the point attribute exists in all point clouds.

        This function might be helpful when merging point clouds, i.e. when
        reading points from multiple databases and storing them in a single
        database. In this case one needs to define the point attributes in the
        target database. If the source databases were generated by different
        software applications (or different versions of the same application),
        the details of the same attribute may slightly differ (see above), which
        makes the definition of the attribute in the target database quite hard.
        So instead of defining the attributes by using a hard-coded table, one
        can use this function to generate the attribute description at runtime.

        __Examples (simplified, fake attributes):__

          | Example 1      | Attribute   | Description      | Resolution | Minimum | Maximum |
          | -------------- | ----------- | ---------------- |----------: | ------: | ------: |
          | Point cloud A  | amplitude   | signal amplitude |       0.01 |    0.00 |  655.35 |
          | Point cloud B  | amplitude   | signal amplitude |       0.01 | -327.68 |  327.67 |
          | Merged details | amplitude   | signal amplitude |       0.01 | -327.68 |  655.35 |

          | Example 2      | Attribute   | Description         | Resolution | Minimum | Maximum |
          | -------------- | ----------- | ------------------- | ---------: | ------: | ------: |
          | Point cloud A  | temperature | coarse temperature  |       0.50 | -100.00 |  500.00 |
          | Point cloud B  | temperature | precise temperature |       0.01 | -100.00 | 1000.00 |
          | Merged details | temperature | precise temperature |       0.01 | -100.00 | 1000.00 |

        In both examples the value range (minimum/maximum) of the attribute has
        changed, so the result range is the combination (disjunction) of both
        ranges.

        In the second example the resolution is different too. The example
        shows that always the higher resolution (lower value) is returned.

        If the description text changes, then the result is the text that is
        stored in the younger database (the database that was created last).
        In example 2, "Point cloud B" is the younger database.

        Args:
            pointclouds: list of point cloud objects (Pointcloud)
            name: attribute name (str)
        """
        if context is None:
            from . import context as ctx
            context = ctx.Context()
        from . import pointattribute
        result = pointattribute.PointAttribute(context)
        #
        list_size = len(pointclouds)
        list_data = (c_void_p * list_size)()
        for i in range(list_size):
            list_data[i] = pointclouds[i].handle
        #
        context.check(
            library.handle.rdb_pointcloud_point_attributes_get_merged(
                context.handle,
                byref(list_data),
                c_uint32(list_size),
                utilities.to_rdb_string(name),
                result.handle
            )
        )
        return result

    @staticmethod
    def get_merged_all(pointclouds, context=None):
        """
        Same as the get_merged() above, but instead of merging one point
        attribute, this function merges all point attributes of all given point
        clouds and returns a list of the merged point attributes descriptions.
        """
        if context is None:
            from . import context as ctx
            context = ctx.Context()

        # prepare point cloud handle array
        pointcloud_size = len(pointclouds)
        pointcloud_data = (c_void_p * pointcloud_size)()
        for i in range(pointcloud_size):
            pointcloud_data[i] = pointclouds[i].handle

        # query number of attributes
        attribute_size = c_uint32(0)
        context.check(
            library.handle.rdb_pointcloud_point_attributes_get_merged_all(
                context.handle,
                byref(pointcloud_data),
                c_uint32(pointcloud_size),
                0, byref(attribute_size)
            )
        )

        # prepare point attribute data and handle arrays
        attribute_list = list()
        attribute_data = (c_void_p * attribute_size.value)()
        from . import pointattribute
        for i in range(attribute_size.value):
            attribute = pointattribute.PointAttribute(context)
            attribute_data[i] = attribute.handle
            attribute_list.append(attribute)

        # query merged point attributes
        context.check(
            library.handle.rdb_pointcloud_point_attributes_get_merged_all(
                context.handle,
                byref(pointcloud_data),
                c_uint32(pointcloud_size),
                byref(attribute_data),
                byref(attribute_size)
            )
        )
        return attribute_list

    def add(self, attribute):
        """
        Add new attribute

        If 'attribute' is NOT an object of class PointAttribute, then it is
        expected to be a string containing the name of a point attribute.
        If the given name refers to an built-in RIEGL default point attribute,
        this function adds the point attribute to the database. Otherwise this
        function fails.

        Note: All attribute name related functions are case sensitive
              (i.e. "xyz" and "XYZ" are different attributes).

        Note: Attribute names are unique. If an attribute with the same
              name already exists in the database, this function fails.

        Args:
            attribute: attribute to be added
        """
        from . import pointattribute
        if isinstance(attribute, pointattribute.PointAttribute):
            self.context.check(
                library.handle.rdb_pointcloud_point_attributes_add(
                    self.context.handle,
                    self.pointcloud.handle,
                    attribute.handle
                )
            )
        else:  # expecting 'attribute' to be the name of a RIEGL attribute
            self.context.check(
                library.handle.rdb_pointcloud_point_attributes_add_default(
                    self.context.handle,
                    self.pointcloud.handle,
                    utilities.to_rdb_string(attribute)
                )
            )

    def put(self, attribute):
        """
        Modify attribute details

        This function allows to modify certain point attribute properties.
        Thereto the function looks for a point attribute of the given name,
        compares the given properties with those stored in the database and
        finally updates the modified properties in the database.

        Note: Only the modification of `name`, `title`, `description`, `tags`,
              `unitSymbol`, `scaleFactor` and `lodSettings` is supported.
              If a point attribute shall be renamed, the `name` property
              must contain the old and the new name separated by " -> ".
              Example: To rename `riegl.xyz` to `riegl.xyz_socs`, set
              `attribute.name` to `riegl.xyz -> riegl.xyz_socs`.

        Args:
            attribute: attribute to be modified
        """
        self.context.check(
            library.handle.rdb_pointcloud_point_attributes_put(
                self.context.handle,
                self.pointcloud.handle,
                attribute.handle
            )
        )

    def remove(self, name):
        """
        Delete attribute

        If the attribute name could not be found, this function does not fail.

        The primary point attribute (see CreateSettings::primaryAttribute)
        and the unique point identifier (ID) cannot be deleted.

        Args:
            name: name of attribute to be removed
        """
        self.context.check(
            library.handle.rdb_pointcloud_point_attributes_remove(
                self.context.handle,
                self.pointcloud.handle,
                utilities.to_rdb_string(name),
            )
        )

    def duplicate(self, source, target):
        """
        Duplicate attribute data

        Use this function to duplicate the current point attribute *data* (but
        not the point attribute description). This can e.g. be used to make a
        backup of point attribute data.

        Note: The source and target attributes must exist and be compatible
              (i.e. attribute minimum, maximum, resolution and length are
              identical).

        Note: The primary point attribute and the point ID attribute cannot be
              used as target attribute.

        Args:
            target: name of source point attribute (str)
            source: name of target point attribute (str)
        """
        self.context.check(
            library.handle.rdb_pointcloud_point_attributes_duplicate(
                self.context.handle,
                self.pointcloud.handle,
                utilities.to_rdb_string(source),
                utilities.to_rdb_string(target)
            )
        )

    def discard(self, name):
        """
        Discard attribute data

        Use this function to delete the current point attribute *data* (but
        not the point attribute description). This is equivalent to a newly
        created point attribute, i.e. all points will have the default value.

        Note: The primary point attribute (see CreateSettings::primaryAttribute)
              and the unique point identifier (ID) data cannot be discarded.

        Args:
            name: name of point attribute to be discarded
        """
        self.context.check(
            library.handle.rdb_pointcloud_point_attributes_discard(
                self.context.handle,
                self.pointcloud.handle,
                utilities.to_rdb_string(name)
            )
        )

    @property
    def point_id_name(self):
        """Point identifier attribute name"""
        result = c_char_p(None)
        self.context.check(
            library.handle.rdb_pointcloud_point_attributes_point_id_name(
                byref(result)
            )
        )
        return utilities.to_std_string(result)

    @property
    def point_id_unit(self):
        """Point identifier attribute unit"""
        result = c_char_p(None)
        self.context.check(
            library.handle.rdb_pointcloud_point_attributes_point_id_unit(
                byref(result)
            )
        )
        return utilities.to_std_string(result)

    @property
    def primary_attribute_name(self):
        """Name of primary point attribute"""
        result = c_char_p(None)
        # noinspection PyPep8
        self.context.check(
            library.handle.rdb_pointcloud_point_attributes_primary_attribute_name(
                self.context.handle,
                self.pointcloud.handle,
                byref(result)
            )
        )
        return utilities.to_std_string(result)

    def __len__(self):
        return len(self.list())

    def __getattr__(self, key):
        name = key.replace("_", ".", 1)
        warn(str('Consider using point_attributes["{0}"] instead').format(name), DeprecationWarning, stacklevel=2)
        return self.get(name)

    def __getitem__(self, key):
        return self.get(key)

    def __setitem__(self, key, value):
        value.name = key
        self.add(value)

    def __delitem__(self, key):
        self.remove(key)

    def __contains__(self, key):
        return self.exists(key)

    def __iter__(self):
        return self.keys()

    def keys(self):
        """Return iterator for attribute names"""
        return iter(self.list())

    def values(self):
        """Return iterator for attribute details"""

        class ValueIterator:
            def __init__(self, parent):
                self.parent = parent
                self.list = list()

            def __iter__(self):
                self.list = self.parent.list()
                return self

            def __next__(self):
                if len(self.list) > 0:
                    return self.parent.get(self.list.pop(0))
                else:
                    raise StopIteration

        return ValueIterator(self)

    def items(self):
        """Return iterator for attribute name/details tuples"""

        class ItemIterator:
            def __init__(self, parent):
                self.parent = parent
                self.list = list()

            def __iter__(self):
                self.list = self.parent.list()
                return self

            def __next__(self):
                if len(self.list) > 0:
                    name = self.list.pop(0)
                    return name, self.parent.get(name)
                else:
                    raise StopIteration

        return ItemIterator(self)

    def __repr__(self):
        return str(self.list())
