"""
AnalogMeasurer is the base class of all custom measurer classes.

A measurer processes a span of contiguous analog samples and produces:

  * Measures (See :py:class:`.measure.Measure`)
  * Annotations (See :py:class:`.annotation.Annotation`)

To implement a custom measurer:

  * Subclass :py:class:`~.AnalogMeasurer`
  * Declare measures and annotation as class members
  * Implement the :py:meth:`~.AnalogMeasurer.measure_range` method

The `measure_range` method is passed a range of analog samples to process and is where it does its work.
The declared measures and annotations should be set within this method.

.. code-block::
  :caption: A basic measurer

  from saleae.data import AnalogSpan
  from saleae.measurements import AnalogMeasurer, Annotation, HorizontalRule, Measure

  class MyMeasurer(AnalogMeasurer):
      min_measure = Measure('min', description='Minimum Value')
      max_measure = Measure('max', description='Maximum Value')
      mid_measure = Measure('mid', description='Mid-point')

      # This annotation will only be shown if the user has enabled `mid_measure`
      mid_annotation = Annotation(measures=[mid_measure])

      def measure_range(self, data: AnalogSpan):
          self.min_measure.value = data.min
          self.max_measure.value = data.max
          self.mid_measure.value = (data.max + data.min) / 2

          self.mid_annotation.value = HorizontalRule(value=self.mid_measure.value)

"""

import inspect

from .measure import Measure, MeasureInstance
from .annotation import Annotation, AnnotationInstance
from saleae.data import AnalogSpan


class MetaAnalogMeasurer(type):
    def __call__(cls, enabled_measures, *args, **kwargs):
        obj = cls.__new__(cls, enabled_measures, *args, **kwargs)
        #obj.__init__(enabled_measures, *args, **kwargs)
        obj.__init__(*args, **kwargs)
        return obj


class Point:
    pass


class AnalogMeasurer(metaclass=MetaAnalogMeasurer):
    """
    AnalogMeasurer base class.

    `measure_range` must be implemented.

    """

    # def measure_point(self, data: AnalogSpan, origin_point: Point):
    #    """
    #    Measure from a specific point. Used by instantaneous measurements.
    #    If this is omitted from the subclass, point-based measurements will not be supported.

    #    Note: Not currently supported or used in Logic 2.
    #    """

    def measure_range(self, data: AnalogSpan):
        """
        This method is called when running a measurement over a span of analog data.

        This must be overridden by subclasses.

        :param data: The span of data to run the measurement on.
        """

    @classmethod
    def _get_measures(cls):
        if hasattr(cls, '_measures'):
            return cls._measures

        measures = []

        for name, value in inspect.getmembers(cls):
            if isinstance(value, Measure):
                measures.append((name, value))

        cls._measures = measures

        return cls._measures

    @classmethod
    def _get_annotations(cls):
        if hasattr(cls, '_annotations'):
            return cls._annotations

        annotations = []

        measures = cls._get_measures()
        for name, value in inspect.getmembers(cls):
            if isinstance(value, Annotation):
                measure_ids = []
                for measure_ref in value.measures:
                    if not isinstance(measure_ref, Measure):
                        raise RuntimeError(
                            f'Expected Measure type in Annotation measures list for "{name}"')

                    matching_measure_id = next(
                        (measure_id for measure_id, measure in measures if measure_ref == measure), None)

                    if matching_measure_id is None:
                        raise RuntimeError(f'Invalid measure: {measure_ref}')

                    measure_ids.append(matching_measure_id)

                annotations.append((name, measure_ids))

        cls._annotations = annotations

        return cls._annotations

    def __new__(cls, enabled_measures, *args, **kwargs):
        obj = super(AnalogMeasurer, cls).__new__(cls, *args, **kwargs)

        setattr(obj, '_enabled_measures_', enabled_measures)

        measures = cls._get_measures()
        all_measure_ids = []

        # Create a MeasureInstance on the object for each Measure
        for measure_id, measure in measures:
            measure_instance = MeasureInstance(
                enabled=measure_id in enabled_measures)
            setattr(obj, measure_id, measure_instance)
            all_measure_ids.append(measure_id)

        # Ensure that all requested measures exist are valid
        for id in enabled_measures:
            if id not in all_measure_ids:
                raise RuntimeError(f'Unrecognized measure requested: {id}')

        # Create an AnnotationInstance on the object for each Annotation
        annotations = cls._get_annotations()
        for annotation_id, _ in annotations:
            annotation_instance = AnnotationInstance()
            setattr(obj, annotation_id, annotation_instance)

        return obj
