# -*- 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
#
#*******************************************************************************
#
"""
Database create settings
"""

import enum
from ctypes import byref, c_void_p, c_uint64, c_uint32, c_uint8, c_char_p

from . import library
from . import utilities
from . import pointattribute
from . import pointattributes
from . import defaultattributes


class CreateSettings:
    """
    Database create settings

    This class defines settings for creating a new point cloud database.
    """

    def __init__(self, context):
        """
        Default constructor

        All values are set to default values.
        """
        from . import pointattribute
        self.context = context
        self.handle = c_void_p(None)
        self.context.check(
            library.handle.rdb_pointcloud_create_settings_new(
                self.context.handle, byref(self.handle)
            )
        )
        self._primary_attribute = pointattribute.PointAttribute(self.context)
        self.context.check(
            library.handle.rdb_pointcloud_create_settings_get_primary_attribute(
                self.context.handle, self.handle, self._primary_attribute.handle
            )
        )

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

    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:

            {
                "primary_attribute": {
                    "name": "riegl.xyz",
                    "title": "XYZ",
                    "description": "Cartesian point coordinates wrt. application coordinate system (0: X, 1: Y, 2: Z)",
                    "unit_symbol": "m",
                    "length": 3,
                    "resolution": 0.00025,
                    "minimum_value": -535000.0,
                    "maximum_value": 535000.0,
                    "default_value": 0.0,
                    "storage_class": "variable",
                    "compression_options": "shuffle",
                    "scale_factor": 1.0
                },
                "chunk_mode": "point_count",
                "chunk_size": 65536,
                "lod_mode": "thinout",
                "chunk_size_lod": 20,
                "cache_size": 524288000,
                "compression_level": 10,
                "optimize_point_id": false
            }
        """
        self.context.check(
            library.handle.rdb_pointcloud_create_settings_json_load(
                self.context.handle, self.handle, utilities.to_rdb_string(json)
            )
        )
        self.context.check(
            library.handle.rdb_pointcloud_create_settings_get_primary_attribute(
                self.context.handle, self.handle, self._primary_attribute.handle
            )
        )

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

    @property
    def primary_attribute(self):
        """
        Primary point attribute

        The primary point attribute defines the attribute that is used
        to sort and index the points. Usually the 3D point coordinates are
        used for that. The primary attribute is automatically added to the
        point cloud (using Pointcloud::attributeAdd()) and cannot be deleted.
        """
        return self._primary_attribute

    @primary_attribute.setter
    def primary_attribute(self, item):
        if isinstance(item, pointattribute.PointAttribute):
            self._primary_attribute.load(item.save())  # copy attribute details
        elif isinstance(item, str):
            try:
                # try to copy from library's list of default attributes
                item = pointattributes.PointAttributes.get_default(item, self.context)
                self._primary_attribute.load(item.save())  # copy attribute details
            except:
                # plan b: copy from our list of default attributes
                keys = defaultattributes.__dict__
                name = [name for name, value in keys.items() if value == item]
                if len(name) != 2:
                    raise RuntimeError("Unknown point attribute: '" + item + "'")
                name = name[0]
                self._primary_attribute.name                = keys[name + "_NAME"]
                self._primary_attribute.title               = keys[name + "_TITLE"]
                self._primary_attribute.description         = keys[name + "_DESCRIPTION"]
                self._primary_attribute.unit_symbol         = keys[name + "_UNIT"]
                self._primary_attribute.length              = keys[name + "_LENGTH"]
                self._primary_attribute.resolution          = keys[name + "_RESOLUTION"]
                self._primary_attribute.minimum_value       = keys[name + "_MINIMUM"]
                self._primary_attribute.maximum_value       = keys[name + "_MAXIMUM"]
                self._primary_attribute.default_value       = keys[name + "_DEFAULT"]
                self._primary_attribute.invalid_value       = keys[name + "_INVALID"]
                self._primary_attribute.storage_class       = keys[name + "_STORAGE_CLASS"]
                self._primary_attribute.compression_options = keys[name + "_COMPRESSION_OPTIONS"]
                self._primary_attribute.lod_settings        = keys[name + "_LOD_SETTINGS"]
                self._primary_attribute.scale_factor        = keys[name + "_SCALE"]

    class ChunkMode(enum.Enum):
        """
        Point chunk mode

        Points are internally organized in chunks (primary point attribute index
        tree leaves). The size of a chunk (in the dimension of the primary point
        attribute) may either be fixed (predefined) or adapted automatically so
        that the number of points in a chunk does not exceed a certain limit. In
        both cases, the "size" is defined by parameter CreateSettings::chunkSize
        and parameter CreateSettings::chunkMode defines the meaning of the value.
        """
        POINT_COUNT = 1  # the chunk size defines the maximum number of points per chunk (the default mode)
        EDGE_LENGTH = 2  # the chunk size defines the edge length of a chunk as 2^N times resolution of the primary point attribute

    @property
    def chunk_mode(self):
        """
        Point chunk mode

        Details see: CreateSettings::ChunkMode

        Default: CreateSettings::ChunkMode::POINT_COUNT
        """
        buffer = c_uint32(0)
        self.context.check(
            library.handle.rdb_pointcloud_create_settings_get_chunk_mode(
                self.context.handle, self.handle, byref(buffer)
            )
        )
        return CreateSettings.ChunkMode(buffer.value)

    @chunk_mode.setter
    def chunk_mode(self, mode):
        buffer = c_uint32(mode)
        self.context.check(
            library.handle.rdb_pointcloud_create_settings_set_chunk_mode(
                self.context.handle, self.handle, buffer
            )
        )

    @property
    def chunk_size(self):
        """
        Point chunk size

        Details see: CreateSettings::ChunkMode

        Default: 65536 points
        """
        buffer = c_uint32(0)
        self.context.check(
            library.handle.rdb_pointcloud_create_settings_get_chunk_size(
                self.context.handle, self.handle, byref(buffer)
            )
        )
        return buffer.value

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

    class LodMode(enum.IntEnum):
        """
        Level of detail mode

        A tree structure is used to sort and organize the point cloud. To create
        a coarse representation of the point cloud (level of detail = "LOD"), a
        number of equally distributed points is extracted from the tree leaf
        nodes and copied to the parent nodes.

        The parameter CreateSettings::chunkSizeLOD defines how many points to
        extract for LOD whereas the meaning of the value and the LOD creation
        algorithm are defined by the parameter CreateSettings::lodMode.
        """
        THINOUT = 1  # the LOD size defines the number of points to copy as a fraction of the total (original) number of points. So if the original point count is for example 19820526 and the size is set to 20%, then the number of LOD points to add is 3964106 (rounded) and the final total number of points is 23784632 (actual value may differ a little bit).
        COMBINE = 2  # the LOD size defines the number of binary subdivisions of the LOD node's volume in each dimension. So if the primary point attribute for example has a length of 2 (2D data) and the LOD size is set to 8, then each LOD node is divided into 2^8 * 2^8 = 2^(8*2) = 2^16 = 65536 sub-volumes. All points of the node's immediate sub-nodes that fall into one of the sub-volumes are merged to a single point and stored in the LOD node. The method to merge the attribute values of a group of points can be defined for each point attribute separately (details see class PointAttribute).

    @property
    def lod_mode(self):
        """
        Level of detail mode

        Details see: CreateSettings::LodMode

        Default: CreateSettings::THINOUT
        """
        buffer = c_uint32(0)
        self.context.check(
            library.handle.rdb_pointcloud_create_settings_get_lod_mode(
                self.context.handle, self.handle, byref(buffer)
            )
        )
        return CreateSettings.LodMode(buffer.value)

    @lod_mode.setter
    def lod_mode(self, mode):
        buffer = c_uint32(mode)
        self.context.check(
            library.handle.rdb_pointcloud_create_settings_set_lod_mode(
                self.context.handle, self.handle, buffer
            )
        )

    @property
    def chunk_size_lod(self):
        """
        Level of detail size

        Details see: CreateSettings::LodMode

        To disable LOD generation, set this parameter to zero (no matter which
        LOD mode is used).

        Default: 20 (i.e. 20% of the original point count).

        Note: In RDB library versions before 2.0.850 this parameter had a
              different meaning. To retain compatibility, the parameter was
              not renamed to e.g. 'overheadLOD' - hence the strange name.
        """
        buffer = c_uint32(0)
        self.context.check(
            library.handle.rdb_pointcloud_create_settings_get_chunk_size_lod(
                self.context.handle, self.handle, byref(buffer)
            )
        )
        return buffer.value

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

    @property
    def cache_size(self):
        """
        Point cache size

        The database engine may buffer read and write operations in an
        internal cache. This value defines the cache size in bytes (octets).

        Default: 500 MB
        """
        buffer = c_uint64(0)
        self.context.check(
            library.handle.rdb_pointcloud_create_settings_get_cache_size_64(
                self.context.handle, self.handle, byref(buffer)
            )
        )
        return buffer.value

    @cache_size.setter
    def cache_size(self, value):
        buffer = c_uint64(value)
        self.context.check(
            library.handle.rdb_pointcloud_create_settings_set_cache_size_64(
                self.context.handle, self.handle, buffer
            )
        )

    @property
    def compression_level(self):
        """
        Data compression level

        The database automatically compresses point data before it is
        stored. The compression level defines the compression quality,
        i.e. higher values produce smaller files.

        Range: 0..100 (i.e. percent)
        Default: 10%
        """
        buffer = c_uint8(0)
        self.context.check(
            library.handle.rdb_pointcloud_create_settings_get_compression_level(
                self.context.handle, self.handle, byref(buffer)
            )
        )
        return buffer.value

    @compression_level.setter
    def compression_level(self, value):
        buffer = c_uint8(value)
        self.context.check(
            library.handle.rdb_pointcloud_create_settings_set_compression_level(
                self.context.handle, self.handle, buffer
            )
        )

    @property
    def optimize_point_id(self):
        """
        Point ID optimization

        Enable this option to apply optimizations to the point ID
        attribute (riegl.id) that can result in smaller files.

        Default: false

        Note: No optimizations are applied when:
              - points were inserted in a previous transaction
              - buffers for the point ID (riegl.id) or dynamic point attributes
                (e.g. "riegl.selected", "riegl.visible") are passed to the insert
                query (QueryInsert class)

        Warning: When optimizations are enabled, the point ID no longer
                 reflects the order in which the points were inserted.
        """
        buffer = c_uint32(0)
        self.context.check(
            library.handle.rdb_pointcloud_create_settings_get_optimize_point_id(
                self.context.handle, self.handle, byref(buffer)
            )
        )
        return buffer.value != 0

    @optimize_point_id.setter
    def optimize_point_id(self, value):
        buffer = c_uint32(1 if value else 0)
        self.context.check(
            library.handle.rdb_pointcloud_create_settings_set_optimize_point_id(
                self.context.handle, self.handle, buffer
            )
        )
