# -*- coding: utf-8 -*-

from twisted.internet import defer
from twisted.internet.error import ConnectionDone
from twisted.internet.protocol import (ClientFactory, DatagramProtocol, )
from twisted.protocols.basic import LineOnlyReceiver
from twisted.python import log

from il2ds_middleware.constants import (DEVICE_LINK_OPCODE as DL_OPCODE,
    DEVICE_LINK_PREFIXES, DEVICE_LINK_CMD_SEPARATOR as DL_CMD_SEP,
    DEVICE_LINK_ARGS_SEPARATOR as DL_ARGS_SEP, DEVICE_LINK_CMD_GROUP_MAX_SIZE,
    REQUEST_TIMEOUT, REQUEST_MISSION_LOAD_TIMEOUT, CHAT_MAX_LENGTH, )
from il2ds_middleware.parser import (ConsolePassthroughParser,
    DeviceLinkPassthroughParser, )
from il2ds_middleware.requests import (
    REQ_SERVER_INFO, REQ_MISSION_STATUS, REQ_MISSION_LOAD, REQ_MISSION_BEGIN,
    REQ_MISSION_END, REQ_MISSION_DESTROY, REQ_CHAT, CHAT_ALL, CHAT_USER,
    CHAT_ARMY, )


class ConsoleClient(LineOnlyReceiver):

    """Parser and timeout_value are set up by factory"""
    parser = None
    timeout_value = None

    def __init__(self):
        self._request_id = 0
        self._responce_id = None
        # { request_id: ([results], deferred, timeout, ), }
        self._requests = {}

    def connectionMade(self):
        self.factory.clientConnectionMade(self)

    def _generate_request_id(self):
        self._request_id += 1
        return self._request_id

    def _make_request(self, d, timeout_value):
        rid = self._generate_request_id()

        def on_timeout(_):
            del self._requests[rid]
            defer.timeout(d)

        from twisted.internet import reactor
        timeout = reactor.callLater(
            timeout_value or self.timeout_value, on_timeout, None)
        return rid, ([], d, timeout, )

    def _send_request(self, line, timeout_value=None):
        d = defer.Deferred()
        rid, request = self._make_request(d, timeout_value)
        self._requests[rid] = request

        wrapper = "rid|{0}".format(rid)
        self.sendLine(wrapper)
        self.sendLine(line)
        self.sendLine(wrapper)
        return d

    def lineReceived(self, line):
        if line.startswith("<consoleN><"):
            return
        self.got_line(line.replace("\\n", '').replace("\\u0020", ' '))

    def got_line(self, line):
        if line.startswith("Command not found: rid"):
            self._process_responce_id(line)
        elif self._responce_id:
            self._requests[self._responce_id][0].append(line)
        else:
            self.parser.parse_line(line)

    def _process_responce_id(self, line):
        try:
            rid = int(line.split('|')[1])
        except IndexError:
            log.err("RID format is malformed in \"{0}\"".format(line))
        except ValueError:
            log.err("Could not get RID value from \"{0}\"".format(line))
        else:
            if rid not in self._requests:
                log.err("Unexpected RID: {0}".format(rid))
                return
            if self._responce_id is None:
                # start request processing
                self._responce_id = rid
            elif self._responce_id == rid:
                # end request processing, send results to callback
                self._responce_id = None
                results, d, timeout = self._requests[rid]
                timeout.cancel()
                del self._requests[rid]
                d.callback(results)

    def server_info(self):
        d = self._send_request(REQ_SERVER_INFO)
        d.addCallback(self.parser.server_info)
        return d

    def mission_status(self, *args):
        d = self._send_request(REQ_MISSION_STATUS)
        d.addCallback(self.parser.mission_status)
        return d

    def mission_load(self, mission):
        d = self._send_request(
            REQ_MISSION_LOAD.format(mission), REQUEST_MISSION_LOAD_TIMEOUT)
        d.addCallback(self.parser.mission_status)
        return d

    def mission_begin(self):
        d = self._send_request(REQ_MISSION_BEGIN)
        d.addCallback(self.parser.mission_status)
        return d

    def mission_end(self):
        d = self._send_request(REQ_MISSION_END)
        d.addCallback(lambda _: self.mission_status())
        return d

    def mission_destroy(self):
        d = self._send_request(REQ_MISSION_DESTROY)
        d.addCallback(lambda _: self.mission_status())
        return d

    def chat_all(self, message):
        self._chat(message, CHAT_ALL)

    def chat_user(self, message, callsign):
        self._chat(message, CHAT_USER.format(callsign))

    def chat_army(self, message, army):
        self._chat(message, CHAT_ARMY.format(army))

    def _chat(self, message, suffix):
        last = 0
        total = len(message)
        while last < total:
            step = min(CHAT_MAX_LENGTH, total-last)
            chunk = message[last:last+step]
            self.sendLine(
                REQ_CHAT.format(chunk.encode('unicode-escape'), suffix))
            last += step


class ConsoleClientFactory(ClientFactory):

    protocol = ConsoleClient

    def __init__(self, parser=None, timeout_value=REQUEST_TIMEOUT):
        self._client = None
        self.parser = parser
        self.timeout_value = timeout_value
        self.on_connecting = defer.Deferred()
        self.on_connection_lost = defer.Deferred()

    def buildProtocol(self, addr):
        self._client = ClientFactory.buildProtocol(self, addr)
        parser, self.parser = self.parser or ConsolePassthroughParser(), None
        tv, self.timeout_value = self.timeout_value, None
        self._client.parser = parser
        self._client.timeout_value = tv
        return self._client

    def clientConnectionMade(self, client):
        if self.on_connecting is not None:
            d, self.on_connecting = self.on_connecting, None
            d.callback(client)

    def clientConnectionFailed(self, connector, reason):
        if self.on_connecting is not None:
            d, self.on_connecting = self.on_connecting, None
            d.errback(reason)

    def clientConnectionLost(self, connector, reason):
        if self.on_connection_lost is not None:
            d, self.on_connection_lost = self.on_connection_lost, None
            if isinstance(reason.value, ConnectionDone):
                d.callback(None)
            else:
                d.errback(reason)


class DeviceLinkProtocol(DatagramProtocol):

    def __init__(self, address=None, parser=None):
        self.address = address
        self.on_start = defer.Deferred()

    def startProtocol(self):
        if self.on_start is not None:
            d, self.on_start = self.on_start, None
            d.callback(None)

    def datagramReceived(self, data, (host, port)):
        if data.startswith(DEVICE_LINK_PREFIXES['answer']):
            processor = self.answers_received
        elif data.startswith(DEVICE_LINK_PREFIXES['request']):
            processor = self.requests_received
        else:
            log.err("Malformed data from {0}: \"{1}\"")
            return
        prepared_data = data[1:].strip(DL_CMD_SEP)
        payloads = self._parse(prepared_data)
        processor(payloads, (host, port))

    def _parse(self, data):
        results = []
        for chunk in data.split(DL_CMD_SEP):
            command = chunk.split(DL_ARGS_SEP)
            arg = command[1:]
            results.append((command[0], arg[0] if arg else None, ))
        return results

    def answers_received(self, answers, address):
        pass

    def requests_received(self, requests, address):
        pass

    def send_request(self, request, address=None):
        self.send_requests([request, ], address)

    def send_requests(self, requests, address=None):
        self._send_payloads(DEVICE_LINK_PREFIXES['request'], requests, address)

    def send_answer(self, answer, address=None):
        self.send_answers([answer, ], address)

    def send_answers(self, answers, address=None):
        self._send_payloads(DEVICE_LINK_PREFIXES['answer'], answers, address)

    def _send_payloads(self, prefix, payloads, address=None):
        data = DL_CMD_SEP.join(
            [prefix, self._format(payloads), ])
        self.transport.write(data, address or self.address)

    def _format(self, payloads):
        chunks = []
        for payload in payloads:
            cmd, arg = payload
            chunks.append(
                DL_ARGS_SEP.join([cmd, str(arg)]) if arg is not None else cmd)
        return DL_CMD_SEP.join(chunks)


class DeviceLinkClient(DeviceLinkProtocol):

    cmd_group_max_size = DEVICE_LINK_CMD_GROUP_MAX_SIZE

    def __init__(self, address, parser=None, timeout_value=REQUEST_TIMEOUT):
        self.parser = parser or DeviceLinkPassthroughParser()
        self.timeout_value = timeout_value
        DeviceLinkProtocol.__init__(self, address)
        # [ (opcode, deferred, timeout), ]
        self._requests = []

    def answers_received(self, answers, address):
        if address == self.address:
            for answer in answers:
                self._answer_received(answer)

    def _answer_received(self, answer):
        cmd, arg = answer
        for request in self._requests:
            opcode, d, timeout = request
            if opcode == cmd:
                timeout.cancel()
                self._requests.remove(request)
                d.callback(arg)
                break

    def _make_request(self, command, d, timeout_value=None):

        def on_timeout(_):
            self._requests.remove(request)
            defer.timeout(d)

        from twisted.internet import reactor
        timeout = reactor.callLater(
            timeout_value or self.timeout_value, on_timeout, None)
        request = (command[0], d, timeout, )
        return request

    def _deferred_request(self, command, timeout_value=None):
        d = defer.Deferred()
        self._requests.append(
            self._make_request(command, d, timeout_value))
        self.send_request(command)
        return d

    def _deferred_requests_iterator(self, commands):
        step = self.cmd_group_max_size
        count = len(commands)
        for i in xrange((count/step)+1):
            start = i*step
            yield commands[start:start+min((count-start), step)]

    def _deferred_requests_group(self, iterator, timeout_value=None):
        try:
            group = iterator.next()
        except StopIteration:
            return defer.succeed(None)
        else:
            dlist = []
            for cmd in group:
                d = defer.Deferred()
                self._requests.append(
                    self._make_request(cmd, d, timeout_value))
                dlist.append(d)
            self.send_requests(group)
            return defer.gatherResults(dlist)

    def _deferred_requests(self, commands, timeout_value=None):
        """
        Send requests in portions. Each next portion is send after result
        of the previous one was received. All results are collected in one
        place and are returned if there no requests has left.
        """
        all_results = []
        iterator = self._deferred_requests_iterator(commands)

        def on_results(results):
            if results:
                all_results.extend(results)
                return do_next()
            else:
                return all_results

        def do_next():
            return self._deferred_requests_group(
                iterator, timeout_value).addCallback(on_results)

        return do_next()

    def refresh_radar(self):
        self.send_request(DL_OPCODE.RADAR_REFRESH.make_command())
        return defer.succeed(None)

    def pilot_count(self):
        d = self._deferred_request(DL_OPCODE.PILOT_COUNT.make_command())
        d.addCallback(self.parser.pilot_count)
        return d

    def pilot_pos(self, index):
        d = self._deferred_request(DL_OPCODE.PILOT_POS.make_command(index))
        d.addCallback(self.parser.pilot_pos)
        return d

    def all_pilots_pos(self):
        d = self._all_pos(self.pilot_count, DL_OPCODE.PILOT_POS)
        d.addCallback(self.parser.all_pilots_pos)
        return d

    def static_count(self):
        d = self._deferred_request(DL_OPCODE.STATIC_COUNT.make_command())
        d.addCallback(self.parser.static_count)
        return d

    def static_pos(self, index):
        d = self._deferred_request(DL_OPCODE.STATIC_POS.make_command(index))
        d.addCallback(self.parser.static_pos)
        return d

    def all_static_pos(self):
        d = self._all_pos(self.static_count, DL_OPCODE.STATIC_POS)
        d.addCallback(self.parser.all_static_pos)
        return d

    def _all_pos(self, get_count, pos_opcode):

        def on_count(result):
            count = int(result)
            if not count:
                return []
            return self._deferred_requests([
                pos_opcode.make_command(i) for i in xrange(count)])

        return get_count().addCallback(on_count)
