from importlib import import_module
from typing import Dict, List

import xarray

from .core import BlueskyRun


__all__ = ['Projector', 'project_xarray']


class ProjectionError(Exception):
    pass


def get_run_projection(run: BlueskyRun, projection_name: str = None):
    """Finds a projection in the run.
    If projection_name is provided, searches through the projections in the run
    to find a match.

    Otherwise, looks in the run to see if there is only one projection. If so, returns it.

    Parameters
    ----------
    run : BlueskyRun
        Run to investigate for a projection
    projection_name : str, optional
        name of the projection to look for, by default None

    Returns
    -------
    dict
        returns a projection dictionary, or None of not found

    Raises
    ------
    KeyError
        If the a projection_name is specified and there is more than one
        projection in the run with that name
    """

    if projection_name is not None:
        projections = [projection for projection in run.metadata['start']['projections']
                       if projection.get('name') == projection_name]
        if len(projections) > 1:
            raise KeyError("Multiple projections of name {projection_name} found")
        if len(projections) == 1:
            return projections[0]
        if len(projections) == 0:
            return None

    projections = run.metadata['start'].get('projections')
    if projections is not None and len(projections) == 1:
        return run.metadata['start']['projections'][0]

    return None


def get_calculated_value(run: BlueskyRun, key: str, mapping: dict):
    """Calls and returns the callable from the calculated projection mapping.

    It is ancticipated that the return will be
    and xarray.DataArray.

    This should be expressed in the familiar 'module:func' syntax borrowed from python entry-points.

    An example implementation of a calculated field projection entry:

        '/projection/key': {
            "type": "calculated",
            "callable": "foo.bar:really_fun",
            "args": ['arg1'], "kwargs": {"foo": "bar"}}

    And a corresponding function implementation might be:

        def really_fun(run, *args, **kwargs)"
            # args will be ['arg1']
            # kwargs will be {"foo": "bar"}
            # for this calculated field
            return xarray.DataArray[[1, 2, 3]]


    Parameters
    ----------
    run : BlueskyRun
        run which can be used for the calcuation
    key : str
        key name for this projection
    mapping : dict
        full contents of this projection

    Returns
    -------
    any
        result of calling the method specified in the calcated field in the projection

    Raises
    ------
    ProjectionError
        [description]
    """
    callable_name = mapping['callable']
    try:
        module_name, function_name = callable_name.split(":")
        module = import_module(module_name)
        callable_func = getattr(module, function_name)
    except ProjectionError as e:
        raise ProjectionError('Error importing callable {function_name}', e)

    calc_args = mapping['args']
    calc_kwargs = mapping['kwargs']
    return callable_func(run, *calc_args, **calc_kwargs)


class Projector():
    """Helper for scanning projection and building custom porjectors.

    EXPERIMENTAL: projection code is experimental and could change in the near future.

    Handles much of the inner workings of projecting a BlueskyRun by scanning the
    projection definition and providing callbacks for the different types of items
    that can be projected.
    """

    def __init__(self, metadata_cb=None, event_configuration_cb=None, event_field_cb=None):
        """Pass optional callbacks for the various types of projected field defined in a projection

        Parameters
        ----------
        metadata_cb : callable, optional
            Receives a field name and a value for every field in the projection with
            type=linked and location=start, by default None
        event_configuration_cb : callable, optional
            Receives a stream name, field name and a value for every field in the projection with
            type=linked and location=configuration, by default None, by default None
        event_field_cb : callable, optional
            Receives a stream name, field name and a value for every field in the projection with
            type=linked and location=event, by default None, by default None
        """
        self._metadata_cb = metadata_cb
        self._event_configuration_cb = event_configuration_cb
        self._event_field_cb = event_field_cb
        self._issues = []

    @property
    def issues(self):
        return self._issues

    def project(self, run: BlueskyRun, projection=None, projection_name=None):
        """Iterates a projection and communicates fields through callbacks.

        Selects projection based on logic of get_run_projection().

        Projections come with multiple types: linked, and caclulated. Calculated fields are only supported
        in the data (not at the top-level attrs).

        Calculated fields in projections schema contain a callable field. This should be expressed in