"""
The simplest no-nonsense progress bar for python.
"""

import shutil


class lowbar:
    """
    The main lowbar class.
    """
    def __init__(self, tasks: range | int = 0, desc: str = "",
                 load_fill: str = "#", blank_fill: str = "-",
                 remove_ends: bool = False, keep_receipt: bool = False) -> None:
        """
        Checks and initializes the bar with the given
        parameters.
        """
        if not isinstance(load_fill, str) or len(load_fill) != 1:
            raise TypeError("arg load_fill should be a single char str")

        if not isinstance(blank_fill, str) or len(blank_fill) != 1:
            raise TypeError("arg blank_fill should be a single char str")

        if not isinstance(desc, str):
            raise TypeError("arg desc should be type str")

        if not isinstance(remove_ends, bool):
            raise TypeError("arg remove_ends should be type bool")

        if not isinstance(keep_receipt, bool):
            raise TypeError("arg keep_receipt should be type bool")

        if not isinstance(tasks, range):
            if not isinstance(tasks, int):
                raise TypeError("arg tasks should be either type range or int")

            tasks = range(tasks)

        self.tasks = tasks
        self.completion = 1
        self.load_fill = load_fill
        self.blank_fill = blank_fill
        self.desc = desc
        self.bar_ends = ("[", "]") if not remove_ends else (" ", " ")
        self.keep_receipt = keep_receipt

    def __enter__(self) -> object:
        """
        Context manager setup to automatically display bar
        without requiring new().
        """
        self.new()
        return self

    def __exit__(self, *exc) -> None:
        """
        Context manager exit to clear the bar automatically
        without requiring clear().
        """
        print() if self.keep_receipt else self.clear()

    def __iter__(self) -> object:
        """
        Iterable manager that automatically runs new() at the
        start and clear() at the end
        """
        self.new()
        div = 100 / len(self.tasks)
        add = div
        try:
            for item in self.tasks:
                yield item
                self.update(int(div))
                div += add

        finally:
            print() if self.keep_receipt else self.clear()

    def _print(self, text: str) -> None:
        """
        Writes data to stdout
        """
        print(text, end='', flush=True)

    def _get_columns(self) -> int:
        """
        Returns the number of columns in the running console.
        """
        return shutil.get_terminal_size().columns

    def _update_bar(self) -> None:
        """
        Refreshes the current bar with new values, and a
        possibly resized console.
        """
        desc = self.desc if len(self.desc) + 20 < self._get_columns() else ""
        label = f"{desc}  {str(self.completion)}" if self.completion < 10 else f"{desc} {str(self.completion)}"
        size = self._get_columns() - (len(label) + 7)
        load_size = int(size * (self.completion / 100))
        bar_blank_fill = size - load_size
        self._print(f"\r{label} % {self.bar_ends[0]}{load_size * self.load_fill}{bar_blank_fill * self.blank_fill}{self.bar_ends[1]} ")

    def _overwrite_bar(self, text: str = "") -> None:
        """
        Overwrite the loading bar with optional text.
        """
        overwrite = " " * (self._get_columns() - len(text))
        self._print(f"\r{text}{overwrite}")

    def update(self, percentage: int) -> None:
        """
        Sets the current completion percentage.
        """
        if not isinstance(percentage, int):
            raise TypeError("arg percentage should be type int")

        if percentage < 0 or percentage > 100:
            raise ValueError("arg percentage out of range (0-100)")

        self.completion = percentage
        self._update_bar()

    def add(self, percentage: int) -> None:
        """
        Adds to the current completion percentage.
        """
        if not isinstance(percentage, int):
            raise TypeError("arg percentage should be type int")

        self.completion += percentage
        self.completion = 100 if self.completion > 100 else self.completion
        self.completion = 0 if self.completion < 0 else self.completion
        self._update_bar()

    def next(self, tasks: int = 0) -> None:
        """
        Automatically adds to the completion percentage based
        on the number of tasks to be completed.
        """
        if not isinstance(tasks, int):
            raise TypeError("arg tasks should be type int")

        if tasks < 1:
            if not self.tasks:
                raise RuntimeError("empty `next` was called without `tasks` being set in the constructor")

            self.add(int(100 / len(self.tasks)))
            return

        self.add(int(100 / tasks))

    def new(self) -> None:
        """
        Alias for update(0)
        """
        self.update(0)

    def log(self, text: str) -> None:
        """
        Log text to the console without affecting the bar.
        """
        if not isinstance(text, str):
            raise TypeError("arg text should be type str")

        self._overwrite_bar(text)
        print()
        self._update_bar()

    def clear(self) -> None:
        """
        Clears the bar completely from the console.
        """
        self._overwrite_bar()
