# -*- 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
#
#*******************************************************************************
#
"""
Main point cloud database class
"""

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

from . import library
from . import utilities


class Pointcloud:
    """
    Main point cloud database class

    Use this class to create or open a point cloud database and insert, update
    or query points.

    Note: All functions of this class throw an Exception of class
          riegl.rdb.error.Error in case of troubles.
    """

    def __init__(self, context=None):
        """
        Create Pointcloud instance

        This creates a new Pointcloud object instance. To actually access or
        create a point cloud database file, you must call open() or create().

        Each Pointcloud instance may only be accessed from one thread/process at
        a time. Opening the same database using different Pointcloud instances
        (in the same or different thread or process) is allowed.

        Note: The implicit copy-constructor and assignment-operators will yield
              to multiple instances pointing to the same database file. This
              is okay, as long as you do not intend to use both instances in
              different threads simultaneously.

        See: create()
        See: open()
        """
        # create own library context if none given
        if context is not None:
            self.context = context
        else:
            from . import context as ctx
            self.context = ctx.Context()

        # finally create point cloud object
        self.handle = c_void_p(None)
        self.context.check(
            library.handle.rdb_pointcloud_new(
                self.context.handle, byref(self.handle)
            )
        )

    def __del__(self):
        if self.handle != c_void_p(None):
            library.handle.rdb_pointcloud_delete(
                self.context.handle, byref(self.handle)
            )
            self.handle = c_void_p(None)

    def __enter__(self):
        return self

    # noinspection PyUnusedLocal
    def __exit__(self, exc_type, exc_value, traceback):
        self.close()

    def create(self, location, settings=None):
        """
        Create new database

        This function creates a new (empty) database. If the given
        database already exists, it will be overwritten (unless it is
        opened by other clients, in which case the creation will fail).
        The target folder must exist - it is not created automatically.
        If the database could not be created, an exception is thrown.

        Args:
            location: name of database to be created
            settings: optional CreateSettings object
        """
        from . import createsettings
        if settings is None:
            settings = createsettings.CreateSettings(self.context)
        else:
            # noinspection PyProtectedMember
            self.context.check(
                library.handle.
                rdb_pointcloud_create_settings_set_primary_attribute(
                    self.context.handle,
                    settings.handle,
                    settings._primary_attribute.handle
                )
            )

        self.context.check(
            library.handle.rdb_pointcloud_create(
                self.context.handle, self.handle,
                utilities.to_rdb_string(location),
                settings.handle
            )
        )

    def create_by_schema(self, location, settings, schema, optionals=False):
        """
        Create new database

        This function creates a new (empty) database. If the given
        database already exists, it will be overwritten (unless it is
        opened by other clients, in which case the creation will fail).
        The target folder must exist - it is not created automatically.
        If the database could not be created, an exception is thrown.

        Additionally, all required point attributes and metadata entries as defined
        by the schema are added to the database. The schema is given in JSON format,
        details see riegl::rdb::pointcloud::Management::validate(). If 'optionals'
        is 'true', then also the optional point attributes and metadata entries
        are added to the database.

        Note:
          Only RIEGL default point attributes and metadata entries are supported.
          Metadata entries are added but have no value (empty string).

        Note:
          If the schema defines a primary point attribute, then it overrides
          the primary point attribute defined in the 'settings' parameter.

        Args:
            location: name of database to be created
            settings: optional CreateSettings object
            schema: database schema object
            optionals: true: include optional items
        """
        from . import createsettings
        if settings is None:
            settings = createsettings.CreateSettings(self.context)
        else:
            # noinspection PyProtectedMember
            self.context.check(
                library.handle.
                rdb_pointcloud_create_settings_set_primary_attribute(
                    self.context.handle,
                    settings.handle,
                    settings._primary_attribute.handle
                )
            )

        schema = json.dumps(schema)
        optionals = c_uint32(1 if optionals else 0)

        self.context.check(
            library.handle.rdb_pointcloud_create_with_schema(
                self.context.handle, self.handle,
                utilities.to_rdb_string(location),
                settings.handle, schema, optionals
            )
        )

    def open(self, location, settings=None):
        """
        Open existing database

        Open existing database location.
        If the given database does not exist, an exception is thrown.

        Args:
            location: name of database to be opened
            settings: optional OpenSettings object
        """
        from . import opensettings
        if settings is None:
            settings = opensettings.OpenSettings(self.context)

        self.context.check(
            library.handle.rdb_pointcloud_open(
                self.context.handle, self.handle,
                utilities.to_rdb_string(location),
                settings.handle
            )
        )

    def close(self):
        """
        Close database

        Close database file and release all internal resources.
        This function fails if there are pending transactions.
        """
        self.context.check(
            library.handle.rdb_pointcloud_close(
                self.context.handle, self.handle
            )
        )

    @property
    def is_open(self):
        """Return True if a database is open"""
        buffer = c_uint32(0)
        self.context.check(
            library.handle.rdb_pointcloud_is_open(
                self.context.handle, self.handle, byref(buffer)
            )
        )
        return buffer.value == 1

    @property
    def is_empty(self):
        """Return True if a database is empty or no database is open"""
        buffer = c_uint32(0)
        self.context.check(
            library.handle.rdb_pointcloud_is_empty(
                self.context.handle, self.handle, byref(buffer)
            )
        )
        return buffer.value == 1

    def get_uuid(self):
        """
        Returns the Universally Unique Identifier of the database file

        Database files created with rdblib prior version 2.1.2 do
        not have a UUID. In this case the "nil" UUID is returned.
        If no database is opened, an empty string is returned.
        """
        buffer = c_char_p(None)
        self.context.check(
            library.handle.rdb_pointcloud_get_uuid(
                self.context.handle, self.handle, byref(buffer)
            )
        )
        return utilities.to_std_string(buffer)

    @property
    def uuid(self):
        return self.get_uuid()

    def inspect(self, format):
        """
        File statistics and debugging information

        This function returns statistics and debugging information about
        the database file which is intended for factory usage only (i.e.
        the format may change at any time and the content is undocumented).
        """
        buffer = c_char_p(None)
        format = c_uint8(format)
        self.context.check(
            library.handle.rdb_pointcloud_inspect(
                self.context.handle, self.handle, format, byref(buffer)
            )
        )
        return utilities.to_std_string(buffer)

    def clear_cache(self):
        """
        Clear internal data cache

        This function clears (flushes) the internal data cache and reduces
        memory consumption as much as possible.
        """
        self.context.check(
            library.handle.rdb_pointcloud_clear_cache(
                self.context.handle, self.handle
            )
        )

    @property
    def management(self):
        """
        Basic point cloud management interface

        Returns:
            riegl.rdb.management.Management: the management API
        """
        from . import management
        return management.Management(self)

    @property
    def changelog(self):
        """
        Manage point cloud changelog

        Returns:
            riegl.rdb.changelog.Changelog: the changelog API
        """
        from . import changelog
        return changelog.Changelog(self)

    @property
    def meta_data(self):
        """
        Manage point cloud meta data

        Returns:
            riegl.rdb.metadata.MetaData: the meta data API
        """
        from . import metadata
        return metadata.MetaData(self)

    @property
    def point_attributes(self):
        """
        Manage point attributes

        Returns:
            riegl.rdb.pointattributes.PointAttributes: the point attributes API
        """
        from . import pointattributes
        return pointattributes.PointAttributes(self)

    @property
    def transactions(self):
        """
        Manage point cloud transactions

        Returns:
            riegl.rdb.transactions.Transactions: the point cloud transactions API
        """
        from . import transactions
        return transactions.Transactions(self)

    def insert(self):
        """
        Insert points

        This function creates a new query object that can be used to
        insert (new) points into the database.
        """
        from . import queryinsert
        return queryinsert.QueryInsert(self)

    def update(self):
        """
        Update points

        This function creates a new query object that can be used to
        update (modify) attributes of existing points.
        """
        from . import queryupdate
        return queryupdate.QueryUpdate(self)

    def select(
        self, selection=None, attributes=None, node_ids=None, chunk_size=100000
    ):
        """
        Select points

        This function creates a new query object that can be used to
        select (read) attributes of existing points.
        The optional filter expression can be used to select particular
        points - if no filter is given, all points will be loaded.

        __Filter expression operators:__

          | operator | meaning                                  |
          | :------: | ---------------------------------------- |
          |    ==    | equality (left == right)                 |
          |    !=    | inequality (left != right)               |
          |    <     | less than (left < right)                 |
          |    <=    | less than or equal to (left <= right)    |
          |    >     | greater than (left > right)              |
          |    >=    | greater than or equal to (left >= right) |
          |    &&    | conjunction (left && right)              |
          |    \|\|  | disjunction (left \|\| right)            |
          |    %     | unsigned modulo (left % right)           |

        __Filter expression syntax:__

          - _constant_ = _integer_ | _double_ | _variable_ > ':' > ('minimum' | 'maximum' | 'default' | 'invalid')
          - _variable_ = [a-zA-Z] > ([0-9a-zA-Z] | '_' | '.')*
          - _operand_ = _constant_ | _variable_ | _expression_
          - _expression_ = (_operand_)? > _operator_ > _operand_

        __Filter expression examples:__

          - "" (empty string means "all points")
          - "amplitude > 5.0"
          - "(reflectance > 0.0) && (selected == 1)"
          - "point_count != point_count:invalid"
          - "(id % 2) == 0"

        __Stored filters:__

          Filters can also be stored in the metadata as `riegl.stored_filters`.
          All activated filters of this list will be applied in addition to the
          filter specified by the application (conjunction). To temporarily ignore
          (override) all stored filters, insert `"!!"` at the beginning or end of
          the filter string (e.g. `"!! riegl.id == 1"` or `"riegl.id == 1 !!"`).

        Args:
            attributes: list of attribute names to load (None = all)
            selection: point filter (selection) expression (None = all)
            node_ids: ids of graph nodes to load points from (value, list
                      or tuple; None = all)
            chunk_size: number of points to load in one step

        Returns:
            riegl.rdb.queryselect.QuerySelect: query object for reading points
        """
        from . import queryselect
        return queryselect.QuerySelect(
            self, selection, attributes, node_ids, chunk_size
        )

    def points(self, selection=None, attributes=None, node_ids=None):
        """
        Select points point by point

        This function is similar to select() but returns an iterator to
        process each point separately.
        """
        for chunk in self.select(selection, attributes, node_ids):
            for point in chunk:
                yield point

    def fill(self, selection=None, node_ids=None):
        """
        Fill points

        This function creates a new query object that can be used to
        set (modify) attributes of existing points.

        Please have a look at select() for details about the filter expression.
        """
        from . import queryfill
        return queryfill.QueryFill(self, selection, node_ids)

    def invert(self, selection=None, node_ids=None):
        """
        Invert points

        This function creates a new query object that can be used to
        invert attributes of existing points.

        Please have a look at select() for details about the filter expression.
        """
        from . import queryinvert
        return queryinvert.QueryInvert(self, selection, node_ids)

    def remove(self):
        """
        Remove points

        This function creates a new query object that can be used to
        remove (delete) existing points.
        """
        from . import queryremove
        return queryremove.QueryRemove(self)

    @property
    def stat(self):
        """
        Query point statistics

        This function creates a new query object that can be used to
        get point attribute statistics like minimum and maximum value.

        """
        from . import querystat
        return querystat.QueryStat(self)


def rdb_open(location, settings=None, context=None):
    """Open existing RDB database and return point cloud object

    Args:
        location: name of database to be opened
        settings: optional OpenSettings object
        context: optional library context

    Returns:
        riegl.rdb.pointcloud.Pointcloud: the opened point cloud instance
    """
    result = Pointcloud(context)
    result.open(location, settings)
    return result


def rdb_create(location, settings=None, context=None):
    """Create new RDB database and return point cloud object

    Args:
        location: name of database to be created
        settings: optional CreateSettings object
        context: optional library context

    Returns:
        riegl.rdb.pointcloud.Pointcloud: the created point cloud instance
    """
    result = Pointcloud(context)
    result.create(location, settings)
    return result
