"""
This module is designed to make romancal tweakreg and resample functions accessible without
installing the original package due to their complex dependencies. As such, it is only subtly modified from
the original to accommodate the less stringent install requirements.


romancal copyright notice:

Copyright (C) 2010 Association of Universities for Research in Astronomy (AURA)

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

    1. Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.

    2. Redistributions in binary form must reproduce the above
      copyright notice, this list of conditions and the following
      disclaimer in the documentation and/or other materials provided
      with the distribution.

    3. The name of AURA and its representatives may not be used to
      endorse or promote products derived from this software without
      specific prior written permission.

THIS SOFTWARE IS PROVIDED BY AURA ``AS IS'' AND ANY EXPRESS OR IMPLIED
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL AURA BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
DAMAGE.

Original: https://github.com/spacetelescope/romancal/blob/main/romancal/associations/lib/process_list.py
"""

from collections import deque
from enum import Enum
from functools import reduce

__all__ = [
    "ListCategory",
    "ProcessList",
    "ProcessItem",
    "ProcessQueue",
    "ProcessQueueSorted",
]


class ListCategory(Enum):
    """Categories in the list"""

    RULES = 0
    BOTH = 1
    EXISTING = 2
    NONSCIENCE = 3


class ProcessItem:
    """Items to be processed

    Parameters
    ----------
    obj : object
        The object to make a `ProcessItem`.
        Objects must be equatable.
    """

    def __init__(self, obj):
        self.obj = obj

    @classmethod
    def to_process_items(cls, iterable):
        """Iterable to convert a list to ProcessItem's

        Parameters
        ----------
        iterable : iterable
            A source of objects to convert

        Returns
        -------
        An iterable where the object has been
        converted to a `ProcessItem`
        """
        for obj in iterable:
            yield cls(obj)

    def __hash__(self):
        try:
            hash_value = self.obj.__hash__()
        except (AttributeError, TypeError):
            hash_value = hash(repr(self))
        return hash_value

    def __eq__(self, other):
        try:
            equality = self.obj == other.obj
        except AttributeError:
            equality = self.__hash__() == other.__hash__()
        return equality


class ProcessList:
    """A Process list

    Parameters
    ----------
    items : [item[, ...]]
        The list of items to process

    rules : [Association[, ...]]
        List of rules to process the items against.

    work_over : int
        What the reprocessing should work on:
        - `ProcessList.EXISTING`:   Only existing associations
        - `ProcessList.RULES`:      Only on the rules to create new associations
        - `ProcessList.BOTH`:       Compare to both existing and rules
        - `ProcessList.NONSCIENCE`: Only on non-science items

    only_on_match : bool
        Only use this object if the overall condition
        is True.

    trigger_constraints : [Constraint[,...]]
        The constraints that created the ProcessList

    trigger_rules : [Association[,...]]
        The association rules that created the ProcessList
    """

    _str_attrs = (
        "rules",
        "work_over",
        "only_on_match",
        "trigger_constraints",
        "trigger_rules",
    )

    def __init__(
        self,
        items=None,
        rules=None,
        work_over=ListCategory.BOTH,
        only_on_match=False,
        trigger_constraints=None,
        trigger_rules=None,
    ):
        self.items = items
        self.rules = rules
        self.work_over = work_over
        self.only_on_match = only_on_match
        self.trigger_constraints = (
            set(trigger_constraints) if trigger_constraints else set()
        )
        self.trigger_rules = set(trigger_rules) if trigger_rules else set()

    @property
    def hash(self):
        """Create a unique hash"""
        return (tuple(self.rules), self.work_over, self.only_on_match)

    def update(self, process_list, full=False):
        """Update with information from ProcessList

        Attributes from `process_list` are added to self's attributes. If `not
        full`, the attributes `rules`, 'work_over`, and `only_on_match` are not
        taken.

        Note that if `full`, destructive action will occur with respect to
        `work_over` and `only_on_match`.

        Parameters
        ----------
        process_list : ProcessList
            The source process list to absorb.

        full : bool
            Include the hash attributes `rules`, `work_over`, and `only_on_match`.
        """
        self.items += process_list.items
        self.trigger_constraints.update(process_list.trigger_constraints)
        self.trigger_rules.update(process_list.trigger_rules)
        if full:
            self.rules += process_list.rules
            self.work_over = process_list.work_over
            self.only_on_match = process_list.only_on_match

    def __str__(self):
        result = "{}(n_items: {}, {})".format(
            self.__class__.__name__,
            len(self.items),
            {str_attr: getattr(self, str_attr) for str_attr in self._str_attrs},
        )
        return result


class ProcessQueue(deque):
    """Make a deque iterable and mutable"""

    def __iter__(self):
        while True:
            try:
                yield self.popleft()
            except (IndexError, StopIteration):
                break


class ProcessListQueue:
    """First-In-First-Out queue of ProcessLists

    Parameters
    ----------
    init : [ProcessList[,...]] or None
        List of ProcessLists to put on the queue.
    """

    def __init__(self, init=None):
        self._queue = {}
        if init is not None:
            self.extend(init)

    def append(self, process_list):
        """Add ProcessList to queue"""
        plhash = process_list.hash
        if plhash not in self._queue:
            self._queue[plhash] = process_list
        else:
            self._queue[plhash].update(process_list)

    def extend(self, iterable):
        """Add objects if not already in the queue"""
        for process_list in iterable:
            self.append(process_list)

    def items(self):
        """Return list generator of all items"""
        for plhash in self._queue:
            yield from self._queue[plhash].items

    def popleft(self):
        """Pop the first-in object"""
        plhash = next(iter(self._queue))
        process_list = self._queue[plhash]
        del self._queue[plhash]
        return process_list

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

    def __iter__(self):
        while True:
            try:
                yield self.popleft()
            except (IndexError, StopIteration):
                break

    def __str__(self):
        result = (
            f"{self.__class__.__name__}: rulesets {len(self)} items"
            f" {len(list(self.items()))}"
        )
        return result


class ProcessQueueSorted:
    """Sort ProcessItem based on work_over

    `ProcessList`s are handled in order of `RULES`, `BOTH`,
    `EXISTING`, and `NONSCIENCE`.

    Parameters
    ----------
    init : [ProcessList[,...]]
        List of `ProcessList` to start the queue with.
    """

    def __init__(self, init=None):
        self.queues = {
            list_category: ProcessListQueue() for list_category in ListCategory
        }

        if init is not None:
            self.extend(init)

    def extend(self, process_lists):
        """Add the list of process items to their appropriate queues"""
        for process_list in process_lists:
            self.queues[process_list.work_over].append(process_list)

    def __iter__(self):
        """Return the queues in order"""
        while len(self) > 0:
            for category in ListCategory:
                for process_list in self.queues[category]:
                    yield process_list
                    break
                else:
                    continue
                break

    def __len__(self):
        return reduce(lambda x, y: x + len(y), self.queues.values(), 0)

    def __str__(self):
        result = f"{self.__class__.__name__}:"
        for queue in self.queues:
            result += f"\n\tQueue {queue}: {self.queues[queue]}"
        return result


def workover_filter(process_list, work_over):
    """Determine and modify workover of input process list

    Parameters
    ---------
    process_list : ProcessList
        The process list under consideration

    work_over : ListCategory
        The ListCategory to compare against.

    Returns
    -------
    process_list : ProcessList or None
        The input process_list with work_over modified.
        None if the process list should not be continued.
    """
    result = process_list
    if process_list.work_over in [ListCategory.RULES, ListCategory.BOTH]:
        if work_over in [ListCategory.RULES, ListCategory.BOTH]:
            result.work_over = ListCategory.BOTH
        else:
            result.work_over = work_over
    else:
        if work_over not in [ListCategory.RULES, ListCategory.BOTH]:
            result = None
    return result