# SPDX-License-Identifier: Apache-2.0
# Copyright 2025 Python Calculator Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""A comprehensive Python calculator with basic and advanced operations.

This module provides a Calculator class that supports:
- Basic arithmetic operations (+, -, *, /, %, **)
- Advanced mathematical functions (sqrt, sin, cos, tan, log, etc.)
- Memory operations (store, recall, clear)
- Expression evaluation with proper error handling
- Interactive command-line interface
"""

import math
import operator
import re
from typing import Dict, Any, Union, Optional


class CalculatorError(Exception):
    """Custom exception for calculator-specific errors."""
    pass


class Calculator:
    """A comprehensive calculator with basic and advanced mathematical operations.
    
    This calculator supports arithmetic operations, mathematical functions,
    memory operations, and expression evaluation with proper error handling.
    
    Attributes:
        memory: Stored value for memory operations
        history: List of previous calculations
        _operators: Dictionary mapping operator symbols to functions
        _functions: Dictionary mapping function names to math functions
    """
    
    def __init__(self) -> None:
        """Initialize the calculator with empty memory and history."""
        self.memory: float = 0.0
        self.history: list[str] = []
        
        # Define supported operators
        self._operators: Dict[str, Any] = {
            '+': operator.add,
            '-': operator.sub,
            '*': operator.mul,
            '/': operator.truediv,
            '//': operator.floordiv,
            '%': operator.mod,
            '**': operator.pow,
            '^': operator.pow,  # Alternative power operator
        }
        
        # Define supported mathematical functions
        self._functions: Dict[str, Any] = {
            'sqrt': math.sqrt,
            'sin': math.sin,
            'cos': math.cos,
            'tan': math.tan,
            'asin': math.asin,
            'acos': math.acos,
            'atan': math.atan,
            'log': math.log,
            'log10': math.log10,
            'exp': math.exp,
            'abs': abs,
            'ceil': math.ceil,
            'floor': math.floor,
            'round': round,
            'factorial': math.factorial,
        }
    
    def add(self, a: float, b: float) -> float:
        """Add two numbers."""
        return a + b
    
    def subtract(self, a: float, b: float) -> float:
        """Subtract second number from first."""
        return a - b
    
    def multiply(self, a: float, b: float) -> float:
        """Multiply two numbers."""
        return a * b
    
    def divide(self, a: float, b: float) -> float:
        """Divide first number by second.
        
        Args:
            a: Dividend
            b: Divisor
            
        Returns:
            Result of division
            
        Raises:
            CalculatorError: If attempting to divide by zero
        """
        if b == 0:
            raise CalculatorError("Cannot divide by zero")
        return a / b
    
    def power(self, base: float, exponent: float) -> float:
        """Raise base to the power of exponent."""
        try:
            return base ** exponent
        except (OverflowError, ValueError) as e:
            raise CalculatorError(f"Power operation failed: {e}") from e
    
    def modulo(self, a: float, b: float) -> float:
        """Calculate remainder of division.
        
        Args:
            a: Dividend
            b: Divisor
            
        Returns:
            Remainder of a divided by b
            
        Raises:
            CalculatorError: If attempting to divide by zero
        """
        if b == 0:
            raise CalculatorError("Cannot calculate modulo with zero divisor")
        return a % b
    
    def square_root(self, number: float) -> float:
        """Calculate square root of a number.
        
        Args:
            number: Number to find square root of
            
        Returns:
            Square root of the number
            
        Raises:
            CalculatorError: If number is negative
        """
        if number < 0:
            raise CalculatorError("Cannot calculate square root of negative number")
        return math.sqrt(number)
    
    def apply_function(self, func_name: str, value: float) -> float:
        """Apply a mathematical function to a value.
        
        Args:
            func_name: Name of the function to apply
            value: Value to apply function to
            
        Returns:
            Result of applying the function
            
        Raises:
            CalculatorError: If function is not supported or calculation fails
        """
        if func_name not in self._functions:
            raise CalculatorError(f"Unsupported function: {func_name}")
        
        try:
            return self._functions[func_name](value)
        except (ValueError, OverflowError, TypeError) as e:
            raise CalculatorError(f"Function '{func_name}' failed: {e}") from e
    
    def evaluate_expression(self, expression: str) -> float:
        """Evaluate a mathematical expression safely.
        
        Args:
            expression: Mathematical expression as string
            
        Returns:
            Result of the expression evaluation
            
        Raises:
            CalculatorError: If expression is invalid or evaluation fails
        """
        # Remove whitespace
        expression = expression.replace(' ', '')
        
        # Basic validation
        if not expression:
            raise CalculatorError("Empty expression")
        
        # Replace common mathematical constants
        expression = expression.replace('pi', str(math.pi))
        expression = expression.replace('e', str(math.e))
        
        # Replace ^ with ** for power operations
        expression = expression.replace('^', '**')
        
        # Validate expression contains only allowed characters
        allowed_chars = set('0123456789+-*/().^%')
        if not all(c in allowed_chars for c in expression):
            # Check for function calls
            func_pattern = r'([a-zA-Z_][a-zA-Z0-9_]*)\s*\('
            functions_in_expr = re.findall(func_pattern, expression)
            
            for func in functions_in_expr:
                if func not in self._functions:
                    raise CalculatorError(f"Unsupported function: {func}")
        
        try:
            # Create a safe namespace for evaluation
            safe_dict = {
                '__builtins__': {},
                'abs': abs,
                'round': round,
                'pow': pow,
                'max': max,
                'min': min,
            }
            
            # Add mathematical functions
            safe_dict.update(self._functions)
            
            # Add mathematical constants
            safe_dict.update({
                'pi': math.pi,
                'e': math.e,
            })
            
            result = eval(expression, safe_dict, {})  # pylint: disable=eval-used
            
            if isinstance(result, (int, float)):
                return float(result)
            else:
                raise CalculatorError("Expression did not evaluate to a number")
                
        except ZeroDivisionError:
            raise CalculatorError("Division by zero in expression") from None
        except (SyntaxError, NameError, TypeError, ValueError) as e:
            raise CalculatorError(f"Invalid expression: {e}") from e
    
    def memory_store(self, value: float) -> None:
        """Store a value in memory."""
        self.memory = value
    
    def memory_recall(self) -> float:
        """Recall the value stored in memory."""
        return self.memory
    
    def memory_clear(self) -> None:
        """Clear the memory."""
        self.memory = 0.0
    
    def memory_add(self, value: float) -> None:
        """Add a value to the current memory value."""
        self.memory += value
    
    def memory_subtract(self, value: float) -> None:
        """Subtract a value from the current memory value."""
        self.memory -= value
    
    def add_to_history(self, calculation: str) -> None:
        """Add a calculation to the history."""
        self.history.append(calculation)
        # Keep only last 50 calculations
        if len(self.history) > 50:
            self.history.pop(0)
    
    def get_history(self) -> list[str]:
        """Get the calculation history."""
        return self.history.copy()
    
    def clear_history(self) -> None:
        """Clear the calculation history."""
        self.history.clear()


def main() -> None:
    """Main function to run the interactive calculator."""
    calc = Calculator()
    
    print("Python Calculator")
    print("================")
    print("Available operations:")
    print("  Basic: +, -, *, /, %, ** (or ^)")
    print("  Functions: sqrt, sin, cos, tan, log, exp, abs, etc.")
    print("  Memory: ms (store), mr (recall), mc (clear), m+ (add), m- (subtract)")
    print("  Commands: history, clear_history, quit")
    print("  Examples: '2 + 3', 'sqrt(16)', 'sin(pi/2)', '2**3'")
    print()
    
    while True:
        try:
            user_input = input("Calculator> ").strip()
            
            if not user_input:
                continue
            
            # Handle special commands
            if user_input.lower() in ('quit', 'exit', 'q'):
                print("Goodbye!")
                break
            elif user_input.lower() == 'history':
                history = calc.get_history()
                if history:
                    print("Calculation History:")
                    for i, calculation in enumerate(history, 1):
                        print(f"  {i}. {calculation}")
                else:
                    print("No calculations in history.")
                continue
            elif user_input.lower() == 'clear_history':
                calc.clear_history()
                print("History cleared.")
                continue
            elif user_input.lower() == 'mr':
                result = calc.memory_recall()
                print(f"Memory: {result}")
                continue
            elif user_input.lower() == 'mc':
                calc.memory_clear()
                print("Memory cleared.")
                continue
            elif user_input.lower().startswith('ms '):
                try:
                    value = float(user_input[3:])
                    calc.memory_store(value)
                    print(f"Stored {value} in memory.")
                except ValueError:
                    print("Error: Invalid number for memory store.")
                continue
            elif user_input.lower().startswith('m+ '):
                try:
                    value = float(user_input[3:])
                    calc.memory_add(value)
                    print(f"Added {value} to memory. Memory: {calc.memory_recall()}")
                except ValueError:
                    print("Error: Invalid number for memory add.")
                continue
            elif user_input.lower().startswith('m- '):
                try:
                    value = float(user_input[3:])
                    calc.memory_subtract(value)
                    print(f"Subtracted {value} from memory. Memory: {calc.memory_recall()}")
                except ValueError:
                    print("Error: Invalid number for memory subtract.")
                continue
            
            # Evaluate mathematical expression
            result = calc.evaluate_expression(user_input)
            calculation_record = f"{user_input} = {result}"
            calc.add_to_history(calculation_record)
            print(f"Result: {result}")
            
        except CalculatorError as e:
            print(f"Calculator Error: {e}")
        except KeyboardInterrupt:
            print("\nGoodbye!")
            break
        except Exception as e:  # pylint: disable=broad-except
            print(f"Unexpected error: {e}")


if __name__ == "__main__":
    main()