import attr
import structlog

from dlms_cosem.exceptions import LocalDlmsProtocolError
from dlms_cosem.protocol import acse, xdlms

LOG = structlog.get_logger()


class _SentinelBase(type):
    """
    Sentinel values

     - Inherit identity-based comparison and hashing from object
     - Have a nice repr
     - Have a *bonus property*: type(sentinel) is sentinel

     The bonus property is useful if you want to take the return value from
     next_event() and do some sort of dispatch based on type(event).

     Taken from h11.
    """

    def __repr__(self):
        return self.__name__


# Some simple flow control classes


@attr.s()
class HlsStart:
    pass


@attr.s()
class HlsSuccess:
    pass


@attr.s()
class HlsFailed:
    pass


@attr.s()
class RejectAssociation:
    pass


@attr.s()
class EndAssociation:
    """
    Is used when settings.use_rlrq_rlre == False to send the state to NO_ASSOCIATION
    """
    pass


def make_sentinel(name):
    cls = _SentinelBase(name, (_SentinelBase,), {})
    cls.__class__ = cls
    return cls


NO_ASSOCIATION = make_sentinel("NO_ASSOCIATION")

AWAITING_ASSOCIATION_RESPONSE = make_sentinel("AWAITING_ASSOCIATION_RESPONSE")

READY = make_sentinel("READY")

AWAITING_RELEASE_RESPONSE = make_sentinel("AWAITING_RELEASE_RESPONSE")
AWAITING_ACTION_RESPONSE = make_sentinel("AWAITING_ACTION_RESPONSE")
AWAITING_GET_RESPONSE = make_sentinel("AWAITING_GET_RESPONSE")
AWAITING_GET_BLOCK_RESPONSE = make_sentinel("AWAITING_GET_BLOCK_RESPONSE")
SHOULD_ACK_LAST_GET_BLOCK = make_sentinel("SHOULD_ACK_LAST_GET_BLOCK")
AWAITING_SET_RESPONSE = make_sentinel("AWAITING_SET_RESPONSE")

SHOULD_SEND_HLS_SEVER_CHALLENGE_RESULT = make_sentinel(
    "SHOULD_SEND_HLS_SEVER_CHALLENGE_RESULT"
)
AWAITING_HLS_CLIENT_CHALLENGE_RESULT = make_sentinel(
    "AWAITING_HLS_CLIENT_CHALLENGE_RESULT"
)
HLS_DONE = make_sentinel("HLS_DONE")

NEED_DATA = make_sentinel("NEED_DATA")

# TODO: block handling is not working with this state layout.

DLMS_STATE_TRANSITIONS = {
    NO_ASSOCIATION: {acse.ApplicationAssociationRequest: AWAITING_ASSOCIATION_RESPONSE},
    AWAITING_ASSOCIATION_RESPONSE: {
        acse.ApplicationAssociationResponse: READY,
        xdlms.ExceptionResponse: NO_ASSOCIATION,
    },
    READY: {
        acse.ReleaseRequest: AWAITING_RELEASE_RESPONSE,
        xdlms.GetRequestNormal: AWAITING_GET_RESPONSE,
        xdlms.GetRequestWithList: AWAITING_GET_RESPONSE,
        xdlms.SetRequestNormal: AWAITING_SET_RESPONSE,
        HlsStart: SHOULD_SEND_HLS_SEVER_CHALLENGE_RESULT,
        RejectAssociation: NO_ASSOCIATION,
        xdlms.ActionRequestNormal: AWAITING_ACTION_RESPONSE,
        xdlms.DataNotification: READY,
        EndAssociation: NO_ASSOCIATION,
    },
    SHOULD_SEND_HLS_SEVER_CHALLENGE_RESULT: {
        xdlms.ActionRequestNormal: AWAITING_HLS_CLIENT_CHALLENGE_RESULT
    },
    AWAITING_HLS_CLIENT_CHALLENGE_RESULT: {
        xdlms.ActionResponseNormalWithData: HLS_DONE,
        xdlms.ActionResponseNormal: NO_ASSOCIATION,
        xdlms.ActionResponseNormalWithError: NO_ASSOCIATION,
    },
    HLS_DONE: {HlsSuccess: READY, HlsFailed: NO_ASSOCIATION},
    AWAITING_GET_RESPONSE: {
        xdlms.GetResponseNormal: READY,
        xdlms.GetResponseWithList: READY,
        xdlms.GetResponseWithBlock: SHOULD_ACK_LAST_GET_BLOCK,
        xdlms.GetResponseNormalWithError: READY,
        xdlms.ExceptionResponse: READY,
    },
    AWAITING_GET_BLOCK_RESPONSE: {
        xdlms.GetResponseWithBlock: SHOULD_ACK_LAST_GET_BLOCK,
        xdlms.GetResponseNormalWithError: READY,
        xdlms.ExceptionResponse: READY,
        xdlms.GetResponseLastBlockWithError: READY,
        xdlms.GetResponseLastBlock: READY,
    },
    AWAITING_SET_RESPONSE: {xdlms.SetResponseNormal: READY},
    AWAITING_ACTION_RESPONSE: {
        xdlms.ActionResponseNormal: READY,
        xdlms.ActionResponseNormalWithData: READY,
        xdlms.ActionResponseNormalWithError: READY,
    },
    SHOULD_ACK_LAST_GET_BLOCK: {xdlms.GetRequestNext: AWAITING_GET_BLOCK_RESPONSE},
    AWAITING_RELEASE_RESPONSE: {
        acse.ReleaseResponse: NO_ASSOCIATION,
        xdlms.ExceptionResponse: READY,
    },
}


@attr.s(auto_attribs=True)
class DlmsConnectionState:
    """
    Handles state changes in DLMS, we only focus on Client implementation as of now.

    A DLMS event is passed to `process_event` and it moves the state machine to the
    correct state. If an event is processed that is not set to be able to transition
    the state in the current state a LocalProtocolError is raised.
    """

    current_state: _SentinelBase = attr.ib(default=NO_ASSOCIATION)

    def process_event(self, event):

        self._transition_state(type(event))

    def _transition_state(self, event_type):
        try:
            new_state = DLMS_STATE_TRANSITIONS[self.current_state][event_type]
        except KeyError:
            raise LocalDlmsProtocolError(
                f"can't handle event type {event_type} when state={self.current_state}"
            )
        old_state = self.current_state
        self.current_state = new_state
        LOG.debug(f"DLMS state transitioned", old_state=old_state, new_state=new_state)
