"""
================================================================================
File: 02_arguments.py
Topic: Function Arguments in Python
================================================================================

This file demonstrates the different ways to pass arguments to functions
in Python. Understanding these patterns is essential for writing flexible
and reusable functions.

Key Concepts:
- Positional arguments
- Keyword arguments
- Default parameter values
- *args (variable positional arguments)
- **kwargs (variable keyword arguments)
- Argument unpacking
- Keyword-only and positional-only arguments

================================================================================
"""

# -----------------------------------------------------------------------------
# 1. Positional Arguments
# -----------------------------------------------------------------------------
# Arguments passed in order; position matters

print("--- Positional Arguments ---")

def greet(first_name, last_name):
    """Greet using positional arguments."""
    print(f"Hello, {first_name} {last_name}!")

greet("John", "Doe")      # Correct order
greet("Jane", "Smith")    # Correct order

# Order matters!
greet("Doe", "John")      # Wrong order gives wrong result

# -----------------------------------------------------------------------------
# 2. Keyword Arguments
# -----------------------------------------------------------------------------
# Arguments passed by name; order doesn't matter

print("\n--- Keyword Arguments ---")

def create_profile(name, age, city):
    """Create a profile using keyword arguments."""
    print(f"Name: {name}, Age: {age}, City: {city}")

# Using keyword arguments (order doesn't matter)
create_profile(name="Alice", age=25, city="NYC")
create_profile(city="Tokyo", name="Bob", age=30)

# Mix positional and keyword (positional must come first!)
create_profile("Charlie", city="London", age=28)

# -----------------------------------------------------------------------------
# 3. Default Parameter Values
# -----------------------------------------------------------------------------
# Parameters can have default values

print("\n--- Default Parameter Values ---")

def greet_with_title(name, title="Mr."):
    """Greet with an optional title."""
    print(f"Hello, {title} {name}!")

greet_with_title("Smith")           # Uses default title
greet_with_title("Johnson", "Dr.")  # Overrides default

# Multiple defaults
def create_user(username, email, role="user", active=True):
    """Create a user with defaults."""
    print(f"User: {username}, Email: {email}, Role: {role}, Active: {active}")

create_user("john", "john@example.com")
create_user("admin", "admin@example.com", role="admin")
create_user("test", "test@example.com", active=False)

# CAUTION: Mutable default arguments
# DON'T DO THIS:
def bad_function(items=[]):  # Bad! List is shared across calls
    items.append(1)
    return items

# DO THIS INSTEAD:
def good_function(items=None):
    """Safe way to handle mutable defaults."""
    if items is None:
        items = []
    items.append(1)
    return items

print(f"\nBad function calls: {bad_function()}, {bad_function()}, {bad_function()}")
print(f"Good function calls: {good_function()}, {good_function()}, {good_function()}")

# -----------------------------------------------------------------------------
# 4. *args - Variable Positional Arguments
# -----------------------------------------------------------------------------
# Accept any number of positional arguments

print("\n--- *args (Variable Positional Arguments) ---")

def sum_all(*args):
    """Sum all provided numbers."""
    print(f"  Received args: {args} (type: {type(args).__name__})")
    return sum(args)

print(f"sum_all(1, 2): {sum_all(1, 2)}")
print(f"sum_all(1, 2, 3, 4, 5): {sum_all(1, 2, 3, 4, 5)}")
print(f"sum_all(): {sum_all()}")

# Combining regular parameters with *args
def greet_people(greeting, *names):
    """Greet multiple people with the same greeting."""
    for name in names:
        print(f"  {greeting}, {name}!")

print("\nGreeting people:")
greet_people("Hello", "Alice", "Bob", "Charlie")

# -----------------------------------------------------------------------------
# 5. **kwargs - Variable Keyword Arguments
# -----------------------------------------------------------------------------
# Accept any number of keyword arguments

print("\n--- **kwargs (Variable Keyword Arguments) ---")

def print_info(**kwargs):
    """Print all keyword arguments."""
    print(f"  Received kwargs: {kwargs} (type: {type(kwargs).__name__})")
    for key, value in kwargs.items():
        print(f"    {key}: {value}")

print_info(name="Baraa", age=25, city="Gaza")
print_info(language="Python", level="Expert")

# Combining all parameter types
def complete_example(required, *args, default="value", **kwargs):
    """Demonstrate all parameter types."""
    print(f"  Required: {required}")
    print(f"  *args: {args}")
    print(f"  Default: {default}")
    print(f"  **kwargs: {kwargs}")

print("\nComplete example:")
complete_example("must_have", 1, 2, 3, default="custom", extra="data", more="stuff")

# -----------------------------------------------------------------------------
# 6. Argument Unpacking
# -----------------------------------------------------------------------------
# Use * and ** to unpack sequences and dictionaries into arguments

print("\n--- Argument Unpacking ---")

def introduce(name, age, city):
    """Introduce a person."""
    print(f"I'm {name}, {age} years old, from {city}")

# Unpack list/tuple with *
person_list = ["Alice", 30, "Paris"]
introduce(*person_list)  # Same as: introduce("Alice", 30, "Paris")

# Unpack dictionary with **
person_dict = {"name": "Bob", "age": 25, "city": "Berlin"}
introduce(**person_dict)  # Same as: introduce(name="Bob", age=25, city="Berlin")

# Combine unpacking
def multiply(a, b, c):
    return a * b * c

nums = [2, 3]
print(f"\nMultiply with unpacking: {multiply(*nums, 4)}")

# -----------------------------------------------------------------------------
# 7. Keyword-Only Arguments (Python 3+)
# -----------------------------------------------------------------------------
# Arguments after * must be passed as keywords

print("\n--- Keyword-Only Arguments ---")

def format_name(first, last, *, upper=False, reverse=False):
    """
    Format a name. 'upper' and 'reverse' are keyword-only.
    """
    name = f"{first} {last}"
    if reverse:
        name = f"{last}, {first}"
    if upper:
        name = name.upper()
    return name

# These work:
print(format_name("John", "Doe"))
print(format_name("John", "Doe", upper=True))
print(format_name("John", "Doe", reverse=True, upper=True))

# This would fail:
# format_name("John", "Doe", True)  # Error! upper must be keyword

# Using bare * for keyword-only
def connect(*, host, port, timeout=30):
    """All parameters are keyword-only."""
    print(f"  Connecting to {host}:{port} (timeout: {timeout}s)")

connect(host="localhost", port=8080)
connect(host="db.example.com", port=5432, timeout=60)

# -----------------------------------------------------------------------------
# 8. Positional-Only Arguments (Python 3.8+)
# -----------------------------------------------------------------------------
# Arguments before / must be passed positionally

print("\n--- Positional-Only Arguments ---")

def divide(x, y, /):
    """x and y must be passed positionally."""
    return x / y

print(f"divide(10, 2): {divide(10, 2)}")
# divide(x=10, y=2)  # Error! x and y are positional-only

# Combining positional-only, regular, and keyword-only
def complex_function(pos_only1, pos_only2, /, regular1, regular2, *, kw_only1, kw_only2="default"):
    """
    pos_only1, pos_only2: positional-only (before /)
    regular1, regular2: can be either
    kw_only1, kw_only2: keyword-only (after *)
    """
    print(f"  pos_only: {pos_only1}, {pos_only2}")
    print(f"  regular: {regular1}, {regular2}")
    print(f"  kw_only: {kw_only1}, {kw_only2}")

print("\nComplex function:")
complex_function(1, 2, 3, regular2=4, kw_only1="required")

# -----------------------------------------------------------------------------
# 9. Type Hints for Arguments (Preview)
# -----------------------------------------------------------------------------
# Adding type information (doesn't enforce, just hints)

print("\n--- Type Hints ---")

def calculate_total(price: float, quantity: int, tax_rate: float = 0.1) -> float:
    """
    Calculate total price with tax.
    
    Args:
        price: Unit price (float)
        quantity: Number of items (int)
        tax_rate: Tax rate as decimal (default 0.1 = 10%)
    
    Returns:
        Total price including tax
    """
    subtotal = price * quantity
    return subtotal * (1 + tax_rate)

total = calculate_total(19.99, 3)
print(f"Total: ${total:.2f}")

# Type hints with complex types
from typing import List, Optional, Dict

def process_scores(scores: List[int], name: Optional[str] = None) -> Dict[str, float]:
    """Process a list of scores and return statistics."""
    return {
        "name": name or "Unknown",
        "average": sum(scores) / len(scores),
        "highest": max(scores),
        "lowest": min(scores)
    }

result = process_scores([85, 90, 78, 92], "Alice")
print(f"Score stats: {result}")

# -----------------------------------------------------------------------------
# 10. Practical Examples
# -----------------------------------------------------------------------------

print("\n--- Practical Examples ---")

# Flexible logging function
def log(message, *tags, level="INFO", **metadata):
    """Log a message with optional tags and metadata."""
    tag_str = " ".join(f"[{tag}]" for tag in tags)
    meta_str = " | ".join(f"{k}={v}" for k, v in metadata.items())
    
    output = f"[{level}] {tag_str} {message}"
    if meta_str:
        output += f" ({meta_str})"
    print(output)

log("Server started", "server", "startup")
log("User logged in", "auth", level="DEBUG", user_id=123, ip="192.168.1.1")

# Builder pattern with kwargs
def create_html_element(tag, content="", **attributes):
    """Create an HTML element string."""
    attrs = " ".join(f'{k}="{v}"' for k, v in attributes.items())
    if attrs:
        attrs = " " + attrs
    return f"<{tag}{attrs}>{content}</{tag}>"

print("\n" + create_html_element("p", "Hello World"))
print(create_html_element("a", "Click me", href="https://example.com", target="_blank"))
print(create_html_element("input", type="text", placeholder="Enter name", id="name_input"))
