# -*- 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_uint8, c_uint32
from warnings import warn

from . import library
from . import utilities


class MetaData:
    """
    Manage point cloud meta data

    This class allows to manage database-wide properties (aka. "meta-data").
    Arbitrary properties can be stored in key-value manner along with the
    point cloud in the database. This might be used for example to store
    comments, operator/software name or georeferencing information.
    """

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

    def list(self):
        """Return list of property names"""
        list_size = c_uint32(0)
        list_data = POINTER(c_char)()
        self.context.check(
            library.handle.rdb_pointcloud_meta_data_list(
                self.context.handle,
                self.pointcloud.handle,
                byref(list_size),
                byref(list_data)
            )
        )
        return utilities.to_std_strings(list_data, int(list_size.value))

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

        Note: Property names are case sensitive (i.e. "name" and "NAME" are
              different properties).

        Returns True if a property with given name exists

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

    def set(self, name, value):
        """Set property value

        Args:
            name: property name (str)
            value: property value (str)
        """
        self.context.check(
            library.handle.rdb_pointcloud_meta_data_set(
                self.context.handle,
                self.pointcloud.handle,
                utilities.to_rdb_string(name),
                utilities.to_rdb_string(value)
            )
        )

    def get(self, name, default=None):
        """
        Get property value

        If the given property name could not be found, the function returns
        the given default value.

        Args:
            name: property name (str)
            default: default value (str)
        """
        result = c_char_p(None)
        self.context.check(
            library.handle.rdb_pointcloud_meta_data_get(
                self.context.handle,
                self.pointcloud.handle,
                utilities.to_rdb_string(name),
                byref(result),
                utilities.to_rdb_string(default)
            )
        )
        return utilities.to_std_string(result)

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

        If the given property name could not be found, this function doesn't fail.

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

    def validate(self, name_or_value, schema=None):
        """
        Validate value

        If no schema is given (schema=None), then this function validates the
        property given by name in 'name_or_value' against the corresponding
        built-in schema for RIEGL default metadata entries.

        If a schema is given (schema="..."), then this function validates
        the JSON value given in 'name_or_value' against the JSON schema.

        If the value does not correspond to the schema, an exception
        is thrown and the reason can be found in the exception details.

        Args:
            name_or_value: name of property or value to be validated
            schema: schema to be validated against (optional, see above)
        """
        if schema is None:
            self.context.check(
                library.handle.rdb_pointcloud_meta_data_validate(
                    self.context.handle,
                    self.pointcloud.handle,
                    utilities.to_rdb_string(name_or_value)
                )
            )
        else:
            self.context.check(
                library.handle.rdb_pointcloud_meta_data_validate_json(
                    self.context.handle,
                    self.pointcloud.handle,
                    utilities.to_rdb_string(name_or_value),
                    utilities.to_rdb_string(schema)
                )
            )

    def create_signature(self, name, method, key):
        """
        Create a signature for a metadata entry.

        The signature is stored next to the metadata entry in the database file
        and cannot be read out or modified. A transaction must be started first.

        Set 'method' to 0 to delete an existing signature ('key' is ignored in
        this case).

        Args:
            name: name of metadata entry to sign
            method: signature method (0: delete, 1: default)
            key: signature encryption key as hex-string or iterable like list/bytes/bytearray (at least 32 byte)

        Since 2.3.0
        """
        if isinstance(key, str):
            key = bytes.fromhex(key)
        key = (c_uint8 * len(key))(*key)
        self.context.check(
            library.handle.rdb_pointcloud_meta_data_create_signature(
                self.context.handle,
                self.pointcloud.handle,
                utilities.to_rdb_string(name),
                c_uint32(method),
                c_uint32(len(key)),
                byref(key)
            )
        )

    def verify_signature(self, name, method, key):
        """
        Verify the signature of a metadata entry.

        Returns False if:

        - there is no signature for the metadata entry
        - the signature does not match the current value
        - a wrong signature encryption key was given

        Otherwise returns True.

        Set 'method' to 0 to check if a signature exists, no matter
        if it is valid or not ('key' is ignored in this case).

        Args:
            name: name of metadata entry to sign
            method: signature method (0: exists, 1: default)
            key: signature encryption key as hex-string or iterable like list/bytes/bytearray (at least 32 byte)

        Since 2.3.0
        """
        valid = c_uint32(0)
        if isinstance(key, str):
            key = bytes.fromhex(key)
        key = (c_uint8 * len(key))(*key)
        self.context.check(
            library.handle.rdb_pointcloud_meta_data_verify_signature(
                self.context.handle,
                self.pointcloud.handle,
                utilities.to_rdb_string(name),
                c_uint32(method),
                c_uint32(len(key)),
                byref(key),
                byref(valid)
            )
        )
        return valid.value != 0

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

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

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

    def __setitem__(self, key, value):
        self.set(key, 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 property names"""
        return iter(self.list())

    def values(self):
        """Return iterator for property values"""

        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 property name/value 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())
