# -*- 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 attribute description
"""

import enum
import json
from ctypes import byref, c_void_p, c_char_p, c_uint8, c_uint32, c_double

from . import context
from . import datatypes
from . import library
from . import utilities


class PointAttribute:
    """
    Point attribute description

    This class describes a point attribute. The database uses this
    information for internal attribute representation and compression.

    While the name is a unique identifier, the description holds some
    text that client programs might display the user in some way. Also
    the physical unit is not used by the database but can be presented
    to the user (see PointAttribute::unitSymbol).

    To avoid point attribute name conflicts, the names (not the titles)
    shall contain a namespace prefix. Namespace and attribute name are
    separated by a dot (".", e.g. "riegl.xyz"). The default namespace
    "riegl" is used if no namespace is given.

    Remarks:

    If the attribute is a vector (i.e. length > 1), then you might append
    a zero-based vector element index to the attribute name when inserting,
    updating or selecting points. Example: use "rgb[0]" to access the red
    color component (0), the green (1) and blue (2) components are not read
    or modified in this case.

    Furthermore, the minimum, maximum and default values are applied to all
    elements of vectors and the vector length must be in the range [1..100000].

    PointAttribute::defaultValue is returned when reading a point attribute
    that has never been written before. The value must be between minimum and
    maximum (both inclusive).

    PointAttribute::invalidValue may be used to define a value that represents
    an invalid/undefined/unknown value. The value must be between minimum and
    maximum (both inclusive) and must be a multiple of the resolution value.
    The value may be equal to the default value and you may use "NaN" (not a
    number) to signal that there is no "invalid value".

    Note:

    Attribute names may only contain following ASCII characters:

      - a-z
      - A-Z
      - 0-9
      - .
      - _

    Attribute title, description and unit symbol may contain UTF-8 characters.
    """

    def __init__(self, parent=None):
        """
        Default constructor

        All values are set to default values.
        """
        from . import pointcloud
        if isinstance(parent, context.Context):
            self.context = parent
        elif isinstance(parent, pointcloud.Pointcloud):
            self.context = parent.context
        else:
            self.context = None
        if self.context is None:
            self.context = context.Context()

        self.handle = c_void_p(None)
        self.context.check(
            library.handle.rdb_pointcloud_point_attribute_new(
                self.context.handle, byref(self.handle)
            )
        )

        if isinstance(parent, str):
            self.load(parent)
        elif isinstance(parent, dict):
            self.load(json.dumps(parent))

    def __del__(self):
        """Destroy point attribute object"""
        if self.handle != c_void_p(None):
            library.handle.rdb_pointcloud_point_attribute_delete(
                self.context.handle, byref(self.handle)
            )
            self.handle = c_void_p(None)

    # noinspection PyPep8
    def __repr__(self):
        return str(
            "riegl.rdb.pointattribute.PointAttribute("
            "name="               "'{0.name}', "
            "title="              "'{0.title}', "
            "tags="               "'{0.tags}', "
            "description="        "'{0.description}', "
            "unit_symbol="        "'{0.unit_symbol}', "
            "length="              "{0.length}, "
            "resolution="          "{0.resolution}, "
            "minimum_value="       "{0.minimum_value}, "
            "maximum_value="       "{0.maximum_value}, "
            "default_value="       "{0.default_value}, "
            "invalid_value="       "{0.invalid_value}, "
            "named_values="        "{0.named_values}, "
            "storage_class="       "{0.storage_class.name}, "
            "compression_options=" "{0.compression_options.name}, "
            "scale_factor="        "{0.scale_factor}"
            ")"
        ).format(self)

    def __str__(self):
        return str(
            "name:                {0.name}\n"
            "title:               {0.title}\n"
            "tags:                {0.tags}\n"
            "description:         {0.description}\n"
            "unit symbol:         {0.unit_symbol}\n"
            "length:              {0.length}\n"
            "resolution:          {0.resolution}\n"
            "minimum value:       {0.minimum_value}\n"
            "maximum value:       {0.maximum_value}\n"
            "default value:       {0.default_value}\n"
            "invalid value:       {0.invalid_value}\n"
            "named values:        {0.named_values}\n"
            "storage class:       {0.storage_class.name}\n"
            "compression options: {0.compression_options.name}\n"
            "scale factor:        {0.scale_factor}"
        ).format(self)

    def load(self, json):
        """
        Load settings from JSON string

        This function parses the given JSON string and applies all available
        properties - missing properties are silently ignored (i.e. the value
        remains unchanged). When parsing the JSON string fails, an exception
        is thrown.

        Example JSON string:

            {
                "name": "riegl.reflectance",
                "title": "Reflectance",
                "tags": "",
                "description": "Target surface reflectance",
                "unit_symbol": "dB",
                "length": 1,
                "resolution": 0.01,
                "minimum_value": -327.68,
                "maximum_value": 327.67,
                "default_value": 0.0,
                "invalid_value": null,
                "named_values": {},
                "storage_class": "variable",
                "compression_options": "shuffle",
                "lod_settings": "default",
                "scale_factor": 1.0
            }
        """
        buffer = utilities.to_rdb_string(json)
        self.context.check(
            library.handle.rdb_pointcloud_point_attribute_json_load(
                self.context.handle, self.handle, buffer
            )
        )

    def save(self):
        """
        Save settings to JSON string
        See load()
        """
        buffer = c_char_p(None)
        self.context.check(
            library.handle.rdb_pointcloud_point_attribute_json_save(
                self.context.handle, self.handle, byref(buffer)
            )
        )
        return utilities.to_std_string(buffer)

    def data_type(self):
        """
        Get buffer data type

        This function suggests a data type that is suitable to
        construct a buffer for storing values of this attribute.

        The suggestion is based on the resolution, minimumValue
        and maximumValue properties, all others are ignored.
        """
        buffer = c_uint32(0)
        self.context.check(
            library.handle.rdb_pointcloud_point_attribute_data_type(
                self.context.handle, self.handle, byref(buffer)
            )
        )
        return datatypes.DataType(buffer.value)

    @property
    def name(self):
        """Unique attribute name (for queries)"""
        buffer = c_char_p(None)
        self.context.check(
            library.handle.rdb_pointcloud_point_attribute_get_name(
                self.context.handle, self.handle, byref(buffer)
            )
        )
        return utilities.to_std_string(buffer)

    @name.setter
    def name(self, value):
        buffer = utilities.to_rdb_string(value)
        self.context.check(
            library.handle.rdb_pointcloud_point_attribute_set_name(
                self.context.handle, self.handle, buffer
            )
        )

    @property
    def title(self):
        """Attribute title (for display)"""
        buffer = c_char_p(None)
        self.context.check(
            library.handle.rdb_pointcloud_point_attribute_get_title(
                self.context.handle, self.handle, byref(buffer)
            )
        )
        return utilities.to_std_string(buffer)

    @title.setter
    def title(self, value):
        buffer = utilities.to_rdb_string(value)
        self.context.check(
            library.handle.rdb_pointcloud_point_attribute_set_title(
                self.context.handle, self.handle, buffer
            )
        )

    @property
    def tags(self):
        """Attribute tags (comma separated, e.g. "position, transform")"""
        buffer = c_char_p(None)
        self.context.check(
            library.handle.rdb_pointcloud_point_attribute_get_tags(
                self.context.handle, self.handle, byref(buffer)
            )
        )
        return utilities.to_std_string(buffer)

    @tags.setter
    def tags(self, value):
        buffer = utilities.to_rdb_string(value)
        self.context.check(
            library.handle.rdb_pointcloud_point_attribute_set_tags(
                self.context.handle, self.handle, buffer
            )
        )

    @property
    def description(self):
        """Attribute description (for display)"""
        buffer = c_char_p(None)
        self.context.check(
            library.handle.rdb_pointcloud_point_attribute_get_description(
                self.context.handle, self.handle, byref(buffer)
            )
        )
        return utilities.to_std_string(buffer)

    @description.setter
    def description(self, value):
        buffer = utilities.to_rdb_string(value)
        self.context.check(
            library.handle.rdb_pointcloud_point_attribute_set_description(
                self.context.handle, self.handle, buffer
            )
        )

    @property
    def unit_symbol(self):
        """Physical unit symbol (e.g. "m", "rad", "K")"""
        buffer = c_char_p(None)
        self.context.check(
            library.handle.rdb_pointcloud_point_attribute_get_unit_symbol(
                self.context.handle, self.handle, byref(buffer)
            )
        )
        return utilities.to_std_string(buffer)

    @unit_symbol.setter
    def unit_symbol(self, value):
        buffer = utilities.to_rdb_string(value)
        self.context.check(
            library.handle.rdb_pointcloud_point_attribute_set_unit_symbol(
                self.context.handle, self.handle, buffer
            )
        )

    @property
    def length(self):
        """
        number of dimensions/elements

        1: scalar, >1: vector, e.g. 3 for point coordinates
        """
        buffer = c_uint32(0)
        self.context.check(
            library.handle.rdb_pointcloud_point_attribute_get_length_u32(
                self.context.handle, self.handle, byref(buffer)
            )
        )
        return buffer.value

    @length.setter
    def length(self, value):
        buffer = c_uint32(value)
        self.context.check(
            library.handle.rdb_pointcloud_point_attribute_set_length_u32(
                self.context.handle, self.handle, buffer
            )
        )

    @property
    def resolution(self):
        """Expected value resolution"""
        buffer = c_double(0)
        self.context.check(
            library.handle.rdb_pointcloud_point_attribute_get_resolution(
                self.context.handle, self.handle, byref(buffer)
            )
        )
        return buffer.value

    @resolution.setter
    def resolution(self, value):
        buffer = c_double(value)
        self.context.check(
            library.handle.rdb_pointcloud_point_attribute_set_resolution(
                self.context.handle, self.handle, buffer
            )
        )

    @property
    def minimum_value(self):
        """Theoretical minimum value"""
        buffer = c_double(0)
        self.context.check(
            library.handle.rdb_pointcloud_point_attribute_get_minimum_value(
                self.context.handle, self.handle, byref(buffer)
            )
        )
        return buffer.value

    @minimum_value.setter
    def minimum_value(self, value):
        buffer = c_double(value)
        self.context.check(
            library.handle.rdb_pointcloud_point_attribute_set_minimum_value(
                self.context.handle, self.handle, buffer
            )
        )

    @property
    def maximum_value(self):
        """Theoretical maximum value"""
        buffer = c_double(0)
        self.context.check(
            library.handle.rdb_pointcloud_point_attribute_get_maximum_value(
                self.context.handle, self.handle, byref(buffer)
            )
        )
        return buffer.value

    @maximum_value.setter
    def maximum_value(self, value):
        buffer = c_double(value)
        self.context.check(
            library.handle.rdb_pointcloud_point_attribute_set_maximum_value(
                self.context.handle, self.handle, buffer
            )
        )

    @property
    def default_value(self):
        """Default value"""
        buffer = c_double(0)
        self.context.check(
            library.handle.rdb_pointcloud_point_attribute_get_default_value(
                self.context.handle, self.handle, byref(buffer)
            )
        )
        return buffer.value

    @default_value.setter
    def default_value(self, value):
        buffer = c_double(value)
        self.context.check(
            library.handle.rdb_pointcloud_point_attribute_set_default_value(
                self.context.handle, self.handle, buffer
            )
        )

    @property
    def invalid_value(self):
        """Invalid value"""
        buffer = c_double(0)
        self.context.check(
            library.handle.rdb_pointcloud_point_attribute_get_invalid_value(
                self.context.handle, self.handle, byref(buffer)
            )
        )
        return buffer.value

    @invalid_value.setter
    def invalid_value(self, value):
        buffer = c_double(float("nan") if value is None else value)
        self.context.check(
            library.handle.rdb_pointcloud_point_attribute_set_invalid_value(
                self.context.handle, self.handle, buffer
            )
        )

    @property
    def named_values(self):
        """List of VALUE=NAME pairs separated by a single line feed character (LF, ASCII 0x0A)"""
        buffer = c_char_p(None)
        self.context.check(
            library.handle.rdb_pointcloud_point_attribute_get_named_values(
                self.context.handle, self.handle, byref(buffer)
            )
        )
        return utilities.to_std_string(buffer)

    @named_values.setter
    def named_values(self, value):
        buffer = utilities.to_rdb_string(value)
        self.context.check(
            library.handle.rdb_pointcloud_point_attribute_set_named_values(
                self.context.handle, self.handle, buffer
            )
        )

    @property
    def named_values_dict(self):
        """Dictionary of VALUE=NAME pairs"""
        result = dict()
        for line in self.named_values.split("\n"):
            try:
                key, name = line.split("=", 1)
                result[int(key)] = name.strip()
            except:
                pass
        return result

    @named_values_dict.setter
    def named_values_dict(self, value):
        self.named_values = str("\n").join([str(key) + "=" + name for key, name in value.items()])

    class StorageClass(enum.IntEnum):
        CONSTANT = 1  # value cannot be changed
        VARIABLE = 2  # value can change from time to time
        DYNAMIC = 3  # value is likely to be changed often

    class CompressionOptions(enum.IntEnum):
        DEFAULT = 0  # nothing special, just use default compression algorithm
        DELTA = 1  # calculate differences between two consecutive values
        SHUFFLE = 2  # shuffle bytes of point attribute values
        DELTA_SHUFFLE = 3  # calculate differences and shuffle bytes

    @property
    def storage_class(self):
        """
        Storage class

        See class StorageClass
        """
        buffer = c_uint8(0)
        self.context.check(
            library.handle.rdb_pointcloud_point_attribute_get_storage_class(
                self.context.handle, self.handle, byref(buffer)
            )
        )
        return PointAttribute.StorageClass(buffer.value)

    @storage_class.setter
    def storage_class(self, cls):
        buffer = c_uint8(cls)
        self.context.check(
            library.handle.rdb_pointcloud_point_attribute_set_storage_class(
                self.context.handle, self.handle, buffer
            )
        )

    @property
    def compression_options(self):
        """
        Compression options

        See class CompressionOptions
        """
        buffer = c_uint8(0)
        self.context.check(
            library.handle.rdb_pointcloud_point_attribute_get_compression_options(
                self.context.handle, self.handle, byref(buffer)
            )
        )
        return PointAttribute.CompressionOptions(buffer.value)

    @compression_options.setter
    def compression_options(self, opt):
        buffer = c_uint8(opt)
        self.context.check(
            library.handle.rdb_pointcloud_point_attribute_set_compression_options(
                self.context.handle, self.handle, buffer
            )
        )

    @property
    def lod_settings(self):
        """
        Level of detail settings

        This field defines the method to be used to generate level of detail
        data (LOD) for this point attribute. Depending on the LOD mode defined
        during database creation (see CreateSettings::LodMode), several LOD
        methods are available. A list of all methods and their settings can
        be found in file "/manual/riegl_rdb_lod_methods.json" in the RDB SDK.
        """
        buffer = c_char_p(None)
        self.context.check(
            library.handle.rdb_pointcloud_point_attribute_get_lod_settings(
                self.context.handle, self.handle, byref(buffer)
            )
        )
        return utilities.to_std_string(buffer)

    @lod_settings.setter
    def lod_settings(self, value):
        buffer = utilities.to_rdb_string(value)
        self.context.check(
            library.handle.rdb_pointcloud_point_attribute_set_lod_settings(
                self.context.handle, self.handle, buffer
            )
        )

    @property
    def scale_factor(self):
        """
        Optional scale factor applied to real numbers (i.e. resolution not
        equal to 1.0)
          - reading points: resultValue = originalValue * scaleFactor
          - writing points: storedValue = givenValue / scaleFactor
        """
        buffer = c_double(0)
        self.context.check(
            library.handle.rdb_pointcloud_point_attribute_get_scale_factor(
                self.context.handle, self.handle, byref(buffer)
            )
        )
        return buffer.value

    @scale_factor.setter
    def scale_factor(self, value):
        """
        Optional scale factor applied to real numbers (i.e. resolution not
        equal to 1.0)
        """
        buffer = c_double(value)
        self.context.check(
            library.handle.rdb_pointcloud_point_attribute_set_scale_factor(
                self.context.handle, self.handle, buffer
            )
        )

    def suggest_data_type(self):
        """
        Determine best fitting data type for attribute
        Deprecated, use function data_type() instead!
        """
        return self.data_type()
