# -*- 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 transactions
"""

import array
import logging
from ctypes import (
    CFUNCTYPE, POINTER, byref, c_void_p, c_char_p, c_uint64, c_uint32, c_uint8
)

from . import library
from . import utilities


class TransactionDetails:
    """
    Point cloud transaction details
    """

    def __init__(self):
        self.id = None
        """transaction number (TAN)"""
        self.rdb = None
        """RDB library version string"""
        self.title = None
        """short description, e.g. 'Import'"""
        self.agent = None
        """software name, e.g. 'rdbimport v1.0'"""
        self.comments = None
        """e.g. process details for humans"""
        self.settings = None
        """e.g. process settings for software"""
        self.start = None
        """start time as 'yyyy-mm-dd hh:mm:ss.zzz',
        e.g. '2015-10-14 13:48:35.801' (local time)"""
        self.stop = None
        """stop  time as 'yyyy-mm-dd hh:mm:ss.zzz',
        e.g. '2015-10-14 13:48:35.801' (local time)"""

    # noinspection PyPep8
    def __repr__(self):
        return str(
            "riegl.rdb.transactions.TransactionDetails("
            "id="        "{0.id}, "
            "rdb="      "'{0.rdb}', "
            "title="    "'{0.title}', "
            "agent="    "'{0.agent}', "
            "comments=" "'{0.comments}', "
            "settings=" "'{0.settings}', "
            "start="    "'{0.start}', "
            "stop="     "'{0.stop}'"
            ")"
        ).format(self)

    def __str__(self):
        return str(
            "id:       {0.id}\n"
            "rdb:      {0.rdb}\n"
            "title:    {0.title}\n"
            "agent:    {0.agent}\n"
            "comments: {0.comments}\n"
            "settings: {0.settings}\n"
            "start:    {0.start}\n"
            "stop:     {0.stop}"
        ).format(self)


class Transaction:
    """
    Transaction context

    This class implements a context manager that begins a new transaction
    and automatically reverts (!) the transaction when the context is left.
    To make your changes persistent, call 'commit()' on the transaction
    context object.
    """

    def __init__(self, pointcloud, title, agent, comments=None, settings=None):
        self.transaction = None
        self.transactions = pointcloud.transactions
        self.title = title
        self.agent = agent
        self.comments = comments
        self.settings = settings

    def __enter__(self):
        return self.begin()

    # noinspection PyUnusedLocal
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.rollback()

    def begin(self):
        if self.transaction is None:
            self.transaction = self.transactions.begin(
                self.title, self.agent, self.comments, self.settings
            )
        return self

    def commit(self, progress=None, signature=0, key=None):
        if self.transaction is not None:
            self.transactions.commit(progress, signature, key)
            self.transaction = None

    def rollback(self):
        if self.transaction is not None:
            self.transactions.rollback()
            self.transaction = None


class Transactions:
    """
    Manage point cloud transactions

    Modifying the point cloud database means to execute a transaction.

    This class allows to start new transactions (see begin()) and to
    browse past (already executed) transactions.
    """

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

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

    def __getitem__(self, item):
        return self.details(item)

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

    def keys(self):
        """Return iterator for transaction IDs"""
        return iter(self.list())

    def values(self):
        """Return iterator for transaction 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.details(self.list.pop(0))
                else:
                    raise StopIteration

        return ValueIterator(self)

    def begin(self, title, agent, comments=None, settings=None):
        """
        Create new transaction

        Whenever you are going to modify the point cloud, you must create a
        transaction by using this function. Without a transaction, any function
        that modifies the point cloud will fail (i.e. it throws an exception).

        A transaction automatically locks the database so that other users
        can query data but can __NOT__ add or modify data. Please note that
        only __one transaction at a time__ is allowed. If there is already
        a transaction pending, this function will raise an exception (see
        Error::TransactionPending).

        To finish a transaction either call commit() or rollback().

        Note: Parameters 'title' and 'agent' are required - i.e. creating
              a new transaction will fail if empty strings are provided.
              All other parameters are optional.

        Note: Parameters 'comments' and 'settings' serve the user and client
              application for information, e.g. to repeat processing steps
              later on. They have no influence on the transaction itself. The
              client application is free to store information in any format
              that is string-compatible (e.g. plain text, INI, XML, JSON).

        Note: The total size of the 'title', 'agent', 'comments' and 'settings'
              strings must not exceed 500 MB (bytes, not characters).
        """
        result = c_uint32(0)
        self.context.check(
            library.handle.rdb_pointcloud_transaction_begin(
                self.context.handle,
                self.pointcloud.handle,
                utilities.to_rdb_string(title),
                utilities.to_rdb_string(agent),
                utilities.to_rdb_string(comments),
                utilities.to_rdb_string(settings),
                byref(result)
            )
        )
        return result.value

    def commit(self, progress=None, signature=0, key=None):
        """
        Commit current transaction

        This function commits the current transaction. All changes made by the
        transaction become visible to others.

        The optional parameter 'progress' can be used to define a callable
        object (function, function-object or lambda-function). This function
        has one parameter which is the commit progress in percent (0..100%)
        and is called on a regular basis by the commit() function.

        The optional parameters 'signature' and 'key' can be used to create a
        signature for the transaction, where 'signature' is the method to use
        (0: none, 1: default) and 'key' is the signature encryption key as
        hex-string or iterable like list/bytes/bytearray (at least 32 byte).
        """

        progress_pointer = c_void_p(0)
        if progress is not None:

            # noinspection PyUnusedLocal
            def progress_wrapper(progress_value, userdata_value):
                try:
                    progress(progress_value)
                except Exception as error:
                    logger = logging.getLogger("riegl.rdb")
                    logger.warning("Commit progress callback failed: %s", error)

            progress_pointer = CFUNCTYPE(None, c_uint8, c_void_p)(
                progress_wrapper
            )

        key_size = 0
        key_data = c_void_p(0)
        if key is not None:
            if isinstance(key, str):
                key = bytes.fromhex(key)
            key_data = (c_uint8 * len(key))(*key)
            key_size = len(key_data)
            key_data = byref(key_data)

        self.context.check(
            library.handle.rdb_pointcloud_transaction_commit_with_signature(
                self.context.handle, self.pointcloud.handle,
                progress_pointer, c_void_p(0),  # not used
                c_uint32(signature),
                c_uint32(key_size),
                key_data
            )
        )

    def rollback(self):
        """
        Abort current transaction

        This function rolls back the current transaction and causes all changes
        made by the transaction to be discarded.
        """
        self.context.check(
            library.handle.rdb_pointcloud_transaction_rollback(
                self.context.handle, self.pointcloud.handle,
            )
        )

    def list(self):
        """
        Get list of transactions

        The database keeps a log (journal) of all transactions. This function
        returns a list of identifiers (IDs) for all recorded transactions.
        """
        list_size = c_uint32(0)
        self.context.check(
            library.handle.rdb_pointcloud_transaction_list(
                self.context.handle,
                self.pointcloud.handle,
                byref(list_size),
                POINTER(c_uint32)()  # null pointer = query array size
            )
        )
        list_type = c_uint32 * list_size.value
        list_data = list_type()
        self.context.check(
            library.handle.rdb_pointcloud_transaction_list(
                self.context.handle,
                self.pointcloud.handle,
                byref(list_size),
                byref(list_data)
            )
        )
        return list(list_data)

    def current(self):
        """
        Current transaction

        This function returns the details of the current transaction.
        Usually this is the transaction that was committed at last. However,
        if restore() is used to return to a previous database state, the
        current transaction is the restored transaction.

        Note: This function does not return details of a pending transaction.
        """
        result = c_uint32(0)
        self.context.check(
            library.handle.rdb_pointcloud_transaction_current(
                self.context.handle,
                self.pointcloud.handle,
                byref(result)
            )
        )
        return self.details(result.value)

    def pending(self):
        """
        Check if transaction is pending

        This function checks whether there is a pending transaction that was
        started by __this__ database instance. This function does __not__ check
        whether a transaction was started by a different database instance of
        the current or different thread or process.
        """
        result = c_uint32(0)
        self.context.check(
            library.handle.rdb_pointcloud_transaction_pending(
                self.context.handle,
                self.pointcloud.handle,
                byref(result)
            )
        )
        return result.value == 1

    def details(self, transaction_id):
        """
        Query transaction details

        Returns:
            object of type riegl.rdb.transactions.TransactionDetails
        """
        result_handle = c_void_p(None)
        result_id = c_uint32(transaction_id)
        result_rdb = c_char_p(None)
        result_title = c_char_p(None)
        result_agent = c_char_p(None)
        result_comments = c_char_p(None)
        result_settings = c_char_p(None)
        result_start = c_char_p(None)
        result_stop = c_char_p(None)

        self.context.check(
            library.handle.rdb_pointcloud_transaction_new(
                self.context.handle, byref(result_handle)
            )
        )
        self.context.check(
            library.handle.rdb_pointcloud_transaction_details(
                self.context.handle, self.pointcloud.handle,
                result_id, result_handle
            )
        )
        self.context.check(
            library.handle.rdb_pointcloud_transaction_get_id(
                self.context.handle, result_handle, byref(result_id)
            )
        )
        self.context.check(
            library.handle.rdb_pointcloud_transaction_get_rdb(
                self.context.handle, result_handle, byref(result_rdb)
            )
        )
        self.context.check(
            library.handle.rdb_pointcloud_transaction_get_title(
                self.context.handle, result_handle, byref(result_title)
            )
        )
        self.context.check(
            library.handle.rdb_pointcloud_transaction_get_agent(
                self.context.handle, result_handle, byref(result_agent)
            )
        )
        self.context.check(
            library.handle.rdb_pointcloud_transaction_get_comments(
                self.context.handle, result_handle, byref(result_comments)
            )
        )
        self.context.check(
            library.handle.rdb_pointcloud_transaction_get_settings(
                self.context.handle, result_handle, byref(result_settings)
            )
        )
        self.context.check(
            library.handle.rdb_pointcloud_transaction_get_start(
                self.context.handle, result_handle, byref(result_start)
            )
        )
        self.context.check(
            library.handle.rdb_pointcloud_transaction_get_stop(
                self.context.handle, result_handle, byref(result_stop)
            )
        )
        self.context.check(
            library.handle.rdb_pointcloud_transaction_delete(
                self.context.handle, byref(result_handle)
            )
        )
        result = TransactionDetails()
        result.id = result_id.value
        result.rdb = utilities.to_std_string(result_rdb)
        result.title = utilities.to_std_string(result_title)
        result.agent = utilities.to_std_string(result_agent)
        result.comments = utilities.to_std_string(result_comments)
        result.settings = utilities.to_std_string(result_settings)
        result.start = utilities.to_std_string(result_start)
        result.stop = utilities.to_std_string(result_stop)
        return result

    def restore(self, transaction_id):
        """
        Restore database state

        This function restores the database state that was current at the end
        of the transaction. Those transactions that were created after the
        restored transaction remain valid until a new transaction is created.
        This offers some simple undo/redo functionality.
        """
        transaction_id = c_uint32(transaction_id)
        self.context.check(
            library.handle.rdb_pointcloud_transaction_restore(
                self.context.handle, self.pointcloud.handle, transaction_id
            )
        )

    def discard(self, transaction_id):
        """
        Discard transaction data

        This function deletes the given transaction(s). Please note that this
        operation only removes the transaction(s) from the database history and
        releases the related data blocks in the database file so that they can
        be re-used by subsequent transactions. However the database file size
        will not decrease unless you call vacuum().

        Note: The first (database creation) and the current transaction
              (last committed or restored) can not be deleted.

        See: riegl::rdb::pointcloud::Management::finalize()
             riegl::rdb::pointcloud::Management::vacuum()
        """
        try:
            cnt = len(transaction_id)
            if cnt == 0:
                return
            ArrayType = c_uint32 * cnt
            tid = ArrayType(*transaction_id)
            cnt = c_uint32(cnt)
        except TypeError:  # doesn't seem to be iterable, assume scalar
            tid = c_uint32(transaction_id)
            cnt = c_uint32(1)
        self.context.check(
            library.handle.rdb_pointcloud_transaction_discard(
                self.context.handle, self.pointcloud.handle, cnt, byref(tid)
            )
        )

    def size(self, transaction_id):
        """
        Estimate database size

        This function returns the size (in bytes) of all data of the specified
        transactions, which is approximately the file size of a database that
        contains only these transactions (and no gaps).

        Note: Data blocks may be shared among multiple transactions and size()
              returns the total size of the union of all used data blocks. That
              is why size(1) + size(2) is not necessarily equal to size(1, 2).

        See: riegl::rdb::pointcloud::Transactions::discard()
             riegl::rdb::pointcloud::Management::finalize()
             riegl::rdb::pointcloud::Management::vacuum()
        """
        try:
            cnt = len(transaction_id)
            if cnt == 0:
                return 0
            ArrayType = c_uint32 * cnt
            tid = ArrayType(*transaction_id)
            cnt = c_uint32(cnt)
        except TypeError:  # doesn't seem to be iterable, assume scalar
            tid = c_uint32(transaction_id)
            cnt = c_uint32(1)
        result = c_uint64(0)
        self.context.check(
            library.handle.rdb_pointcloud_transaction_size(
                self.context.handle, self.pointcloud.handle, cnt, byref(tid), byref(result)
            )
        )
        return result.value
