import subprocess
from typing import List, Optional, Set

from zulint.printer import ENDC, GREEN

from .template_parser import Token, is_django_block_tag, tokenize


def requires_indent(line: str) -> bool:
    line = line.lstrip()
    return line.startswith("<")


def open_token(token: Token) -> bool:
    if token.kind in (
        "handlebars_start",
        "html_start",
    ):
        return True

    if token.kind in (
        "django_start",
        "jinja2_whitespace_stripped_start",
        "jinja2_whitespace_stripped_type2_start",
    ):
        return is_django_block_tag(token.tag)

    return False


def close_token(token: Token) -> bool:
    return token.kind in (
        "django_end",
        "handlebars_end",
        "html_end",
        "jinja2_whitespace_stripped_end",
    )


def else_token(token: Token) -> bool:
    return token.kind in (
        "django_else",
        "handlebars_else",
    )


def pop_unused_tokens(tokens: List[Token], row: int) -> bool:
    while tokens and tokens[-1].line <= row:
        token = tokens.pop()
        if close_token(token):
            return True
    return False


def indent_pref(row: int, tokens: List[Token], line: str) -> str:
    opens = 0
    closes = 0
    is_else = False

    while tokens and tokens[-1].line == row:
        token = tokens.pop()
        if open_token(token):
            opens += 1
        elif close_token(token):
            closes += 1
        elif else_token(token):
            is_else = True

    if is_else:
        if opens and closes:
            return "neutral"
        return "else"

    i = opens - closes
    if i == 0:
        return "neutral"
    elif i == 1:
        return "open"
    elif i == -1:
        return "close"
    else:
        print(i, opens, closes)
        raise Exception(f"too many tokens on row {row}")


def indent_level(s: str) -> int:
    return len(s) - len(s.lstrip())


def same_indent(s1: str, s2: str) -> bool:
    return indent_level(s1) == indent_level(s2)


def next_non_blank_line(lines: List[str], i: int) -> str:
    next_line = ""
    for j in range(i + 1, len(lines)):
        next_line = lines[j]
        if next_line.strip() != "":
            break
    return next_line


def get_exempted_lines(tokens: List[Token]) -> Set[int]:
    exempted = set()
    for code_tag in ("code", "pre", "script"):
        for token in tokens:
            if token.kind == "html_start" and token.tag == code_tag:
                start: Optional[int] = token.line

            if token.kind == "html_end" and token.tag == code_tag:
                # The pretty printer expects well-formed HTML, even
                # if it's strangely formatted, so we expect start
                # to be None.
                assert start is not None

                # We leave code blocks completely alone, including
                # the start and end tags.
                for i in range(start, token.line + 1):
                    exempted.add(i)
                    start = None
    return exempted


def pretty_print_html(html: str) -> str:
    tokens = tokenize(html)

    exempted_lines = get_exempted_lines(tokens)

    tokens.reverse()
    lines = html.split("\n")

    open_offsets: List[str] = []
    formatted_lines = []
    next_offset: str = ""
    tag_end_row: Optional[int] = None
    tag_continuation_offset = ""

    def line_offset(row: int, line: str, next_line: str) -> Optional[str]:
        nonlocal next_offset
        nonlocal tag_end_row
        nonlocal tag_continuation_offset

        if tag_end_row and row < tag_end_row:
            was_closed = pop_unused_tokens(tokens, row)
            if was_closed:
                next_offset = open_offsets.pop()
            return tag_continuation_offset

        offset = next_offset
        if tokens:
            token = tokens[-1]
            if token.line == row and token.line_span > 1:
                if token.kind in ("django_comment", "handlebar_comment", "html_comment"):
                    tag_continuation_offset = offset
                else:
                    tag_continuation_offset = offset + "  "
                tag_end_row = row + token.line_span

        pref = indent_pref(row, tokens, line)
        if pref == "open":
            if same_indent(line, next_line) and not requires_indent(line):
                next_offset = offset
            else:
                next_offset = offset + " " * 4
            open_offsets.append(offset)
        elif pref == "else":
            offset = open_offsets[-1]
            if same_indent(line, next_line):
                next_offset = offset
            else:
                next_offset = offset + " " * 4
        elif pref == "close":
            offset = open_offsets.pop()
            next_offset = offset
        return offset

    def adjusted_line(row: int, line: str, next_line: str) -> str:
        if line.strip() == "":
            return ""

        offset = line_offset(row, line, next_line)

        if row in exempted_lines:
            return line.rstrip()

        if offset is None:
            return line.rstrip()

        return offset + line.strip()

    for i, line in enumerate(lines):
        # We use 1-based indexing for both rows and columns.
        next_line = next_non_blank_line(lines, i)
        row = i + 1
        formatted_lines.append(adjusted_line(row, line, next_line))

    return "\n".join(formatted_lines)


def validate_indent_html(fn: str, fix: bool) -> bool:
    with open(fn) as f:
        html = f.read()
    phtml = pretty_print_html(html)
    if not html.split("\n") == phtml.split("\n"):
        if fix:
            print(GREEN + "Automatically fixing problems..." + ENDC)
            with open(fn, "w") as f:
                f.write(phtml)
            # Since we successfully fixed the issues, we return True.
            return True
        print(
            "Invalid indentation detected in file: "
            f"{fn}\nDiff for the file against expected indented file:",
            flush=True,
        )
        subprocess.run(["diff", fn, "-"], input=phtml, universal_newlines=True)
        print()
        print("This problem can be fixed with the `--fix` option.")
        return False
    return True

@@ -5632,16 +5632,17 @@
 GREEN + 
+f
 %22Automat
@@ -5659,19 +5659,28 @@
 ing 
-problems...
+indentation for %7Bfn%7D
 %22 + 

Tweak message for fixes.