# Lists
import logging

from ..common.utils import isStrSpace
from .state_block import StateBlock

LOGGER = logging.getLogger(__name__)


# Search `[-+*][\n ]`, returns next pos after marker on success
# or -1 on fail.
def skipBulletListMarker(state: StateBlock, startLine: int) -> int:
    pos = state.bMarks[startLine] + state.tShift[startLine]
    maximum = state.eMarks[startLine]

    try:
        marker = state.src[pos]
    except IndexError:
        return -1
    pos += 1

    if marker not in ("*", "-", "+"):
        return -1

    if pos < maximum:
        ch = state.src[pos]

        if not isStrSpace(ch):
            # " -test " - is not a list item
            return -1

    return pos


# Search `\d+[.)][\n ]`, returns next pos after marker on success
# or -1 on fail.
def skipOrderedListMarker(state: StateBlock, startLine: int) -> int:
    start = state.bMarks[startLine] + state.tShift[startLine]
    pos = start
    maximum = state.eMarks[startLine]

    # List marker should have at least 2 chars (digit + dot)
    if pos + 1 >= maximum:
        return -1

    ch = state.src[pos]
    pos += 1

    ch_ord = ord(ch)
    # /* 0 */  /* 9 */
    if ch_ord < 0x30 or ch_ord > 0x39:
        return -1

    while True:
        # EOL -> fail
        if pos >= maximum:
            return -1

        ch = state.src[pos]
        pos += 1

        # /* 0 */  /* 9 */
        ch_ord = ord(ch)
        if ch_ord >= 0x30 and ch_ord <= 0x39:
            # List marker should have no more than 9 digits
            # (prevents integer overflow in browsers)
            if pos - start >= 10:
                return -1

            continue

        # found valid marker
        if ch in (")", "."):
            break

        return -1

    if pos < maximum:
        ch = state.src[pos]

        if not isStrSpace(ch):
            # " 1.test " - is not a list item
            return -1

    return pos


def markTightParagraphs(state: StateBlock, idx: int) -> None:
    level = state.level + 2

    i = idx + 2
    length = len(state.tokens) - 2
    while i < length:
        if state.tokens[i].level == level and state.tokens[i].type == "paragraph_open":
            state.tokens[i + 2].hidden = True
            state.tokens[i].hidden = True
            i += 2
        i += 1


def list_block(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bool:
    LOGGER.debug("entering list: %s, %s, %s, %s", state, startLine, endLine, silent)

    isTerminatingParagraph = False
    tight = True

    if state.is_code_block(startLine):
        return False

    # Special case:
    #  - item 1
    #   - item 2
    #    - item 3
    #     - item 4
    #      - this one is a paragraph continuation
    if (
        state.listIndent >= 0
        and state.sCount[startLine] - state.listIndent >= 4
        and state.sCount[startLine] < state.blkIndent
    ):
        return False

    # limit conditions when list can interrupt
    # a paragraph (validation mode only)
    # Next list item should still terminate previous list item
    #
    # This code can fail if plugins use blkIndent as well as lists,
    # but I hope the spec gets fixed long before that happens.
    #
    if (
        silent
        and state.parentType == "paragraph"
        and state.sCount[startLine] >= state.blkIndent
    ):
        isTerminatingParagraph = True

    # Detect list type and position after marker
    posAfterMarker = skipOrderedListMarker(state, startLine)
    if posAfterMarker >= 0:
        isOrdered = True
        start = state.bMarks[startLine] + state.tShift[startLine]
        markerValue = int(state.src[start : posAfterMarker - 1])

        # If we're starting a new ordered list right after
        # a paragraph, it should start with 1.
        if isTerminatingParagraph and markerValue != 1:
            return False
    else:
        posAfterMarker = skipBulletListMarker(state, startLine)
        if posAfterMarker >= 0:
            isOrdered = False
        else:
            return False

    # If we're starting a new unordered list right after
    # a paragraph, first line should not be empty.
    if (
        isTerminatingParagraph
        and state.skipSpaces(posAfterMarker) >= state.eMarks[startLine]
    ):
        return False

    # We should terminate list on style change. Remember first one to compare.
    markerChar = state.src[posAfterMarker - 1]

    # For validation mode we can terminate immediately
    if silent:
        return True

    # Start list
    listTokIdx = len(state.tokens)

    if isOrdered:
        token = state.push("ordered_list_open", "ol", 1)
        if markerValue != 1:
            token.attrs = {"start": markerValue}

    else:
        token = state.push("bullet_list_open", "ul", 1)

    token.map = listLines = [startLine, 0]
    token.markup = markerChar

    #
    # Iterate list items
    #

    nextLine = startLine
    prevEmptyEnd = False
    terminatorRules = state.md.block.ruler.getRules("list")

    oldParentType = state.parentType
    state.parentType = "list"

    while nextLine < endLine:
        pos = posAfterMarker
        maximum = state.eMarks[nextLine]

        initial = offset = (
            state.sCount[nextLine]
            + posAfterMarker
            - (state.bMarks[startLine] + state.tShift[startLine])
        )

        while pos < maximum:
            ch = state.src[pos]

            if ch == "\t":
                offset += 4 - (offset + state.bsCount[nextLine]) % 4
            elif ch == " ":
                offset += 1
            else:
                break

            pos += 1

        contentStart = pos

        # trimming space in "-    \n  3" case, indent is 1 here
        indentAfterMarker = 1 if contentStart >= maximum else offset - initial

        # If we have more than 4 spaces, the indent is 1
        # (the rest is just indented code block)
        if indentAfterMarker > 4:
            indentAfterMarker = 1

        # "  -  test"
        #  ^^^^^ - calculating total length of this thing
        indent = initial + indentAfterMarker

        # Run subparser & write tokens
        token = state.push("list_item_open", "li", 1)
        token.markup = markerChar
        token.map = itemLines = [startLine, 0]
        if isOrdered:
            token.info = state.src[start : posAfterMarker - 1]

        # Detect GFM task checkbox: `[ ] ` or `[x] `/`[X] ` at content start
        checkboxLen = 0
        if state.md.options.get("tasklists", False) and contentStart < maximum:
            checked = _detect_task_checkbox(state.src, contentStart, maximum)
            if checked is not None:
                token.meta = {"checked": checked}
                # Advance content past the checkbox: `[x]` (3 chars) + whitespace.
                # `_detect_task_checkbox` already guarantees a whitespace char at
                # pos+3, so we always consume 4 characters.
                checkboxLen = 4

        # change current state, then restore it after parser subcall
        oldTight = state.tight
        oldBMark = state.bMarks[startLine]
        oldTShift = state.tShift[startLine]
        oldSCount = state.sCount[startLine]

        #  - example list
        # ^ listIndent position will be here
        #   ^ blkIndent position will be here
        #
        oldListIndent = state.listIndent
        state.listIndent = state.blkIndent
        state.blkIndent = indent

        state.tight = True
        state.tShift[startLine] = contentStart - state.bMarks[startLine]
        state.sCount[startLine] = offset

        # If we detected a checkbox, advance bMarks past it so that
        # getLines() doesn't include the checkbox text in the content.
        if checkboxLen:
            state.bMarks[startLine] = contentStart + checkboxLen
            state.tShift[startLine] = 0

        if contentStart >= maximum and state.isEmpty(startLine + 1):
            # workaround for this case
            # (list item is empty, list terminates before "foo"):
            # ~~~~~~~~
            #   -
            #
            #     foo
            # ~~~~~~~~
            state.line = min(state.line + 2, endLine)
        else:
            # NOTE in list.js this was:
            # state.md.block.tokenize(state, startLine, endLine, True)
            # but  tokeniz does not take the final parameter
            state.md.block.tokenize(state, startLine, endLine)

        # If any of list item is tight, mark list as tight
        if (not state.tight) or prevEmptyEnd:
            tight = False

        # Item become loose if finish with empty line,
        # but we should filter last element, because it means list finish
        prevEmptyEnd = (state.line - startLine) > 1 and state.isEmpty(state.line - 1)

        state.blkIndent = state.listIndent
        state.listIndent = oldListIndent
        if checkboxLen:
            state.bMarks[startLine] = oldBMark
        state.tShift[startLine] = oldTShift
        state.sCount[startLine] = oldSCount
        state.tight = oldTight

        token = state.push("list_item_close", "li", -1)
        token.markup = markerChar

        nextLine = startLine = state.line
        itemLines[1] = nextLine

        if nextLine >= endLine:
            break

        contentStart = state.bMarks[startLine]

        #
        # Try to check if list is terminated or continued.
        #
        if state.sCount[nextLine] < state.blkIndent:
            break

        if state.is_code_block(startLine):
            break

        # fail if terminating block found
        terminate = False
        for terminatorRule in terminatorRules:
            if terminatorRule(state, nextLine, endLine, True):
                terminate = True
                break

        if terminate:
            break

        # fail if list has another type
        if isOrdered:
            posAfterMarker = skipOrderedListMarker(state, nextLine)
            if posAfterMarker < 0:
                break
            start = state.bMarks[nextLine] + state.tShift[nextLine]
        else:
            posAfterMarker = skipBulletListMarker(state, nextLine)
            if posAfterMarker < 0:
                break

        if markerChar != state.src[posAfterMarker - 1]:
            break

    # Finalize list

    # If any direct list item has a task checkbox, add class to the list
    if state.md.options.get("tasklists", False):
        containsTask = False
        level = state.tokens[listTokIdx].level
        for j in range(listTokIdx + 1, len(state.tokens)):
            tok = state.tokens[j]
            if (
                tok.level == level + 1
                and tok.type == "list_item_open"
                and tok.meta
                and "checked" in tok.meta
            ):
                tok.attrJoin("class", "task-list-item")
                containsTask = True
        if containsTask:
            state.tokens[listTokIdx].attrJoin("class", "contains-task-list")

    if isOrdered:
        token = state.push("ordered_list_close", "ol", -1)
    else:
        token = state.push("bullet_list_close", "ul", -1)

    token.markup = markerChar

    listLines[1] = nextLine
    state.line = nextLine

    state.parentType = oldParentType

    # mark paragraphs tight if needed
    if tight:
        markTightParagraphs(state, listTokIdx)

    return True


def _detect_task_checkbox(src: str, pos: int, maximum: int) -> bool | None:
    """Detect ``[ ]``, ``[x]``, or ``[X]`` at *pos*, followed by whitespace.

    Returns ``True`` (checked), ``False`` (unchecked), or ``None`` (no match).
    """
    # Need at least 4 chars: `[`, char, `]`, whitespace
    if pos + 4 > maximum:
        return None
    if src[pos] != "[":
        return None
    inner = src[pos + 1]
    if src[pos + 2] != "]":
        return None
    if inner == " ":
        checked = False
    elif inner in ("x", "X"):
        checked = True
    else:
        return None
    # After `]`, must have whitespace
    if src[pos + 3] not in (" ", "\t"):
        return None
    return checked
