"""Compile device information before and after Redis publication
"""

from collections import OrderedDict, namedtuple

DeviceTypeDefinition = namedtuple(
    "DeviceTypeDefinition",
    ["namemap", "typemap", "unitmap"],
)

DEFAULT_TYPE_DEFINITION = DeviceTypeDefinition(
    namemap=dict(), typemap=dict(), unitmap=dict()
)

TYPE_DEFINITIONS = {
    "mca": DeviceTypeDefinition(
        namemap={  # See https://gitlab.esrf.fr/-/snippets/249
            "icr": "trigger_count_rate",  # triggers/trigger_live_time
            "ocr": "event_count_rate",  # events/elapsed_time
            "triggers": "triggers",
            "events": "events",
            "deadtime": "fractional_dead_time",
            "trigger_livetime": "trigger_live_time",
            "energy_livetime": "live_time",
            "realtime": "elapsed_time",
            "det": "spectrum",
        },
        typemap={
            "spectrum": "principal",
            "icr": "icr",
            "ocr": "ocr",
            "triggers": "triggers",
            "events": "events",
            "deadtime": "deadtime",
            "trigger_livetime": "trigger_livetime",
            "energy_livetime": "livetime",
            "realtime": "realtime",
        },
        unitmap={
            "icr": "hertz",
            "orc": "hertz",
            "livetime": "s",
            "trigger_livetime": "s",
            "realtime": "s",
        },
    ),
    "mosca": DeviceTypeDefinition(
        namemap={  # See https://gitlab.esrf.fr/-/snippets/249
            "icr": "trigger_count_rate",  # triggers/trigger_live_time
            "ocr": "event_count_rate",  # events/elapsed_time
            "triggers": "triggers",
            "events": "events",
            "deadtime": "fractional_dead_time",
            "trigger_livetime": "trigger_live_time",
            "energy_livetime": "live_time",
            "realtime": "elapsed_time",
            "det": "spectrum",
        },
        typemap={
            "spectrum": "principal",
            "icr": "icr",
            "ocr": "ocr",
            "triggers": "triggers",
            "events": "events",
            "deadtime": "deadtime",
            "trigger_livetime": "trigger_livetime",
            "energy_livetime": "livetime",
            "realtime": "realtime",
        },
        unitmap={
            "icr": "hertz",
            "orc": "hertz",
            "livetime": "s",
            "trigger_livetime": "s",
            "realtime": "s",
        },
    ),
    "lima": DeviceTypeDefinition(
        namemap=dict(),
        typemap={"image": "principal", "sum": "principal"},
        unitmap=dict(),
    ),
    "mythen": DeviceTypeDefinition(
        namemap=dict(), typemap={"spectrum": "principal"}, unitmap=dict()
    ),
    "samplingcounter": DeviceTypeDefinition(
        namemap=dict(), typemap={"signal": "principal"}, unitmap=dict()
    ),
}


def shortnamemap(names, separator=":"):
    """Map full Redis names to short (but still unique) names

    :param list(str) names:
    :param str separator:
    :returns dict:
    """
    if not names:
        return {}
    names = set(names)
    parts = [name.split(separator) for name in names]
    nparts = max(map(len, parts))
    parts = [([""] * (nparts - len(lst))) + lst for lst in parts]
    ret = {}
    for i in reversed(range(-nparts, 0)):
        joinednames = [separator.join(s for s in lst[i:] if s) for lst in parts]
        newnames = joinednames + list(ret.values())
        selection = [
            (idx, (separator.join(s for s in lst if s), name))
            for idx, (name, lst) in enumerate(zip(joinednames, parts))
            if newnames.count(name) == 1
        ]
        if selection:
            idx, tuples = list(zip(*selection))
            ret.update(tuples)
            parts = [lst for j, lst in enumerate(parts) if j not in idx]
    return ret


def fill_device(fullname, device, device_info=None, data_info=None):
    """Add missing keys with default values

        device_type: type for the writer (not saved), e.g. positioner, mca, lima
        device_name: HDF5 group name (measurement or positioners when missing)
        device_info: HDF5 group datasets
        data_type: maximal one device dataset has type "principal"
        data_name: HDF5 dataset name
        data_alias: HDF5 dataset link name
        data_info: HDF5 dataset attributes
        unique_name: Unique name for HDF5 links
        master_index: >=0 axis order used for plotting
        single: only data of this device
                a single's data_name is either "data" (detector) or "value" (positioner)
        dependencies: fullnames

    :param str fulname:
    :param dict device:
    :param dict device_info:
    :param dict data_info:
    """
    if device_info is None:
        device_info = {}
    if data_info is None:
        data_info = {}
    device["device_type"] = device.get("device_type", "")
    device["device_name"] = device.get("device_name", fullname)
    device["device_info"] = device.get("device_info", device_info)
    device["data_type"] = device.get("data_type", "principal")
    device["data_name"] = device.get("data_name", "data")
    device["data_alias"] = device.get("data_alias", None)
    device["data_info"] = device.get("data_info", data_info)
    device["unique_name"] = device.get("unique_name", fullname)
    device["master_index"] = -1
    device["single"] = None
    device["dependencies"] = {}
    device["metadata_keys"] = {}


def update_device(devices, fullname, device_info=None, data_info=None):
    """Add missing device and/or keys

    :param dict devices:
    :param str fullname:
    :param dict device_info:
    :param dict data_info:
    """
    devices[fullname] = device = devices.get(fullname, {})
    fill_device(fullname, device, device_info=device_info, data_info=data_info)
    return device


def parse_devices(devices, short_names=True, multivalue_positioners=False):
    """Determine names and types based on device name and type

    :param dict devices:
    :param bool short_names:
    :param bool multivalue_positioners:
    """
    namemap = shortnamemap(list(devices.keys()))
    for fullname, device in devices.items():
        device["device_name"] = namemap.get(fullname, fullname)
        type_definition = TYPE_DEFINITIONS.get(
            device["device_type"], DEFAULT_TYPE_DEFINITION
        )
        if device["device_type"] == "mca":
            # 'xmap1:xxxxxx_det1'
            # 'xmap1:roi1'
            #
            #   xxxxxx: spectrum, icr, ocr, triggers, events,
            #           deadtime, trigger_livetime, energy_livetime,
            #           realtime, roi1, roi2, ...
            #
            #   device_name = 'xmap1:det1'
            #   data_type = mcatypemap('xxxxxx')
            #   data_name = mcanamemap('xxxxxx')
            parts = fullname.split(":")
            lastparts = parts[-1].split("_")
            if len(lastparts) == 1:
                mcachannel = "sum"
                datatype = lastparts[-1]  # roi1
            else:
                mcachannel = lastparts[-1]
                datatype = "_".join(lastparts[:-1])  # xxxxxx

            parts = parts[:-1] + [mcachannel]
            device["device_name"] = ":".join(parts)
            device["data_name"] = type_definition.namemap.get(datatype, datatype)
            if datatype.startswith("roi"):
                datatype = "roi"
            device["data_type"] = type_definition.typemap.get(datatype, datatype)
            device["data_info"]["units"] = type_definition.unitmap.get(
                device["data_type"], None
            )
        elif device["device_type"] == "mosca":
            # 'falconx:spectrum:det01'
            #
            # 'falconx:stat:xxxx_det01'
            #  with xxxx in: [ icr, ocr, triggers, events, deadtime, trigger_livetime, energy_livetime, realtime ]
            #
            # 'falconx:roi:xxx_det01' or 'falconx:roi:xxx_sum_all' or 'falconx:roi:xxx_sum_yy_zz'
            #
            parts = fullname.split(":")
            lastparts = parts[-1].split("_")
            if parts[-2] == "spectrum":
                mcachannel = f"det{int(lastparts[-1][-2:])}"
                datatype = "spectrum"
            elif parts[-2] == "stat":
                mcachannel = f"det{int(lastparts[-1][-2:])}"
                datatype = "_".join(lastparts[:-1])  # xxxx
            elif parts[-2] == "roi":
                channel = device.get("channel")
                if isinstance(channel, tuple):
                    cfrom, cto = channel
                    mcachannel = f"sum_{cfrom}_{cto}"
                    datatype = "_".join(lastparts[:-3])  # xxx
                elif channel == -1:
                    mcachannel = "sum_all"
                    datatype = "_".join(lastparts[:-2])  # xxx
                else:
                    mcachannel = f"det{channel}"
                    datatype = "_".join(lastparts[:-1])  # xxx

            device["data_name"] = type_definition.namemap.get(datatype, datatype)
            datatype = "roi" if parts[-2] == "roi" else datatype
            device["data_type"] = type_definition.typemap.get(datatype, datatype)
            parts = parts[:-2] + [mcachannel]
            device["device_name"] = ":".join(parts)
            device["data_info"]["units"] = type_definition.unitmap.get(
                device["data_type"], None
            )
        elif device["device_type"] == "mythen":
            # 'mythen1:spectrum'
            # 'mythen1:roi1'
            parts = fullname.split(":")
            device_name = parts[0]
            datatype = ":".join(parts[1:])
            device["device_name"] = device_name
            device["data_type"] = type_definition.typemap.get(datatype, datatype)
            device["data_name"] = type_definition.namemap.get(datatype, datatype)
        elif device["device_type"] == "lima":
            # 'frelon1:image'
            # 'frelon1:roi_counters:roi1_min'
            # 'frelon1:roi_profiles:roi2'
            # 'frelon1:bpm:fwhm_x'
            # 'frelon1:roi_collection:roi_collection_counter_data'
            parts = fullname.split(":")
            if len(parts) == 3:
                device["dependencies"] = {parts[0] + ":image": "image"}
                if parts[1] == "roi_counters":
                    roi_name, _, processing_name = parts[-1].rpartition("_")
                    device_name = ":".join([parts[0], roi_name])
                    datatype = processing_name
                    device["metadata_keys"] = {roi_name: "selection"}
                elif parts[1] == "roi_profiles":
                    device_name = ":".join([parts[0], parts[-1]])
                    datatype = "sum"
                    device["metadata_keys"] = {parts[-1]: "selection"}
                elif parts[1] == "roi_collection":
                    device_name = ":".join([parts[0], parts[1]])
                    datatype = "sum"
                    device["metadata_keys"] = {"roi_collection_counter": "selection"}
                else:
                    device_name = ":".join(parts[:2])
                    datatype = parts[2]
            else:
                device_name = parts[0]
                datatype = ":".join(parts[1:])
            device["device_name"] = device_name
            device["data_type"] = type_definition.typemap.get(datatype, datatype)
            device["data_name"] = type_definition.namemap.get(datatype, datatype)
        elif device["device_type"] == "autofilter":
            # autofilter1:xxx
            #  device_name = 'autofilter1'
            #   data_type = typemap('xxxxxx')
            #   data_name = namemap('xxxxxx')
            device["device_name"], datatype = fullname.split(":")
            device["data_type"] = type_definition.typemap.get(datatype, datatype)
            device["data_name"] = type_definition.namemap.get(datatype, datatype)
        elif device["device_type"] == "samplingcounter":
            if device["data_type"] == "signal":
                datatype = "signal"
            else:
                # 'simdiodeSAMPLES_xxxxx'
                #   device_name = 'simdiodeSAMPLES'
                #   data_type = typemap('xxxxxx')
                #   data_name = namemap('xxxxxx')
                parts = device["device_name"].split("_")
                datatype = parts[-1]  # xxxxxx
                parts = ["_".join(parts[:-1])]
                device["device_name"] = "_".join(parts)
            device["data_type"] = type_definition.typemap.get(datatype, datatype)
            device["data_name"] = type_definition.namemap.get(datatype, datatype)
        elif device["device_type"] == "positionergroup":
            parts = fullname.split(":")
            if device["data_type"] != "principal":
                device["data_type"] = parts[-1]
            if multivalue_positioners:
                device["device_name"] = ":".join(parts[:-1])
                if device["data_type"] == "principal":
                    device["data_name"] = "value"
                else:
                    device["data_name"] = device["data_type"]
            else:
                device["data_name"] = "value"
                device["single"] = True
        elif device["device_type"] == "positioner":
            device["data_name"] = "value"
            device["data_type"] = "principal"
        else:
            device["data_name"] = "data"
            device["data_type"] = "principal"
        if device["single"] is None:
            device["single"] = device["data_type"] == "principal"
        if device["data_type"] == "principal":
            if device["device_type"] in ("positionergroup", "positioner"):
                device["data_alias"] = "value"
            else:
                device["data_alias"] = "data"
        if device["single"]:
            device["unique_name"] = device["device_name"]
        else:
            device["unique_name"] = device["device_name"] + ":" + device["data_name"]


Device = namedtuple(
    "Device",
    ["subscan", "fullname", "info", "is_positioner", "in_positioner_group", "index"],
)


def iter_devices(scan_info, scan_ndim=1):
    """
    :param dict scan_info:
    :param int scan_ndim:
    :yields Device:
    """
    for subscan_name, subscan_info in scan_info["acquisition_chain"].items():
        ntriggers = 0
        for group_name in subscan_info["devices"]:
            group_info = scan_info["devices"][group_name]
            is_trigger = len(group_info.get("triggered_devices", [])) > 0
            if is_trigger:
                ntriggers += 1
            is_first_trigger = is_trigger and ntriggers == 1
            group_size = len(group_info["channels"])
            for i, fullname in enumerate(group_info["channels"]):
                info = scan_info["channels"].get(fullname, dict())
                is_positioner = is_first_trigger and info.get("dim") == 0
                in_positioner_group = (
                    is_positioner
                    and scan_ndim <= 1
                    and group_size > 1
                    and ":" in fullname
                )
                yield Device(
                    subscan=subscan_name,
                    fullname=fullname,
                    info=info,
                    is_positioner=is_positioner,
                    in_positioner_group=in_positioner_group,
                    index=i,
                )


def device_info(
    devices, scan_info, scan_ndim=1, short_names=True, multivalue_positioners=False
):
    """Merge device information from `writer_config_publish.device_info`
    and from the scan info published by the Bliss core library.

    :param dict devices: as provided by `writer_config_publish.device_info`
    :param dict scan_info:
    :param int scan_ndim: number of scan dimensions
    :param bool short_names:
    :param bool multivalue_positioners:
    :returns dict: subscanname:dict(fullname:dict)
                   ordered according to position in
                   acquisition chain
    """
    ret = OrderedDict()
    device_save_keys = {"unit": "units"}
    for cdevice in iter_devices(scan_info, scan_ndim=scan_ndim):
        # Devices are grouped per subscan
        if cdevice.subscan not in ret:
            subscan_devices = ret[cdevice.subscan] = dict()

        # Initial device information from `writer_config_publish.device_info`
        subscan_devices[cdevice.fullname] = devices.get(cdevice.fullname, dict())

        # Add device information
        data_info = {
            kset: cdevice.info.get(kget, None)
            for kget, kset in device_save_keys.items()
        }
        device = update_device(subscan_devices, cdevice.fullname, data_info=data_info)

        # Add extra information for positioners
        if cdevice.is_positioner:
            if cdevice.in_positioner_group:
                device["device_type"] = "positionergroup"
                device["master_index"] = 0
            else:
                device["device_type"] = "positioner"
                device["master_index"] = cdevice.index
            if cdevice.index == 0:
                device["data_type"] = "principal"
            else:
                device["data_type"] = ""

    # Parse/normalize device information
    for devices in ret.values():
        parse_devices(
            devices,
            short_names=short_names,
            multivalue_positioners=multivalue_positioners,
        )
    return ret
