from typing import Optional, List
from decimal import Decimal
from datetime import datetime, date
from validator_collection import validators, checkers
from highcharts_core.options.series.data.base import DataBase
from highcharts_gantt import errors, constants
from highcharts_gantt.decorators import validate_types
from highcharts_gantt.metaclasses import HighchartsMeta
from highcharts_gantt.options.series.data.connect import DataConnection
from highcharts_gantt.utility_functions import validate_color, parse_jira_issue
from highcharts_gantt.utility_classes.gradients import Gradient
from highcharts_gantt.utility_classes.patterns import Pattern
[docs]class ProgressIndicator(HighchartsMeta):
    """Object representing the progress completed within a data point."""
    def __init__(self, **kwargs):
        self._amount = None
        self._fill = None
        self.amount = kwargs.get('amount', None)
        self.fill = kwargs.get('fill', None)
    @property
    def amount(self) -> Optional[int | float | Decimal]:
        """The amount completed in the progress indicator, ranging from ``0`` (not
        started) to ``1`` (completed). Defaults to :obj:`None <python:None>`, which
        behaves as ``0``.
        :rtype: numeric or :obj:`None <python:None>`
        """
        return self._amount
    @amount.setter
    def amount(self, value):
        self._amount = validators.numeric(value,
                                          allow_empty = True,
                                          minimum = 0,
                                          maximum = 1)
    @property
    def fill(self) -> Optional[str | Gradient | Pattern]:
        """Fill color to apply to the completion portion. Defaults to
        :obj:`None <python:None>`, which will apply a darkened variant of the data point's
        color.
        :rtype: :obj:`None <python:None>`, :class:`Gradient`, or :class:`Pattern`
        """
        return self._fill
    @fill.setter
    def fill(self, value):
        self._fill = validate_color(value)
    @classmethod
    def _get_kwargs_from_dict(cls, as_dict):
        kwargs = {
            'amount': as_dict.get('amount', None),
            'fill': as_dict.get('fill', None),
        }
        return kwargs
    def _to_untrimmed_dict(self, in_cls = None) -> dict:
        untrimmed = {
            'amount': self.amount,
            'fill': self.fill
        }
        return untrimmed 
[docs]class GanttData(DataBase):
    """Data point used in a
    :class:`GanttSeries <highcharts_gantt.options.series.gantt.GanttSeries>`.
    """
    def __init__(self, **kwargs):
        self._collapsed = None
        self._completed = None
        self._dependency = None
        self._end = None
        self._milestone = None
        self._parent = None
        self._start = None
        self._y = None
        
        self.collapsed = kwargs.get('collapsed', None)
        self.completed = kwargs.get('completed', None)
        self.dependency = kwargs.get('dependency', None)
        self.end = kwargs.get('end', None)
        self.milestone = kwargs.get('milestone', None)
        self.parent = kwargs.get('parent', None)
        self.start = kwargs.get('start', None)
        self.y = kwargs.get('y', None)
        super().__init__(**kwargs)
    @property
    def collapsed(self) -> Optional[bool]:
        """If ``True``, collapses the grid node belonging to this data point. Defaults to
        ``False``.
        .. note::
          Respected in axes of type ``'treegrid'``.
        :rtype: :class:`bool <python:bool>` or :obj:`None <python:None>`
        """
        return self._collapsed
    @collapsed.setter
    def collapsed(self, value):
        if value is None:
            self._collapsed = None
        else:
            self._collapsed = bool(value)
    @property
    def completed(self) -> Optional[int | float | Decimal | ProgressIndicator]:
        """Configuration of a progress indicator for the data point. Accepts either a
        numeric value between ``0`` (no progress) to ``1`` (fully complete) or a
        :class:`ProgressIndicator` object. Defaults to :obj:`None <python:None>`.
        .. note::
          If supplied as a number, will fill in the completed portion with a darkened
          version of the main color.
        :rtype: numeric or :obj:`ProgressIndicator` or :obj:`None <python:None>`
        """
        return self._completed
    @completed.setter
    def completed(self, value):
        if value is None:
            self._completed = None
        else:
            try:
                value = validators.numeric(value,
                                           allow_empty = False,
                                           minimum = 0,
                                           maximum = 1)
            except (ValueError, TypeError):
                value = validate_types(value, ProgressIndicator)
            self._completed = value
    @property
    def dependency(self) -> Optional[DataConnection | str | List[DataConnection | str]]:
        """Indicates data points that this data point depends on.
        Accepts either a :class:`str <python:str>` which corresponds to the ID of a
        different data point, a
        :class:`DataConnection <highcharts_gantt.options.series.data.connect.DataConnection>`
        object configuring the connection, an array of either, or
        :obj:`None <python:None>`. Defaults to :obj:`None <python:None>`
        :rtype: :class:`str <python:str>` or
          :class:`DataConnection <highcharts_gantt.options.series.data.connect.DataConnection>`
          or an iterable of :class:`str <python:str>` or
          :class:`DataConnection <highcharts_gantt.options.series.data.connect.DataConnection>`
          or :obj:`None <python:None>`
        """
        return self._dependency
    @dependency.setter
    def dependency(self, value):
        if not value:
            self._dependency = None
        else:
            if not checkers.is_iterable(value, forbid_literals = (str, bytes, dict)):
                try:
                    value = validators.string(value)
                except (ValueError, TypeError):
                    value = validate_types(value, DataConnection)
                self._dependency = value
            else:
                processed_value = []
                for item in value:
                    try:
                        item = validators.string(item)
                    except (ValueError, TypeError):
                        item = validate_types(item, DataConnection)
                    processed_value.append(item)
                self._dependency = [x for x in processed_value]
    @property
    def end(self) -> Optional[datetime | date]:
        """The end time of the data point (task).
        .. note::
          While **Highcharts Gantt for Python** represents this value as a
          :class:`datetime <python:datetime.datetime>`, it actually represents a POSIX
          timestamp (number of milliseconds since January 1, 1970). If you supply a
          numerical value, it will be converted to a
          :class:`datetime <python:datetime.datetime>`, and when serialized back to
          JavaScript object literal notation it will be converted back to a numerical
          value.
        :rtype: :class:`datetime <python:datetime.datetime>` or :class:`date <python:datetime.date>` 
          or :obj:`None <python:None>`
        """
        return self._end
    @end.setter
    def end(self, value):
        if not value:
            self._end = None
        elif checkers.is_date(value):
            self._end = validators.date(value, allow_empty = True)
        else:
            self._end = validators.datetime(value, allow_empty = True, coerce_value = True)
    @property
    def milestone(self) -> Optional[bool]:
        """If ``True``, indicates that this task (data point) is a milestone, which means
        that only the
        :meth:`.start <highcharts_gantt.options.series.data.gantt.GanttData.start>`
        property is taken into consideration and the
        :meth:`.end <highcharts_gantt.options.series.data.gantt.GanttData.end>` property
        is ignored. Defaults to :obj:`None <python:None>`, which behaves as ``False``.
        :rtype: :class:`bool <python:bool>` or :obj:`None <python:None>`
        """
        return self._milestone
    @milestone.setter
    def milestone(self, value):
        if value is None:
            self._milestone = None
        else:
            self._milestone = bool(value)
    @property
    def parent(self) -> Optional[str]:
        """The ID of the parent task (data point) in the Gantt series. Defaults to
        :obj:`None <python:None>`.
        :rtype: :class:`str <python:str>` or :obj:`None <python:None>`
        """
        return self._parent
    @parent.setter
    def parent(self, value):
        self._parent = validators.string(value, allow_empty = True)
    @property
    def start(self) -> Optional[date | datetime]:
        """The start time of the data point (task).
        .. note::
          While **Highcharts Gantt for Python** represents this value as a
          :class:`datetime <python:datetime.datetime>`, it actually represents a POSIX
          timestamp (number of milliseconds since January 1, 1970). If you supply a
          numerical value, it will be converted to a
          :class:`datetime <python:datetime.datetime>`, and when serialized back to
          JavaScript object literal notation it will be converted back to a numerical
          value.
        :rtype: :class:`datetime <python:datetime.datetime>` or :class:`date <python:datetime.date>` 
          or :obj:`None <python:None>`
        """
        return self._start
    @start.setter
    def start(self, value):
        if not value:
            self._start = None
        elif checkers.is_date(value):
            self._start = validators.date(value, allow_empty = True)
        else:
            self._start = validators.datetime(value, allow_empty = True, coerce_value = True)
    @property
    def y(self) -> Optional[int | float | Decimal]:
        """The Y-axis value of the task (data point).
        :rtype: numerical or :obj:`None <python:None>`
        """
        return self._y
    @y.setter
    def y(self, value):
        self._y = validators.numeric(value, allow_empty = True)
    @classmethod
    def _get_kwargs_from_dict(cls, as_dict):
        """Convenience method which returns the keyword arguments used to initialize the
        class from a Highcharts Javascript-compatible :class:`dict <python:dict>` object.
        :param as_dict: The HighCharts JS compatible :class:`dict <python:dict>`
          representation of the object.
        :type as_dict: :class:`dict <python:dict>`
        :returns: The keyword arguments that would be used to initialize an instance.
        :rtype: :class:`dict <python:dict>`
        """
        kwargs = {
            'accessibility': as_dict.get('accessibility', None),
            'class_name': as_dict.get('className', None),
            'color': as_dict.get('color', None),
            'color_index': as_dict.get('colorIndex', None),
            'custom': as_dict.get('custom', None),
            'description': as_dict.get('description', None),
            'events': as_dict.get('events', None),
            'id': as_dict.get('id', None),
            'label_rank': as_dict.get('labelRank',
                                      None) or as_dict.get('labelrank',
                                                           None),
            'name': as_dict.get('name', None),
            'selected': as_dict.get('selected', None),
            'collapsed': as_dict.get('collapsed', None),
            'completed': as_dict.get('completed', None),
            'dependency': as_dict.get('dependency', None),
            'end': as_dict.get('end', None),
            'milestone': as_dict.get('milestone', None),
            'parent': as_dict.get('parent', None),
            'start': as_dict.get('start', None),
            'y': as_dict.get('y', None),
        }
        return kwargs
    def _to_untrimmed_dict(self, in_cls = None) -> dict:
        untrimmed = {
            'collapsed': self.collapsed,
            'completed': self.completed,
            'dependency': self.dependency,
            'end': None,
            'milestone': self.milestone,
            'parent': self.parent,
            'start': None,
            'y': self.y
        }
        if self.end is not None and hasattr(self.end, 'timestamp'):
            untrimmed['end'] = self.end.timestamp()
        else:
            untrimmed['end'] = self.end
        if self.start is not None and hasattr(self.start, 'timestamp'):
            untrimmed['start'] = self.start.timestamp()
        else:
            untrimmed['start'] = self.start
        parent_as_dict = super()._to_untrimmed_dict(in_cls = in_cls)
        for key in parent_as_dict:
            untrimmed[key] = parent_as_dict[key]
        return untrimmed
[docs]    @classmethod
    def from_array(cls, value):
        """Creates a collection of data point instances, parsing the contents of ``value``
        as an array (iterable). This method is specifically used to parse data that is
        input to **Highcharts for Python** without property names, in an array-organized
        structure as described in the `Highcharts JS <https://www.highcharts.com>`__
        documentation.
          .. seealso::
            The specific structure of the expected array is highly dependent on the type
            of data point that the series needs, which itself is dependent on the series
            type itself.
            Please review the detailed :ref:`series documentation <series_documentation>`
            for series type-specific details of relevant array structures.
          .. note::
            An example of how this works for a simple
            :class:`LineSeries <highcharts_core.options.series.area.LineSeries>` (which
            uses
            :class:`CartesianData <highcharts_core.options.series.data.cartesian.CartesianData>`
            data points) would be:
            .. code-block:: python
              my_series = LineSeries()
              # A simple array of numerical values which correspond to the Y value of the
              # data point
              my_series.data = [0, 5, 3, 5]
              # An array containing 2-member arrays (corresponding to the X and Y values
              # of the data point)
              my_series.data = [
                  [0, 0],
                  [1, 5],
                  [2, 3],
                  [3, 5]
              ]
              # An array of dict with named values
              my_series.data = [
                {
                    'x': 0,
                    'y': 0,
                    'name': 'Point1',
                    'color': '#00FF00'
                },
                {
                    'x': 1,
                    'y': 5,
                    'name': 'Point2',
                    'color': '#CCC'
                },
                {
                    'x': 2,
                    'y': 3,
                    'name': 'Point3',
                    'color': '#999'
                },
                {
                    'x': 3,
                    'y': 5,
                    'name': 'Point4',
                    'color': '#000'
                }
              ]
        :param value: The value that should contain the data which will be converted into
          data point instances.
          .. note::
            If ``value`` is not an iterable, it will be converted into an iterable to be
            further de-serialized correctly.
        :type value: iterable
        :returns: Collection of :term:`data point` instances (descended from
          :class:`DataBase <highcharts_core.options.series.data.base.DataBase>`)
        :rtype: :class:`list <python:list>` of
          :class:`DataBase <highcharts_core.options.series.data.base.DataBase>`
          descendant instances
        """
        if not value:
            return []
        elif checkers.is_string(value):
            try:
                value = validators.json(value)
            except (ValueError, TypeError):
                pass
        elif not checkers.is_iterable(value):
            value = [value]
        collection = []
        for item in value:
            if checkers.is_type(item, 'GanttData'):
                as_obj = item
            elif checkers.is_dict(item):
                as_obj = cls.from_dict(item)
            elif item is None or isinstance(item, constants.EnforcedNullType):
                as_obj = cls()
            else:
                raise errors.HighchartsValueError(f'each data point supplied must either '
                                                  f'be a GanttData Data Point or be '
                                                  f'coercable to one. Could not coerce: '
                                                  f'{item}')
            collection.append(as_obj)
        return collection 
[docs]    @classmethod
    def from_asana(cls,
                   task,
                   use_html_description = True,
                   connection_callback = None,
                   connection_kwargs = None):
        """Create a 
        :class:`GanttData <highcharts_gantt.options.series.data.gantt.Ganttdata>` 
        instance from an Asana task :class:`dict <python:dict>` representation.
        
        :param task: An Asana task object.
        :type task: :class:`dict <python:dict>`
        
        :param use_html_description: If ``True``, will use the Asana task's HTML notes 
          in the data point's 
          :meth:`.description <highcharts_gantt.options.series.data.gantt.GanttData.description>` 
          field. If ``False``, will use the non-HTML notes. Defaults to ``True``.
        :type use_html_description: :class:`bool <python:bool>`
        
        :param connection_callback: A custom Python function or method which accepts two
          keyword arguments: ``connection_target`` (which expects the dependency 
          :class:`dict <python:dict>` object from the Asana task), and ``task`` 
          (which expects the originating Asana task :class:`dict <python:dict>` 
          object). The function should return a 
          :class:`DataConnection <highcharts_gantt.options.series.data.connect.DataConnection>` 
          instance. Defaults to :obj:`None <python:None>`
          
          .. tip::
          
            The ``connection_callback`` argument is useful if you want to customize the
            connection styling based on properties included in the Asana task.
            
        :type connection_callback: Callable or :obj:`None <python:None>`
        :param connection_kwargs: Set of keyword arugments to supply to the   
          :class:`DataConnection <highcharts_gantt.options.series.data.connect.DataConnection>`
          constructor, besides the :meth:`.to <highcharts_gantt.options.series.data.connect.DataConnection.to>` property which is derived from the task. Defaults
          to :obj:`None <python:None>`
        :type connection_kwargs: :class:`dict <python:dict>` or 
          :obj:`None <python:None>`
        :returns: A 
          :class:`GanttData <highcharts_gantt.options.series.data.gantt.Ganttdata>` 
          instance
        :rtype: :class:`GanttData <highcharts_gantt.options.series.data.gantt.Ganttdata>` 
        """
        if connection_callback and not checkers.is_callable(connection_callback):
            raise errors.HighchartsValueError('connection_callback - if supplied - '
                                              'must be callable.')
        connection_kwargs = validators.dict(connection_kwargs,
                                            allow_empty = True) or {}
        is_milestone = task.get('resource_subtype', None) == 'milestone'
        data_point = cls(end = task.get('due_on', None) or task.get('due_at', None),
                         start = task.get('start_on', None) or task.get('start_at',
                                                                        None),
                         parent = task.get('parent', None),
                         completed = task.get('completed', None),
                         milestone = is_milestone,
                         id = task['gid'],
                         name = task['name'])
        if use_html_description:
            data_point.description = task.get('html_notes', None)
        else:
            data_point.description = task.get('notes', None)
        dependencies = []
        for item in task['dependencies']:
            if connection_callback:
                connection = connection_callback(connection_target = item, 
                                                 task = task)
            elif connection_kwargs:
                connection_kwargs['to'] = item['gid']
                connection = DataConnection(**connection_kwargs)
            else:
                connection = item['gid']
            dependencies.append(connection)
            
        data_point.dependency = dependencies
        #data_point.custom = task
        
        return data_point 
[docs]    @classmethod
    def from_monday(cls,
                    task,
                    template = None,
                    property_column_map = None,
                    connection_kwargs = None,
                    connection_callback = None):
        """Create a 
        :class:`GanttData <highcharts_gantt.options.series.data.gantt.Ganttdata>` 
        instance from a Monday.com task :class:`dict <python:dict>` representation.
        
        :param task: A Monday.com task object.
        :type task: :class:`dict <python:dict>`
        
        :param connection_callback: A custom Python function or method which accepts two
          keyword arguments: ``connection_target`` (which expects the dependency 
          :class:`dict <python:dict>` object from the Monday.com task), and ``task`` 
          (which expects the originating Monday.com task :class:`dict <python:dict>` 
          object). The function should return a 
          :class:`DataConnection <highcharts_gantt.options.series.data.connect.DataConnection>` 
          instance. Defaults to :obj:`None <python:None>`
          
          .. tip::
          
            The ``connection_callback`` argument is useful if you want to customize the
            connection styling based on properties included in the Monday.com task.
            
        :type connection_callback: Callable or :obj:`None <python:None>`
        :param connection_kwargs: Set of keyword arugments to supply to the   
          :class:`DataConnection <highcharts_gantt.options.series.data.connect.DataConnection>`
          constructor, besides the :meth:`.to <highcharts_gantt.options.series.data.connect.DataConnection.to>` 
          property which is derived from the task. Defaults
          to :obj:`None <python:None>`
        :type connection_kwargs: :class:`dict <python:dict>` or 
          :obj:`None <python:None>`
        :returns: A 
          :class:`GanttData <highcharts_gantt.options.series.data.gantt.Ganttdata>` 
          instance
        :rtype: :class:`GanttData <highcharts_gantt.options.series.data.gantt.Ganttdata>` 
        
        :raises HighchartsValueError: if both ``template`` and ``property_column_map`` 
          are :obj:`None <python:None>` or ``connection_callback`` is not callable
        :raises MondayTemplateError: if ``template`` is not :obj:`None <python:None>`, 
          but is not supported
        """
        template = validators.string(template, allow_empty = True) or 'task-management'
        property_column_map = validators.dict(property_column_map, allow_empty = True)
        if connection_callback and not checkers.is_callable(connection_callback):
            raise errors.HighchartsValueError('connection_callback - if supplied - '
                                              'must be callable.')
        connection_kwargs = validators.dict(connection_kwargs,
                                            allow_empty = True) or {}
        if not property_column_map:
            property_column_map = constants.MONDAY_TEMPLATES.get(template, None)
            if not property_column_map:
                raise errors.MondayTemplateError(f'template ("{template}") is not '
                                                 f'a supported Monday.com template name.')
        data_point_kwargs = {}
        for key, field_name in property_column_map.items():
            data_point_kwargs[key] = task.get(field_name, None)
        data_point = cls(**data_point_kwargs)
        dependencies = []
        for item in task.get('dependencies', []):
            item = validators.string(item, allow_empty = False, coerce_value = True)
            if connection_callback:
                connection = connection_callback(connection_target = item,
                                                 task = task)
            elif connection_kwargs:
                connection_kwargs['to'] = item
                connection = DataConnection(**connection_kwargs)
            else:
                connection = DataConnection(to = item)
            dependencies.append(connection)
        data_point.dependency = dependencies
        data_point.custom = task
        return data_point 
[docs]    @classmethod
    def from_jira(cls,
                  issue,
                  connection_kwargs = None,
                  connection_callback = None):
        """Create a 
        :class:`GanttData <highcharts_gantt.options.series.data.gantt.GanttData>` 
        instance from a JIRA :class:`Issue <jira:jira.resources.Issue>` representation.
        
        :param issue: A JIRA :class:`Issue <jira:jira.resources.Issue>` instance
        :type issue: :class:`Issue <jira:jira.resources.Issue>`
        
        :param connection_callback: A custom Python function or method which accepts two
          keyword arguments: ``connection_target`` (which expects the dependency 
          :class:`Issue <jira:jira.resources.Issue>` instance), and ``issue`` 
          (which expects the originating :class:`Issue <jira:jira.resources.Issue>`
          object). The function should return a 
          :class:`DataConnection <highcharts_gantt.options.series.data.connect.DataConnection>` 
          instance. Defaults to :obj:`None <python:None>`
          
          .. tip::
          
            The ``connection_callback`` argument is useful if you want to customize the
            connection styling based on properties included in the Monday.com task.
            
        :type connection_callback: Callable or :obj:`None <python:None>`
        :param connection_kwargs: Set of keyword arugments to supply to the   
          :class:`DataConnection <highcharts_gantt.options.series.data.connect.DataConnection>`
          constructor, besides the :meth:`.to <highcharts_gantt.options.series.data.connect.DataConnection.to>` 
          property which is derived from the task. Defaults to :obj:`None <python:None>`
        :type connection_kwargs: :class:`dict <python:dict>` or 
          :obj:`None <python:None>`
        :returns: A 
          :class:`GanttData <highcharts_gantt.options.series.data.gantt.Ganttdata>` 
          instance
        :rtype: :class:`GanttData <highcharts_gantt.options.series.data.gantt.Ganttdata>` 
        
        :raises HighchartsValueError: if both ``template`` and ``property_column_map`` 
          are :obj:`None <python:None>` or ``connection_callback`` is not callable
        :raises MondayTemplateError: if ``template`` is not :obj:`None <python:None>`, 
          but is not supported
        """
        try:
            data_point_kwargs = parse_jira_issue(issue)
        except errors.JIRADuplicateIssueError:
            return None
        
        data_point = cls(**data_point_kwargs)
        
        return data_point