#!/usr/local/bin/python3.13

__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'

from collections import defaultdict, deque

from qt.core import QTextBlock, QTextBlockUserData, QTextCursor, QTextFormat, QTextLayout, QTimer

from calibre.gui2.tweak_book.editor.themes import highlight_to_char_format
from calibre.gui2.widgets import BusyCursor
from calibre.utils.icu import utf16_length
from polyglot.builtins import iteritems


def run_loop(user_data, state_map, formats, text):
    state = user_data.state
    i = 0
    fix_offsets = utf16_length(text) != len(text)
    seen_states = defaultdict(set)
    while i < len(text):
        orig_i = i
        seen_states[i].add(state.parse)
        fmt = state_map[state.parse](state, text, i, formats, user_data)
        for num, f in fmt:
            if num > 0:
                if fix_offsets:
                    # We need to map offsets/lengths from UCS-4 to UTF-16 in
                    # which non-BMP characters are two code points wide
                    yield utf16_length(text[:i]), utf16_length(text[i:i+num]), f
                else:
                    yield i, num, f
                i += num
        if orig_i == i and state.parse in seen_states[i]:
            # Something went wrong in the syntax highlighter
            print('Syntax highlighter returned a zero length format, parse state:', state.parse)
            break


class SimpleState:

    __slots__ = ('parse',)

    def __init__(self):
        self.parse = 0

    def copy(self):
        s = SimpleState()
        s.parse = self.parse
        return s


class SimpleUserData(QTextBlockUserData):

    def __init__(self):
        QTextBlockUserData.__init__(self)
        self.state = SimpleState()
        self.doc_name = None

    def clear(self, state=None, doc_name=None):
        self.state = SimpleState() if state is None else state
        self.doc_name = doc_name


class SyntaxHighlighter:

    def create_formats_func(highlighter):
        return {}
    spell_attributes = ()
    def tag_ok_for_spell(x):
        return False
    user_data_factory = SimpleUserData

    def __init__(self):
        self.doc = None
        self.doc_name = None
        self.requests = deque()
        self.ignore_requests = False

    @property
    def has_requests(self):
        return bool(self.requests)

    def apply_theme(self, theme):
        self.theme = {k:highlight_to_char_format(v) for k, v in iteritems(theme)}
        self.create_formats()
        self.rehighlight()

    def create_formats(self):
        self.formats = self.create_formats_func()

    def set_document(self, doc, doc_name=None):
        old_doc = self.doc
        if old_doc is not None:
            old_doc.contentsChange.disconnect(self.reformat_blocks)
            c = QTextCursor(old_doc)
            c.beginEditBlock()
            blk = old_doc.begin()
            while blk.isValid():
                blk.layout().clearFormats()
                blk = blk.next()
            c.endEditBlock()
        self.doc = self.doc_name = None
        if doc is not None:
            self.doc = doc
            self.doc_name = doc_name
            doc.contentsChange.connect(self.reformat_blocks)
            self.rehighlight()

    def rehighlight(self):
        doc = self.doc
        if doc is None:
            return
        lb = doc.lastBlock()
        with BusyCursor():
            self.reformat_blocks(0, 0, lb.position() + lb.length())

    def get_user_data(self, block):
        ud = block.userData()
        new_data = False
        if ud is None:
            ud = self.user_data_factory()
            block.setUserData(ud)
            new_data = True
        return ud, new_data

    def reformat_blocks(self, position, removed, added):
        doc = self.doc
        if doc is None or self.ignore_requests or not hasattr(self, 'state_map'):
            return

        block = doc.findBlock(position)
        if not block.isValid():
            return
        start_cursor = QTextCursor(block)
        last_block = doc.findBlock(position + added + (1 if removed > 0 else 0))
        if not last_block.isValid():
            last_block = doc.lastBlock()
        end_cursor = QTextCursor(last_block)
        end_cursor.movePosition(QTextCursor.MoveOperation.EndOfBlock)
        self.requests.append((start_cursor, end_cursor))
        QTimer.singleShot(0, self.do_one_block)

    def do_one_block(self):
        try:
            start_cursor, end_cursor = self.requests[0]
        except IndexError:
            return
        self.ignore_requests = True
        try:
            block = start_cursor.block()
            if not block.isValid():
                self.requests.popleft()
                return
            formats, force_next_highlight = self.parse_single_block(block)
            self.apply_format_changes(block, formats)
            try:
                self.doc.markContentsDirty(block.position(), block.length())
            except AttributeError:
                self.requests.clear()
                return
            ok = start_cursor.movePosition(QTextCursor.MoveOperation.NextBlock)
            if not ok:
                self.requests.popleft()
                return
            next_block = start_cursor.block()
            if next_block.position() > end_cursor.position():
                if force_next_highlight:
                    end_cursor.setPosition(next_block.position() + 1)
                else:
                    self.requests.popleft()
                return
        finally:
            self.ignore_requests = False
            QTimer.singleShot(0, self.do_one_block)

    def join(self):
        ''' Blocks until all pending highlighting requests are handled '''
        doc = self.doc
        if doc is None:
            self.requests.clear()
            return
        self.ignore_requests = True
        try:
            while self.requests:
                start_cursor, end_cursor = self.requests.popleft()
                block = start_cursor.block()
                last_block = end_cursor.block()
                if not last_block.isValid():
                    last_block = doc.lastBlock()
                end_pos = last_block.position() + last_block.length()
                force_next_highlight = False
                while block.isValid() and (force_next_highlight or block.position() < end_pos):
                    formats, force_next_highlight = self.parse_single_block(block)
                    self.apply_format_changes(block, formats)
                    doc.markContentsDirty(block.position(), block.length())
                    block = block.next()
        finally:
            self.ignore_requests = False

    @property
    def is_working(self):
        return bool(self.requests)

    def parse_single_block(self, block):
        ud, is_new_ud = self.get_user_data(block)
        orig_state = ud.state
        pblock = block.previous()
        if pblock.isValid():
            start_state = pblock.userData()
            if start_state is None:
                start_state = self.user_data_factory().state
            else:
                start_state = start_state.state.copy()
        else:
            start_state = self.user_data_factory().state
        ud.clear(state=start_state, doc_name=self.doc_name)  # Ensure no stale user data lingers
        formats = []
        for i, num, fmt in run_loop(ud, self.state_map, self.formats, str(block.text())):
            if fmt is not None:
                r = QTextLayout.FormatRange()
                r.start, r.length, r.format = i, num, fmt
                formats.append(r)
        force_next_highlight = is_new_ud or ud.state != orig_state
        return formats, force_next_highlight

    def reformat_block(self, block):
        if block.isValid():
            self.reformat_blocks(block.position(), 0, 1)

    def apply_format_changes(self, block, formats):
        layout = block.layout()
        preedit_start = layout.preeditAreaPosition()
        preedit_length = len(layout.preeditAreaText())
        if preedit_length != 0 and preedit_start != 0:
            for r in formats:
                # Adjust range by pre-edit text, if any
                if r.start >= preedit_start:
                    r.start += preedit_length
                elif r.start + r.length >= preedit_start:
                    r.length += preedit_length
        layout.setFormats(formats)

    def formats_for_line(self, block: QTextBlock, start, length):
        layout = block.layout()
        start_in_block = start - block.position()
        limit = start_in_block + length
        for f in layout.formats():
            if f.start >= start_in_block and f.start < limit and f.format.hasProperty(QTextFormat.Property.BackgroundBrush):
                yield f
