#
##
##  SPDX-FileCopyrightText: © 2007-2023 Benedict Verhegghe <bverheg@gmail.com>
##  SPDX-License-Identifier: GPL-3.0-or-later
##
##  This file is part of pyFormex 3.4  (Thu Nov 16 18:07:39 CET 2023)
##  pyFormex is a tool for generating, manipulating and transforming 3D
##  geometrical models by sequences of mathematical operations.
##  Home page: https://pyformex.org
##  Project page: https://savannah.nongnu.org/projects/pyformex/
##  Development: https://gitlab.com/bverheg/pyformex
##  Distributed under the GNU General Public License version 3 or later.
##
##  This program is free software: you can redistribute it and/or modify
##  it under the terms of the GNU General Public License as published by
##  the Free Software Foundation, either version 3 of the License, or
##  (at your option) any later version.
##
##  This program is distributed in the hope that it will be useful,
##  but WITHOUT ANY WARRANTY; without even the implied warranty of
##  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
##  GNU General Public License for more details.
##
##  You should have received a copy of the GNU General Public License
##  along with this program.  If not, see http://www.gnu.org/licenses/.
##
"""Tools for handling collections of elements belonging to multiple parts.
This module defines the Collection class.
"""
from copy import deepcopy
import numpy as np
import pyformex as pf
################# Collection of Actors or Actor Elements ###############
[docs]def uniq(data, keep_order):
    """Uniqify data, keeping order if needed"""
    if keep_order:
        return np.asarray(list(dict.fromkeys(data)))
    else:
        return np.unique(data) 
[docs]def uniq_add(a, b, keep_order):
    """Add to a uniq array. a is supposed to be uniq, not checked!"""
    if keep_order:
        return np.concatenate([a, [i for i in b if i not in a]])
    else:
        return np.union1d(a, b) 
[docs]def uniq_remove(a, b, keep_order):
    """Remove from a uniq array. a is supposed to be uniq, not checked!"""
    if keep_order:
        return np.asarray([i for i in a if i not in b])
    else:
        return np.setdiff1d(a, b) 
[docs]class Collection:
    """A collection  is a set of (int,int) tuples.
    The first part of the tuple has a limited number of values and are used
    as the keys in a dict.
    The second part can have a lot of different values and is implemented
    as an integer array with unique values.
    This is e.g. used to identify a set of individual parts of one or more
    OpenGL actors.
    Examples
    --------
    >>> a = Collection()
    >>> a.add(range(7),3)
    >>> a.add(range(4))
    >>> a.remove([2,4],3)
    >>> print(a)
    -1 [0 1 2 3]; 3 [0 1 3 5 6];
    >>> a.add([[2,0],[2,3],[-1,7],[3,88]])
    >>> print(a)
    -1 [0 1 2 3 7]; 2 [0 3]; 3 [ 0  1  3  5  6 88];
    >>> print(a.total())
    13
    >>> a[2] = [1,2,3]
    >>> print(a)
    -1 [0 1 2 3 7]; 2 [1 2 3]; 3 [ 0  1  3  5  6 88];
    >>> a[2] = []
    >>> print(a)
    -1 [0 1 2 3 7]; 3 [ 0  1  3  5  6 88];
    >>> a.set([[2,0],[2,3],[-1,7],[3,88]])
    >>> print(a)
    -1 [7]; 2 [0 3]; 3 [88];
    >>> print(a.keys())
    [-1  2  3]
    >>> for k, i in a.singles(): print(k,i)
    -1 7
    2 0
    2 3
    3 88
    >>> print([k for k in a])
    [-1, 2, 3]
    >>> 2 in a
    True
    >>> del a[2]
    >>> 2 in a
    False
    """
    def __init__(self, obj_type=None, keep_order=False):
        self.d = {}
        self.obj_type = obj_type
        self.keep_order = keep_order
    def copy(self):
        return deepcopy(self)
    def __len__(self):
        return len(self.d)
    def total(self):
        return sum([len(v) for v in self.d.values()])
    def clear(self, keys=[]):
        if keys:
            for k in keys:
                k = int(k)
                if k in self.d:
                    del self.d[k]
        else:
            self.d = {}
[docs]    def add(self, data, key=-1):
        """Add new data to a Collection.
        Parameters
        ----------
        data: :term:`array_like` | Collection
            An int array with shape (ndata, 2) or (ndata,), or another
            Collection with the same obj_type as self. If an array
            with shape (ndata, 2), each row has a key value in column 0
            and a data value in column 1. If an (ndata,) shaped array,
            all items have the same key and it has to be specified separately.
        key: int
            The key value if data is an (ndata,) shaped array. Else ignored.
        keep_order
        separately, or a default value will be used.
        data can also be another Collection, if it has the same object
        type.
        """
        if len(data) == 0:
            return
        if isinstance(data, Collection):
            if data.obj_type == self.obj_type:
                for k in data.d:
                    self.add(data.d[k], k)
                return
            else:
                raise ValueError(
                    "Cannot add Collections with different object type")
        data = np.asarray(data)
        if data.ndim == 2:
            for key in np.unique(data[:, 0]):
                self.add(data[data[:, 0] == key, 1], key)
        else:
            if key in self.d:
                self.d[key] = uniq_add(self.d[key], data, self.keep_order)
            elif data.size > 0:
                self.d[key] = uniq(data, self.keep_order) 
[docs]    def set(self, data, key=-1):
        """Set the collection to the specified data.
        This is equivalent to clearing the corresponding keys
        before adding.
        """
        self.clear()
        self.add(data, key) 
[docs]    def remove(self, data, key=-1):
        """Remove data from the collection."""
        if isinstance(data, Collection):
            if data.obj_type == self.obj_type:
                for k in data.d:
                    self.remove(data.d[k], k)
                return
            else:
                raise ValueError(
                    "Cannot remove Collections with different object type")
        data = np.asarray(data)
        if data.ndim == 2:
            for key in np.unique(data[:, 0]):
                self.remove(data[data[:, 0] == key, 1], key)
        else:
            key = int(key)
            if key in self.d:
                data = uniq_remove(self.d[key], data, self.keep_order)
                if data.size > 0:
                    self.d[key] = data
                else:
                    del self.d[key]
            else:
                pf.debug(f"Not removing from non-existing selection "
                         f"for actor {key}", pf.DEBUG.DRAW) 
    def __contains__(self, key):
        """Check whether the collection has an entry for the key.
         This inplements the 'in' operator for a Collection.
         """
        return key in self.d
    def __setitem__(self, key, data):
        """Set new values for the given key. This does not force uniqueness"""
        key = int(key)
        data = np.asarray(data)
        if data.size > 0:
            self.d[key] = data
        else:
            del self.d[key]
    def __iter__(self):
        """Return an iterator for the Collection
        The iterator loops over the sorted keys.
        """
        return iter(self.keys())
    def __getitem__(self, key):
        """Return item with given key."""
        return self.d[key]
    def __delitem__(self, key):
        """Delete an item with given key"""
        del self.d[key]
[docs]    def get(self, key, default=[]):
        """Return item with given key or default."""
        key = int(key)
        return self.d.get(key, default) 
[docs]    def keys(self):
        """Return a sorted array with the keys"""
        return np.asarray(sorted(self.d.keys())) 
[docs]    def items(self):
        """Return an iterator over (key,value) pairs."""
        return self.d.items() 
[docs]    def singles(self):
        """Return an iterator over the single instances
        Returns next (k, i) pair from the collection.
        """
        for k in self:
            for v in self.d[k]:
                yield k, v 
    def __str__(self):
        if len(self) == 0:
            s = 'Empty Collection'
        else:
            s = ''
            for k in self.keys():
                s += f"{k} {self.d[k]}; "
            s = s.rstrip()
        return s 
# End