from typing import Iterable

from ..database.can.database import Database
from ..database.can.message import Message
from ..database.namedsignalvalue import NamedSignalValue
from ..typechecking import (
    ContainerDecodeResultType,
    ContainerUnpackResultType,
    SignalDictType,
)

MULTI_LINE_FMT = '''
{message}(
{signals}
)\
'''


def _format_signals(message, decoded_signals):
    formatted_signals = []

    for signal in message.signals:
        try:
            value = decoded_signals[signal.name]
        except KeyError:
            continue

        signal_name = signal.name

        if signal.unit is None or \
           isinstance(value, NamedSignalValue) or \
           isinstance(value, str):

            formatted_signal = f'{signal_name}: {value}'

        else:
            formatted_signal = f'{signal_name}: {value} {signal.unit}'

        formatted_signals.append(formatted_signal)

    return formatted_signals


def _format_message_single_line(message : Message,
                                formatted_signals : Iterable[str]) -> str:
    return ' {}({})'.format(message.name,
                            ', '.join(formatted_signals))


def _format_message_multi_line(message : Message,
                               formatted_signals : Iterable[str]) -> str:
    indented_signals = [
        '    ' + formatted_signal
        for formatted_signal in formatted_signals
    ]

    return MULTI_LINE_FMT.format(message=message.name,
                                 signals=',\n'.join(indented_signals))

def _format_container_single_line(message : Message,
                                  unpacked_data : ContainerUnpackResultType,
                                  decoded_data : ContainerDecodeResultType) \
                                  -> str:
    contained_list = []
    for i, (cm, signals) in enumerate(decoded_data):
        if isinstance(cm, Message):
            if isinstance(signals, bytes):
                formatted_cm = f'{cm.name}: Undecodable data: {signals.hex(" ")}'
                contained_list.append(formatted_cm)
            else:
                formatted_cm_signals = _format_signals(cm, signals)
                formatted_cm = _format_message_single_line(cm, formatted_cm_signals)
            contained_list.append(formatted_cm)
        else:
            header_id = cm
            data = unpacked_data[i][1]
            contained_list.append(
                f'(Unknown contained message: Header ID: 0x{header_id:x}, '
                f'Data: {bytes(data).hex()})')

    return f' {message.name}({", ".join(contained_list)})'


def _format_container_multi_line(message : Message,
                                 unpacked_data : ContainerUnpackResultType,
                                 decoded_data : ContainerDecodeResultType) -> str:
    contained_list = []
    for i, (cm, signals) in enumerate(decoded_data):
        if isinstance(cm, Message):
            if isinstance(signals, bytes):
                formatted_cm = f'    {cm.header_id:06x}##{signals.hex()} ::\n'
                formatted_cm += f'    {cm.name}: Undecodable data'
                contained_list.append(formatted_cm)
            else:
                formatted_cm_signals = _format_signals(cm, signals)
                formatted_cm = f'{cm.header_id:06x}##'
                formatted_cm += f'{bytes(unpacked_data[i][1]).hex()} ::'
                formatted_cm += _format_message_multi_line(cm, formatted_cm_signals)
                formatted_cm = formatted_cm.replace('\n', '\n    ')
                contained_list.append('    '+formatted_cm.strip())
        else:
            header_id = cm
            data = unpacked_data[i][1]
            contained_list.append(
                f'    {header_id:06x}##{data.hex()} ::\n'
                f'    Unknown contained message')

    return \
        f'\n{message.name}(\n' + \
        ',\n'.join(contained_list) + \
        '\n)'

def format_message_by_frame_id(dbase : Database,
                               frame_id : int,
                               data : bytes,
                               decode_choices : bool,
                               single_line : bool,
                               decode_containers : bool,
                               *,
                               allow_truncated: bool,
                               allow_excess: bool) -> str:
    try:
        message = dbase.get_message_by_frame_id(frame_id)
    except KeyError:
        return f' Unknown frame id {frame_id} (0x{frame_id:x})'

    if message.is_container:
        if decode_containers:
            return format_container_message(message,
                                            data,
                                            decode_choices,
                                            single_line,
                                            allow_truncated=allow_truncated,
                                            allow_excess=allow_excess)
        else:
            return f' Frame 0x{frame_id:x} is a container message'

    return format_message(message,
                          data,
                          decode_choices,
                          single_line,
                          allow_truncated=allow_truncated,
                          allow_excess=allow_excess)

def format_container_message(message : Message,
                             data : bytes,
                             decode_choices : bool,
                             single_line : bool,
                             *,
                             allow_truncated : bool,
                             allow_excess: bool) -> str:
    try:
        unpacked_message = message.unpack_container(data,
                                                    allow_truncated=allow_truncated)
        decoded_message = message.decode_container(data,
                                                   decode_choices=True,
                                                   scaling=True,
                                                   allow_truncated=allow_truncated,
                                                   allow_excess=allow_excess)

    except Exception as e:
        return ' ' + str(e)

    if single_line:
        return _format_container_single_line(message,
                                             unpacked_message,
                                             decoded_message)
    else:
        return _format_container_multi_line(message,
                                            unpacked_message,
                                            decoded_message)


def format_message(message : Message,
                   data : bytes,
                   decode_choices : bool,
                   single_line : bool,
                   allow_truncated : bool,
                   allow_excess : bool) -> str:
    try:
        decoded_signals = message.decode_simple(data,
                                                decode_choices,
                                                allow_truncated=allow_truncated,
                                                allow_excess=allow_excess)
    except Exception as e:
        return ' ' + str(e)

    formatted_signals = _format_signals(message, decoded_signals)

    if single_line:
        return _format_message_single_line(message, formatted_signals)
    else:
        return _format_message_multi_line(message, formatted_signals)

def format_multiplexed_name(message : Message,
                            data : bytes,
                            decode_choices : bool,
                            allow_truncated : bool,
                            allow_excess: bool) -> str:
    decoded_signals : SignalDictType \
        = message.decode(data,
                         decode_choices,
                         allow_truncated=allow_truncated,
                         allow_excess=allow_excess) # type: ignore

    # The idea here is that we rely on the sorted order of the Signals, and
    # then simply go through each possible Multiplexer and build a composite
    # key consisting of the Message name prepended to all the possible MUX
    # Signals (and their values). This composite key is therefore unique for
    # all the different possible enumerations of MUX values, which allows us
    # to display each MUXed Message on its own separate line.
    result = [message.name]

    for signal in message.signals:
        if signal.is_multiplexer:
            if signal.name in decoded_signals:
                result.append(str(decoded_signals[signal.name]))
            elif signal.raw_initial is not None:
                result.append(str(signal.raw_initial))
            else:
                result.append('0')

    return '__'.join(result)
