import asyncio
import sys
import random
import time
import platform
import subprocess
import getpass
import os
import shutil

from pychess.System.Log import log
from pychess.System.prefix import getEngineDataPrefix
from pychess.ic.icc import B_DTGR_END, B_UNIT_END

if not hasattr(asyncio.StreamReader, "readuntil"):
    from pychess.System.readuntil import readuntil, _wait_for_data

    asyncio.StreamReader.readuntil = readuntil
    asyncio.StreamReader._wait_for_data = _wait_for_data

ENCODE = [ord(i) for i in "Timestamp (FICS) v1.0 - programmed by Henrik Gram."]
ENCODELEN = len(ENCODE)
G_RESPONSE = "\x029"
FILLER = b"1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
# was: b"".join([telnetlib.IAC, telnetlib.WONT, telnetlib.ECHO])
# but telnetlib was removed in Python 3.13
IAC_WONT_ECHO = b"\xff\xfc\x01"

_DEFAULT_LIMIT = 2**16


class CanceledException(Exception):
    pass


class ICSStreamReader(asyncio.StreamReader):
    def __init__(self, limit, loop, connected_event, name):
        asyncio.StreamReader.__init__(self, limit=limit, loop=loop)
        self.connected_event = connected_event
        self.name = name

    async def read_until(self, *untils):
        while True:
            for i, until in enumerate(untils):
                start = self._buffer.find(bytearray(until, "ascii"))
                if start >= 0:
                    self._buffer = self._buffer[start:]
                    return i
                else:
                    not_find = f"read_until:{until} , got:'{self._buffer}'"
                    log.debug(not_find, extra={"task": (self.name, "raw")})

            await self._wait_for_data("read_until")


class ICSStreamReaderProtocol(asyncio.StreamReaderProtocol):
    def __init__(self, stream_reader, client_connected_cb, loop, name, timeseal):
        asyncio.StreamReaderProtocol.__init__(
            self, stream_reader, client_connected_cb=client_connected_cb, loop=loop
        )
        self.name = name
        self.timeseal = timeseal
        self.connected = False
        self.stateinfo = None

    def data_received(self, data):
        cooked = self.cook_some(data)
        self._stream_reader.feed_data(cooked)

    def cook_some(self, data):
        if not self.connected:
            log.debug(data, extra={"task": (self.name, "raw")})
            self.connected = True
            if b"FatICS" in data:
                self.FatICS = True
            elif b"puertorico.com" in data:
                self.USCN = True
                data = data.replace(IAC_WONT_ECHO, b"")
            elif b"chessclub.com" in data:
                self.ICC = True
                data = data.replace(IAC_WONT_ECHO, b"")
            elif b"Starting FICS session" in data:
                data = data.replace(IAC_WONT_ECHO, b"")
        else:
            if self.timeseal:
                data, g_count, self.stateinfo = self.decode(data, self.stateinfo)
            data = data.replace(b"\r", b"").replace(b"\x07", b"")
            # enable this only for temporary debugging
            log.debug(data, extra={"task": (self.name, "raw")})

            if self.timeseal:
                for i in range(g_count):
                    self._stream_writer.write(
                        self.encode(bytearray(G_RESPONSE, "utf-8")) + b"\n"
                    )
        return data

    def encode(self, inbuf, timestamp=None):
        assert inbuf == b"" or inbuf[-1] != b"\n"

        if not timestamp:
            timestamp = int(time.time() * 1000 % 1e7)
        enc = inbuf + bytearray("\x18%d\x19" % timestamp, "ascii")
        padding = 12 - len(enc) % 12
        filler = random.sample(FILLER, padding)
        enc += bytearray(filler)

        buf = enc

        for i in range(0, len(buf), 12):
            buf[i + 11], buf[i] = buf[i], buf[i + 11]
            buf[i + 9], buf[i + 2] = buf[i + 2], buf[i + 9]
            buf[i + 7], buf[i + 4] = buf[i + 4], buf[i + 7]

        encode_offset = random.randrange(ENCODELEN)

        for i in range(len(buf)):
            buf[i] |= 0x80
            j = (i + encode_offset) % ENCODELEN
            buf[i] = (buf[i] ^ ENCODE[j]) - 32

        buf += bytearray([0x80 | encode_offset])
        return buf

    def decode(self, buf, stateinfo=None):
        expected_table = b"[G]\n\r"
        # TODO: add support to FatICS's new zipseal protocol when it finalizes
        # expected_table = "[G]\n\r" if not self.FatICS else "[G]\r\n"
        final_state = len(expected_table)
        g_count = 0
        result = []

        if stateinfo:
            state, lookahead = stateinfo
        else:
            state, lookahead = 0, []

        lenb = len(buf)
        idx = 0
        while idx < lenb:
            buffer_item = buf[idx]
            expected = expected_table[state]
            if buffer_item == expected:
                state += 1
                if state == final_state:
                    g_count += 1
                    lookahead = []
                    state = 0
                else:
                    lookahead.append(buffer_item)
                idx += 1
            elif state == 0:
                result.append(buffer_item)
                idx += 1
            else:
                result.extend(lookahead)
                lookahead = []
                state = 0

        return bytearray(result), g_count, (state, lookahead)


# You can get ICC timestamp from
# https://www.chessclub.com/user/resources/icc/timestamp/
if sys.platform == "win32":
    timestamp = "timestamp_win32.exe"
else:
    timestamp = "timestamp_linux_2.6.8"

altpath = getEngineDataPrefix()
timestamp_path = shutil.which(timestamp, os.X_OK, path=altpath)


class ICSTelnet:
    sensitive = False

    def __init__(self, timeseal):
        self.name = ""
        self.connected = False
        self.canceled = False
        self.FatICS = False
        self.USCN = False
        self.ICC = False
        self.timeseal = timeseal
        self.ICC_buffer = ""

    async def start(self, host, port, connected_event):
        if self.canceled:
            raise CanceledException()
        self.port = port
        self.host = host
        self.connected_event = connected_event

        self.name = host

        if host == "chessclub.com":
            self.ICC = True

            if self.timeseal and timestamp_path is not None:
                self.host = "127.0.0.1"
                self.port = 5500
                try:
                    if sys.platform == "win32":
                        # To prevent engines opening console window
                        startupinfo = subprocess.STARTUPINFO()
                        startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
                    else:
                        startupinfo = None
                    create = asyncio.create_subprocess_exec(
                        *["%s" % timestamp_path, "-p", "%s" % self.port],
                        startupinfo=startupinfo,
                    )
                    self.timestamp_proc = await create
                    log.info("%s started OK" % timestamp_path)
                except OSError as err:
                    log.info(
                        "Can't start {} OSError: {} {}".format(
                            timestamp_path, err.errno, err.strerror
                        )
                    )
                    self.port = port
                    self.host = host
            else:
                log.info("%s not found" % timestamp_path)

            self.timeseal = False

        def cb(reader, writer):
            reader.stream_writer = writer
            reader.connected_event.set()

        loop = asyncio.get_event_loop()
        self.reader = ICSStreamReader(
            _DEFAULT_LIMIT, loop, self.connected_event, self.name
        )
        self.protocol = ICSStreamReaderProtocol(
            self.reader, cb, loop, self.name, self.timeseal
        )
        coro = loop.create_connection(lambda: self.protocol, self.host, self.port)
        self.transport, _protocol = await coro
        # writer = asyncio.StreamWriter(transport, protocol, reader, loop)

        if self.timeseal:
            self.write(self.get_init_string())

    def cancel(self):
        self.canceled = True
        self.close()

    def close(self):
        if self.protocol.connected:
            self.protocol.connected = False
            self.transport.close()

    def get_init_string(self):
        """timeseal header: TIMESTAMP|bruce|Linux gruber 2.6.15-gentoo-r1 #9
        PREEMPT Thu Feb 9 20:09:47 GMT 2006 i686 Intel(R) Celeron(R) CPU
        2.00GHz GenuineIntel GNU/Linux| 93049"""
        user = getpass.getuser()
        uname = " ".join(list(platform.uname()))
        return "TIMESTAMP|" + user + "|" + uname + "|"

    def write(self, string):
        logstr = "*" * len(string) if self.sensitive else string
        self.sensitive = False
        log.info(logstr, extra={"task": (self.name, "raw")})

        if self.timeseal:
            self.transport.write(
                self.protocol.encode(bytearray(string, "utf-8")) + b"\n"
            )
        else:
            self.transport.write(string.encode() + b"\n")

    async def readline(self):
        if self.canceled:
            raise CanceledException()

        if self.ICC:
            line = await self.readuntil(b"\n")
            return line.strip()
        else:
            line = await self.reader.readline()
            return line.decode("latin_1").strip()

    async def readuntil(self, until):
        if self.canceled:
            raise CanceledException()

        if self.ICC:
            if len(self.ICC_buffer) == 0:
                self.ICC_buffer = await self.reader.readuntil(until)
            i = self.ICC_buffer.find(until)
            m = sys.maxsize
            if i >= 0:
                m = i
            j = self.ICC_buffer.find(B_DTGR_END)
            if j >= 0:
                m = min(m, j)
            k = self.ICC_buffer.find(B_UNIT_END)
            if k >= 0:
                m = min(m, k)
            if m != sys.maxsize:
                if m == i:
                    stuff = self.ICC_buffer[: m + len(until)]
                    self.ICC_buffer = self.ICC_buffer[m + len(until) :]
                    return stuff.decode("latin_1")
                else:
                    stuff = self.ICC_buffer[: m + 2]
                    self.ICC_buffer = self.ICC_buffer[m + 2 :]
                    return stuff.decode("latin_1")
            else:
                return ""
        else:
            data = await self.reader.readuntil(until)
            return data.decode("latin_1")

    async def read_until(self, *untils):
        if self.canceled:
            raise CanceledException()

        ret = await self.reader.read_until(*untils)
        return ret
