#!/usr/bin/env python3
"""Format comments."""

import argparse
import sys
import textwrap
from collections.abc import Iterator

from libcli.basecli import BaseCLI

BLOCK: list[str] | None = None
TAG: str | None = None
WRAPPER: textwrap.TextWrapper | None = None


class CommentWrapper(textwrap.TextWrapper):
    """Wrap hash-style (`#`) comments; sh, make, perl, python..."""

    def wrap(self, text: str) -> list[str]:
        """Wrap comment text."""

        # PLW0603: TAG is a module-level sentinel shared across recursive calls.
        global TAG  # noqa: PLW0603

        # find the first non-empty line
        lines = text.splitlines()
        while lines and not lines[0].strip():
            lines.pop(0)
        if not lines:
            return []

        # determine indent
        first = lines[0].rstrip().expandtabs(tabsize=self.tabsize)
        stripped = first.lstrip()
        indent = " " * (len(first) - len(stripped))
        self.initial_indent += indent
        self.subsequent_indent += indent

        # determine formatter
        tag = stripped.split()[0]
        if tag in ("#", ";", '"', "//"):
            TAG = tag
            return self._format_hash_style(lines)

        if tag.startswith('"""'):
            TAG = '"""'
            return self._format_docstring(lines)

        return self._format_default(lines)

    def _format_hash_style(self, lines: list[str]) -> list[str]:
        indent_whitespace = self.initial_indent

        # PLW0603: WRAPPER is a module-level cache shared across recursive calls.
        global WRAPPER  # noqa: PLW0603
        WRAPPER = self._build_wrapper()
        assert TAG is not None
        WRAPPER.initial_indent += TAG + " "
        WRAPPER.subsequent_indent += TAG + " "

        blocks = list(self._get_comment_block(lines, indent_whitespace))
        text = "\n".join(blocks)
        return text.splitlines()

    def _format_docstring(self, lines: list[str]) -> list[str]:
        indent_whitespace = self.initial_indent

        # PLW0603: WRAPPER is a module-level cache shared across recursive calls.
        global WRAPPER  # noqa: PLW0603
        WRAPPER = self._build_wrapper()
        assert TAG is not None
        WRAPPER.initial_indent += TAG
        WRAPPER.subsequent_indent += len(TAG) * " "

        blocks = list(self._get_comment_block(lines, indent_whitespace))

        if blocks:
            # remove closing tag, if present, and add closing tag on line
            # of its own.

            closing = indent_whitespace + TAG

            if blocks[-1].endswith(TAG):
                blocks[-1] = blocks[-1][: -len(TAG)].rstrip()

            if blocks[-1]:
                blocks.append(closing)
            else:
                blocks[-1] = closing

        text = "\n".join(blocks)
        return text.splitlines()

        # """This is a python docstring. Notice that it begin with 3 double-
        #    quotes, the first word follows immediately, with no intervening
        #    space.
        # """

    def _get_comment_block(self, lines: list[str], indent_whitespace: str) -> Iterator[str]:
        # lines that match this pattern, such as a horizontal
        # bar, signify the end of a block, and are preserved.
        horzbar = "-" * 25

        # PLW0603: BLOCK is a module-level accumulator shared across recursive calls.
        global BLOCK  # noqa: PLW0603
        BLOCK = []
        lazy_sep = None

        assert TAG is not None
        for line in [x.strip() for x in lines]:
            active_sep = None

            # horizontal bar?
            if line and line.startswith(TAG):
                if horzbar in line:
                    # yes, preserve (don't wrap) it
                    active_sep = indent_whitespace + line
                    # PLW2901: line is cleared after saving it as active_sep.
                    line = ""  # noqa: PLW2901
                else:
                    # no, remove the comment marker
                    # PLW2901: line is stripped of the comment marker before processing.
                    line = line[len(TAG) :].lstrip()  # noqa: PLW2901

            # horizontal bar or blank line or solo hash?
            if not line:
                yield from self._render()
                if active_sep:
                    if lazy_sep is not None:
                        yield lazy_sep
                        lazy_sep = None
                    yield active_sep
                    lazy_sep = None
                    # completely rendered
                else:
                    lazy_sep = indent_whitespace + TAG
                    # partially rendered
            else:
                if lazy_sep is not None:
                    yield lazy_sep
                    lazy_sep = None

                BLOCK.append(line)
        yield from self._render()

    def _format_default(self, lines: list[str]) -> list[str]:
        # PLW0603: WRAPPER is a module-level cache shared across recursive calls.
        global WRAPPER  # noqa: PLW0603
        WRAPPER = self._build_wrapper()

        blocks = list(self._get_default_block(lines))
        text = "\n".join(blocks)
        return text.splitlines()

    def _get_default_block(self, lines: list[str]) -> Iterator[str]:
        # PLW0603: BLOCK is a module-level accumulator shared across recursive calls.
        global BLOCK  # noqa: PLW0603
        BLOCK = []
        lazy_sep = None

        for line in [x.strip() for x in lines]:
            if not line:
                yield from self._render()
                lazy_sep = ""
                # partially rendered
            else:
                if lazy_sep is not None:
                    yield lazy_sep
                    lazy_sep = None

                BLOCK.append(line)
        yield from self._render()

    def _render(self) -> list[str]:
        rendered = []
        # PLW0603: BLOCK is a module-level accumulator shared across recursive calls.
        global BLOCK  # noqa: PLW0603
        if BLOCK:
            assert WRAPPER is not None
            rendered.append(WRAPPER.fill(self._squeeze(BLOCK)))
            BLOCK = []

        return rendered

    @staticmethod
    def _squeeze(lines: list[str]) -> str:
        # reduce runs of whitespace to single spaces
        return " ".join(" ".join(lines).split())

    def _build_wrapper(self) -> textwrap.TextWrapper:
        # kwargs = {k: v for k, v in self.__dict__.items()}
        # return TextWrapper(**kwargs)

        return textwrap.TextWrapper(
            width=self.width,
            initial_indent=self.initial_indent,
            subsequent_indent=self.subsequent_indent,
        )


class FormatCommentsCLI(BaseCLI):
    """Format comments command line interface."""

    def init_parser(self) -> None:
        """Initialize argument parser."""

        self.parser = self.ArgumentParser(
            description=self.dedent("""
        Format shell (`#`) and python docstring (`\"\"\"`) comments.

        By default, `%(prog)s` reads from `stdin` and `writes` to stdout.
                """),
        )

    def add_arguments(self) -> None:
        """Add command-line arguments."""

        arg = self.parser.add_argument(
            "-w",
            "--width",
            default=75,
            type=int,
            help="The maximum length of wrapped lines",
        )
        self.add_default_to_help(arg)

        arg = self.parser.add_argument(
            "-t",
            "--tabsize",
            default=8,
            type=int,
            help="The size of a tab character",
        )
        self.add_default_to_help(arg)

        self.parser.add_argument(
            "FILE",
            nargs="?",
            default=sys.stdin,
            type=argparse.FileType("r"),
            help="Read lines from FILE instead of stdin",
        )

    def main(self) -> None:
        """Run format-comments."""

        fmtcmt = CommentWrapper(width=self.options.width, tabsize=self.options.tabsize)
        if text := fmtcmt.fill(self.options.FILE.read()):
            print(text)


def main() -> None:
    """CLI entry point."""
    FormatCommentsCLI().main()


if __name__ == "__main__":
    main()
