"""
@file calculator.py
@brief Calculator model, operations, and formatted output helpers.
@details
Provides the input accumulator, arithmetic operation handling, and result
formatting for the calculator application.
@author Adam Bajtay
@date 2026-04-15
"""
import math
from enum import Enum
import mathlib


class Sign(Enum):
    """@brief Sign of the current input value."""
    Positive = "+"
    Negative = "-"


class Operation(Enum):
    """@brief Supported calculator operations."""
    EQUAL = "="
    ADD = "+"
    SUBTRACT = "-"
    MULTIPLY = "*"
    DIVIDE = "/"
    POWER = "^"
    ROOT = "√"
    MODULO = "%"


class Calculator:
    """@brief Calculator state and input/output formatting logic."""

    def __init__(self, max_digits: int = 15):
        """@brief Initialize calculator state.

        @param max_digits Maximum number of characters allowed before using
                          scientific notation for formatted output.
        """
        self.MAX_CHARACTERS = max_digits
        self.accumulator: str = "0"
        self.sign: Sign = Sign.Positive
        self.result: float = 0.0
        self.current_operation: Operation = Operation.EQUAL

    def get_accumulator_string(self) -> str:
        """@brief Return the current input accumulator as text.

        @return Current accumulator string including sign and operation prefix.
        """
        if len(self.accumulator) > self.MAX_CHARACTERS:
            self.accumulator = "inf"

        sign: str = "" if self.sign == Sign.Positive else "-"
        operator: str = "" if self.current_operation == Operation.EQUAL else str(self.current_operation.value)
        return operator + sign + self.accumulator

    def get_result_string(self) -> str:
        """@brief Return the current result in display-friendly form.

        @return Formatted result string.
        """
        return self._format_value(self.result)

    def add_digit(self, digit: int) -> str:
        """@brief Append one digit to the input accumulator.

        @param digit The digit to append.
        @return Updated accumulator string.
        """
        if len(self.accumulator) >= self.MAX_CHARACTERS:
            return self.get_accumulator_string()

        digit_str = str(digit)
        if self.accumulator == "0" or self.accumulator == "inf":
            self.accumulator = digit_str
        else:
            self.accumulator += digit_str

        return self.get_accumulator_string()

    def add_decimal_point(self) -> str:
        """@brief Append a decimal point to the input accumulator.

        @return Updated accumulator string.
        """
        if len(self.accumulator) >= self.MAX_CHARACTERS:
            return self.get_accumulator_string()

        if "." not in self.accumulator and self.accumulator != "inf":
            self.accumulator += "."
        return self.get_accumulator_string()

    def delete_digit(self) -> str:
        """@brief Remove the last character from the input accumulator.

        @return Updated accumulator string.
        """
        if len(self.accumulator) <= 1:
            self.accumulator = "0"
        else:
            self.accumulator = self.accumulator[:-1]

        return self.get_accumulator_string()

    def clear(self) -> str:
        """@brief Clear the current input accumulator.

        @return Reset accumulator string.
        """
        self.accumulator = "0"
        self.sign = Sign.Positive
        return self.get_accumulator_string()

    def reset(self) -> str:
        """@brief Reset the calculator state and current result.

        @return Reset accumulator string.
        """
        self.clear()
        self.result = 0.0
        self.current_operation = Operation.EQUAL
        return self.get_accumulator_string()

    def change_sign(self) -> str:
        """@brief Toggle the sign of the current input value.

        @return Updated accumulator string.
        """

        # When adding or subtracting, change operator rather than the sign
        if self.current_operation == Operation.ADD:
            self.current_operation = Operation.SUBTRACT
            return self.get_accumulator_string()
        elif self.current_operation == Operation.SUBTRACT:
            self.current_operation = Operation.ADD
            return self.get_accumulator_string()


        if self.sign == Sign.Positive:
            self.sign = Sign.Negative
        else:
            self.sign = Sign.Positive

        return self.get_accumulator_string()

    def perform_operation(self, operation: Operation) -> str:
        """@brief Apply the pending operation and prepare the next one.

        @param operation to store for the next calculation step.

        @return Reset accumulator string.
        """
        sign_prefix = "" if self.sign == Sign.Positive else self.sign.value
        accumulator = float(sign_prefix + self.accumulator)

        if self.current_operation == Operation.EQUAL:
            self.result = accumulator
        elif self.current_operation == Operation.ADD:
            self.result = mathlib.add(self.result, accumulator)
        elif self.current_operation == Operation.SUBTRACT:
            self.result = mathlib.subtract(self.result, accumulator)
        elif self.current_operation == Operation.MULTIPLY:
            self.result = mathlib.multiply(self.result, accumulator)
        elif self.current_operation == Operation.DIVIDE:
            self.result = mathlib.divide(self.result, accumulator)
        elif self.current_operation == Operation.POWER:
            self.result = mathlib.power(self.result, accumulator)
        elif self.current_operation == Operation.ROOT:
            self.result = mathlib.root(self.result, accumulator)
        elif self.current_operation == Operation.MODULO:
            self.result = mathlib.modulo(self.result, accumulator)

        self.current_operation = operation
        self.clear()
        return self.get_accumulator_string()

    def set_factorial(self) -> str:
        """@brief Convert the accumulator to integer.
        Use that integer to calculate the factorial and set the accumulator to the result.

        @return Updated accumulator string.
        """
        try: result = str(int(mathlib.factorial(float(self.accumulator))))
        except OverflowError: result = str(mathlib.factorial(float(self.accumulator)))
        self.accumulator = result
        return self.get_accumulator_string()

    def set_pi(self) -> str:
        """@brief Set the accumulator to the value of pi.

        @return Updated accumulator string.
        """
        pi_str = str(mathlib.pi())
        self.accumulator = pi_str[:self.MAX_CHARACTERS]
        return self.get_accumulator_string()

    def _format_value(self, value: float) -> str:
        """@brief Format a numeric value for display within the digit limit.

        @param value The numeric value to format.
        @return Formatted value as string, using scientific notation if necessary.
        """
        text = str(int(value)) if value.is_integer() else str(value)

        if len(text) <= self.MAX_CHARACTERS:
            return text

        for precision in range(self.MAX_CHARACTERS, -1, -1):
            sci = f"{value:.{precision}e}"
            if len(sci) <= self.MAX_CHARACTERS:
                return sci

        return f"{value:.0e}"