"""A utility class for a code container.

A code container is a class which holds source code for a debugger.  It knows how
to color the text, and also how to translate lines into offsets, and back.
"""

from __future__ import annotations

import os
import sys
import tokenize
from keyword import kwlist
from typing import Any

import win32api
import winerror
from win32com.axdebug import axdebug, contexts
from win32com.axdebug.util import _wrap
from win32com.server.exception import COMException

_keywords = {
    _keyword
    for _keyword in kwlist
    # Avoids including True/False/None
    if _keyword.islower()
}
"""set of Python keywords"""


class SourceCodeContainer:
    def __init__(
        self,
        text: str | None,
        fileName="<Remove Me!>",
        sourceContext=0,
        startLineNumber=0,
        site=None,
        debugDocument=None,
    ):
        self.sourceContext = sourceContext  # The source context added by a smart host.
        self.text: str | None = text
        if text:
            self._buildlines()
        self.nextLineNo = 0
        self.fileName = fileName
        # Any: PyIDispatch type is not statically exposed
        self.codeContexts: dict[int, Any] = {}
        self.site = site
        self.startLineNumber = startLineNumber
        self.debugDocument = debugDocument

    def _Close(self):
        self.text = self.lines = self.lineOffsets = None
        self.codeContexts = None
        self.debugDocument = None
        self.site = None
        self.sourceContext = None

    def GetText(self):
        return self.text

    def GetName(self, dnt):
        raise NotImplementedError("You must subclass this")

    def GetFileName(self):
        return self.fileName

    def GetPositionOfLine(self, cLineNumber):
        self.GetText()  # Prime us.
        try:
            return self.lineOffsets[cLineNumber]
        except IndexError:
            raise COMException(scode=winerror.S_FALSE)

    def GetLineOfPosition(self, charPos):
        self.GetText()  # Prime us.
        lastOffset = 0
        lineNo = 0
        for lineOffset in self.lineOffsets[1:]:
            if lineOffset > charPos:
                break
            lastOffset = lineOffset
            lineNo += 1
        else:  # for not broken.
            # print("Can't find", charPos, "in", self.lineOffsets)
            raise COMException(scode=winerror.S_FALSE)
        # print("GLOP ret=", lineNo, (charPos - lastOffset))
        return lineNo, (charPos - lastOffset)

    def GetNextLine(self):
        if self.nextLineNo >= len(self.lines):
            self.nextLineNo = 0  # auto-reset.
            return ""
        rc = self.lines[self.nextLineNo]
        self.nextLineNo += 1
        return rc

    def GetLine(self, num):
        self.GetText()  # Prime us.
        return self.lines[num]

    def GetNumChars(self):
        return len(self.GetText())

    def GetNumLines(self):
        self.GetText()  # Prime us.
        return len(self.lines)

    def _buildline(self, pos):
        i = self.text.find("\n", pos)
        if i < 0:
            newpos = len(self.text)
        else:
            newpos = i + 1
        r = self.text[pos:newpos]
        return r, newpos

    def _buildlines(self):
        self.lines = []
        self.lineOffsets = [0]
        line, pos = self._buildline(0)
        while line:
            self.lines.append(line)
            self.lineOffsets.append(pos)
            line, pos = self._buildline(pos)

    def _ProcessToken(self, type, token, spos, epos, line):
        srow, scol = spos
        erow, ecol = epos
        self.GetText()  # Prime us.
        linenum = srow - 1  # Lines zero based for us too.
        realCharPos = self.lineOffsets[linenum] + scol
        numskipped = realCharPos - self.lastPos
        if numskipped == 0:
            pass
        elif numskipped == 1:
            self.attrs.append(axdebug.SOURCETEXT_ATTR_COMMENT)
        else:
            self.attrs.append((axdebug.SOURCETEXT_ATTR_COMMENT, numskipped))
        kwSize = len(token)
        self.lastPos = realCharPos + kwSize
        attr = 0

        if type == tokenize.NAME:
            if token in _keywords:
                attr = axdebug.SOURCETEXT_ATTR_KEYWORD
        elif type == tokenize.STRING:
            attr = axdebug.SOURCETEXT_ATTR_STRING
        elif type == tokenize.NUMBER:
            attr = axdebug.SOURCETEXT_ATTR_NUMBER
        elif type == tokenize.OP:
            attr = axdebug.SOURCETEXT_ATTR_OPERATOR
        elif type == tokenize.COMMENT:
            attr = axdebug.SOURCETEXT_ATTR_COMMENT
        # else attr remains zero...
        if kwSize == 0:
            pass
        elif kwSize == 1:
            self.attrs.append(attr)
        else:
            self.attrs.append((attr, kwSize))

    def GetSyntaxColorAttributes(self):
        self.lastPos = 0
        self.attrs = []
        try:
            for tokens in tokenize.tokenize(self.GetNextLine):
                self._ProcessToken(*tokens)
        except tokenize.TokenError:
            pass  # Ignore - will cause all subsequent text to be commented.
        numAtEnd = len(self.GetText()) - self.lastPos
        if numAtEnd:
            self.attrs.append((axdebug.SOURCETEXT_ATTR_COMMENT, numAtEnd))
        return self.attrs

    # We also provide and manage DebugDocumentContext objects
    def _MakeDebugCodeContext(self, lineNo, charPos, len):
        return _wrap(
            contexts.DebugCodeContext(lineNo, charPos, len, self, self.site),
            axdebug.IID_IDebugCodeContext,
        )

    # Make a context at the given position.  It should take up the entire context.
    def _MakeContextAtPosition(self, charPos):
        lineNo, offset = self.GetLineOfPosition(charPos)
        try:
            endPos = self.GetPositionOfLine(lineNo + 1)
        except:
            endPos = charPos
        codecontext = self._MakeDebugCodeContext(lineNo, charPos, endPos - charPos)
        return codecontext

    # Returns a DebugCodeContext.  debugDocument can be None for smart hosts.
    def GetCodeContextAtPosition(self, charPos):
        # trace("GetContextOfPos", charPos, maxChars)
        # Convert to line number.
        lineNo, offset = self.GetLineOfPosition(charPos)
        charPos = self.GetPositionOfLine(lineNo)
        try:
            cc = self.codeContexts[charPos]
        except KeyError:
            cc = self._MakeContextAtPosition(charPos)
            self.codeContexts[charPos] = cc
        return cc


class SourceModuleContainer(SourceCodeContainer):
    def __init__(self, module):
        self.module = module
        if hasattr(module, "__file__"):
            fname = self.module.__file__
            # Check for .pyc or .pyo or even .pys!
            if fname[-1] in ["O", "o", "C", "c", "S", "s"]:
                fname = fname[:-1]
            try:
                fname = win32api.GetFullPathName(fname)
            except win32api.error:
                pass
        else:
            if module.__name__ == "__main__" and len(sys.argv) > 0:
                fname = sys.argv[0]
            else:
                fname = "<Unknown!>"
        SourceCodeContainer.__init__(self, None, fname)

    def GetText(self):
        if self.text is None:
            fname = self.GetFileName()
            if fname:
                try:
                    self.text = open(fname, "r").read()
                except OSError as details:
                    self.text = f"# COMException opening file\n# {details!r}"
            else:
                self.text = f"# No file available for module '{self.module}'"
            self._buildlines()
        return self.text

    def GetName(self, dnt):
        name = self.module.__name__
        try:
            fname = win32api.GetFullPathName(self.module.__file__)
        except win32api.error:
            fname = self.module.__file__
        except AttributeError:
            fname = name
        if dnt == axdebug.DOCUMENTNAMETYPE_APPNODE:
            return name.split(".")[-1]
        elif dnt == axdebug.DOCUMENTNAMETYPE_TITLE:
            return fname
        elif dnt == axdebug.DOCUMENTNAMETYPE_FILE_TAIL:
            return os.path.split(fname)[1]
        elif dnt == axdebug.DOCUMENTNAMETYPE_URL:
            return f"file:{fname}"
        else:
            raise COMException(scode=winerror.E_UNEXPECTED)


if __name__ == "__main__":
    from Test import ttest

    sc = SourceModuleContainer(ttest)
    # sc = SourceCodeContainer(open(sys.argv[1], "rb").read(), sys.argv[1])
    attrs = sc.GetSyntaxColorAttributes()
    attrlen = 0
    for attr in attrs:
        if isinstance(attr, tuple):
            attrlen += attr[1]
        else:
            attrlen += 1
    text = sc.GetText()
    if attrlen != len(text):
        print(f"Lengths don't match!!! ({attrlen}/{len(text)})")

    # print("Attributes:")
    # print(attrs)
    print("GetLineOfPos=", sc.GetLineOfPosition(0))
    print("GetLineOfPos=", sc.GetLineOfPosition(4))
    print("GetLineOfPos=", sc.GetLineOfPosition(10))
