"""
================================================================================
File: 04_polymorphism.py
Topic: Polymorphism in Python
================================================================================

This file demonstrates polymorphism in Python - the ability of different
objects to respond to the same method call in different ways. This is a
core OOP principle that enables flexible and extensible code.

Key Concepts:
- Duck typing
- Method polymorphism
- Operator overloading
- Protocols and ABC
- Practical polymorphism patterns

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

# -----------------------------------------------------------------------------
# 1. What is Polymorphism?
# -----------------------------------------------------------------------------
# "Many forms" - same interface, different implementations

print("--- What is Polymorphism? ---")

class Dog:
    def speak(self):
        return "Woof!"

class Cat:
    def speak(self):
        return "Meow!"

class Duck:
    def speak(self):
        return "Quack!"

# Same method call, different behavior
animals = [Dog(), Cat(), Duck()]

print("Different animals, same method:")
for animal in animals:
    print(f"  {type(animal).__name__}: {animal.speak()}")

# -----------------------------------------------------------------------------
# 2. Duck Typing
# -----------------------------------------------------------------------------
# "If it walks like a duck and quacks like a duck, it's a duck"

print("\n--- Duck Typing ---")

class RealDuck:
    """A real duck."""
    def quack(self):
        return "Quack quack!"
    
    def fly(self):
        return "Flap flap, flying!"

class RobotDuck:
    """A robot that acts like a duck."""
    def quack(self):
        return "Beep boop quack!"
    
    def fly(self):
        return "Propellers spinning, ascending!"

class Person:
    """A person pretending to be a duck."""
    def quack(self):
        return "*Person making quack sounds*"
    
    def fly(self):
        return "*Person flapping arms*"

def duck_demo(duck):
    """Demonstrate a duck (or anything duck-like)."""
    print(f"  {type(duck).__name__}:")
    print(f"    Quacking: {duck.quack()}")
    print(f"    Flying: {duck.fly()}")

# All these work because they have the required methods
print("Duck typing demo:")
duck_demo(RealDuck())
duck_demo(RobotDuck())
duck_demo(Person())

# -----------------------------------------------------------------------------
# 3. Method Polymorphism with Inheritance
# -----------------------------------------------------------------------------

print("\n--- Method Polymorphism ---")

class Shape:
    """Base shape class."""
    
    def area(self):
        raise NotImplementedError("Subclass must implement area()")
    
    def describe(self):
        return f"{self.__class__.__name__} with area {self.area():.2f}"

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    def area(self):
        return self.width * self.height

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius
    
    def area(self):
        import math
        return math.pi * self.radius ** 2

class Triangle(Shape):
    def __init__(self, base, height):
        self.base = base
        self.height = height
    
    def area(self):
        return 0.5 * self.base * self.height

# Polymorphism in action
shapes = [
    Rectangle(5, 3),
    Circle(4),
    Triangle(6, 4)
]

print("Shape descriptions:")
for shape in shapes:
    print(f"  {shape.describe()}")

# Calculate total area polymorphically
total_area = sum(shape.area() for shape in shapes)
print(f"\nTotal area of all shapes: {total_area:.2f}")

# -----------------------------------------------------------------------------
# 4. Operator Overloading
# -----------------------------------------------------------------------------
# Same operators, different behaviors

print("\n--- Operator Overloading ---")

class Vector:
    """A 2D vector with overloaded operators."""
    
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __str__(self):
        return f"Vector({self.x}, {self.y})"
    
    def __repr__(self):
        return self.__str__()
    
    def __add__(self, other):
        """Vector addition: v1 + v2"""
        return Vector(self.x + other.x, self.y + other.y)
    
    def __sub__(self, other):
        """Vector subtraction: v1 - v2"""
        return Vector(self.x - other.x, self.y - other.y)
    
    def __mul__(self, scalar):
        """Scalar multiplication: v * n"""
        return Vector(self.x * scalar, self.y * scalar)
    
    def __rmul__(self, scalar):
        """Reverse multiplication: n * v"""
        return self.__mul__(scalar)
    
    def __neg__(self):
        """Negation: -v"""
        return Vector(-self.x, -self.y)
    
    def __eq__(self, other):
        """Equality: v1 == v2"""
        return self.x == other.x and self.y == other.y
    
    def __abs__(self):
        """Magnitude: abs(v)"""
        return (self.x ** 2 + self.y ** 2) ** 0.5

v1 = Vector(3, 4)
v2 = Vector(1, 2)

print(f"v1 = {v1}")
print(f"v2 = {v2}")
print(f"v1 + v2 = {v1 + v2}")
print(f"v1 - v2 = {v1 - v2}")
print(f"v1 * 3 = {v1 * 3}")
print(f"2 * v2 = {2 * v2}")
print(f"-v1 = {-v1}")
print(f"|v1| = {abs(v1)}")
print(f"v1 == Vector(3, 4): {v1 == Vector(3, 4)}")

# -----------------------------------------------------------------------------
# 5. Polymorphism with Built-in Functions
# -----------------------------------------------------------------------------

print("\n--- Built-in Function Polymorphism ---")

class Playlist:
    """A playlist that works with len() and iteration."""
    
    def __init__(self, name):
        self.name = name
        self.songs = []
    
    def add_song(self, song):
        self.songs.append(song)
    
    def __len__(self):
        """Enable len(playlist)"""
        return len(self.songs)
    
    def __iter__(self):
        """Enable for song in playlist"""
        return iter(self.songs)
    
    def __getitem__(self, index):
        """Enable playlist[index]"""
        return self.songs[index]
    
    def __contains__(self, song):
        """Enable 'song' in playlist"""
        return song in self.songs

playlist = Playlist("My Favorites")
playlist.add_song("Song A")
playlist.add_song("Song B")
playlist.add_song("Song C")

print(f"Playlist '{playlist.name}':")
print(f"  Length: {len(playlist)}")
print(f"  First song: {playlist[0]}")
print(f"  'Song B' in playlist: {'Song B' in playlist}")
print("  All songs:")
for song in playlist:
    print(f"    - {song}")

# -----------------------------------------------------------------------------
# 6. Polymorphic Functions
# -----------------------------------------------------------------------------

print("\n--- Polymorphic Functions ---")

def process_payment(payment_method):
    """
    Process any payment method polymorphically.
    Any object with a process() method works!
    """
    print(f"  Processing with {type(payment_method).__name__}...")
    return payment_method.process()

class CreditCard:
    def __init__(self, card_number):
        self.card_number = card_number[-4:]  # Last 4 digits
    
    def process(self):
        return f"Charged to card ending in {self.card_number}"

class PayPal:
    def __init__(self, email):
        self.email = email
    
    def process(self):
        return f"Payment sent via PayPal ({self.email})"

class CryptoCurrency:
    def __init__(self, wallet):
        self.wallet = wallet[:8]
    
    def process(self):
        return f"Crypto transferred from {self.wallet}..."

# Same function, different payment methods
payment_methods = [
    CreditCard("4111111111111234"),
    PayPal("user@example.com"),
    CryptoCurrency("0x1234567890abcdef")
]

print("Processing payments:")
for method in payment_methods:
    result = process_payment(method)
    print(f"    Result: {result}")

# -----------------------------------------------------------------------------
# 7. Protocols (Informal Interfaces)
# -----------------------------------------------------------------------------

print("\n--- Protocols (Informal Interfaces) ---")

# Python 3.8+ has typing.Protocol for formal protocols
# Here's the concept with duck typing:

class Drawable:
    """Protocol: anything with a draw() method."""
    def draw(self):
        raise NotImplementedError

class Circle2D:
    def draw(self):
        return "Drawing a circle: O"

class Square2D:
    def draw(self):
        return "Drawing a square: □"

class Triangle2D:
    def draw(self):
        return "Drawing a triangle: △"

class Text2D:
    def __init__(self, text):
        self.text = text
    
    def draw(self):
        return f"Drawing text: '{self.text}'"

def render_canvas(drawables):
    """Render anything that has a draw() method."""
    print("Canvas:")
    for drawable in drawables:
        print(f"  {drawable.draw()}")

# All these can be rendered
elements = [Circle2D(), Square2D(), Triangle2D(), Text2D("Hello")]
render_canvas(elements)

# -----------------------------------------------------------------------------
# 8. Polymorphism with Abstract Base Classes
# -----------------------------------------------------------------------------

print("\n--- ABC Polymorphism ---")

from abc import ABC, abstractmethod

class DataExporter(ABC):
    """Abstract base class for data exporters."""
    
    @abstractmethod
    def export(self, data):
        """Export data - must be implemented."""
        pass
    
    def validate(self, data):
        """Common validation (can be overridden)."""
        if not data:
            raise ValueError("No data to export")
        return True

class JSONExporter(DataExporter):
    def export(self, data):
        import json
        self.validate(data)
        return json.dumps(data, indent=2)

class CSVExporter(DataExporter):
    def export(self, data):
        self.validate(data)
        if not data:
            return ""
        headers = ",".join(data[0].keys())
        rows = [",".join(str(v) for v in row.values()) for row in data]
        return headers + "\n" + "\n".join(rows)

class XMLExporter(DataExporter):
    def export(self, data):
        self.validate(data)
        xml = "<root>\n"
        for item in data:
            xml += "  <item>\n"
            for key, value in item.items():
                xml += f"    <{key}>{value}</{key}>\n"
            xml += "  </item>\n"
        xml += "</root>"
        return xml

# Same data, different formats
sample_data = [
    {"name": "Alice", "age": 25},
    {"name": "Bob", "age": 30}
]

exporters = [JSONExporter(), CSVExporter(), XMLExporter()]

print("Exporting same data in different formats:")
for exporter in exporters:
    print(f"\n{type(exporter).__name__}:")
    print(exporter.export(sample_data))

# -----------------------------------------------------------------------------
# 9. Method Dispatch Based on Type
# -----------------------------------------------------------------------------

print("\n--- Type-Based Method Dispatch ---")

from functools import singledispatch

@singledispatch
def process(value):
    """Default processing for unknown types."""
    return f"Don't know how to process {type(value).__name__}"

@process.register(int)
def _(value):
    """Process integers."""
    return f"Integer: {value * 2}"

@process.register(str)
def _(value):
    """Process strings."""
    return f"String: {value.upper()}"

@process.register(list)
def _(value):
    """Process lists."""
    return f"List with {len(value)} items"

# Same function name, different behavior based on type
print("Single dispatch polymorphism:")
print(f"  process(42): {process(42)}")
print(f"  process('hello'): {process('hello')}")
print(f"  process([1,2,3]): {process([1,2,3])}")
print(f"  process(3.14): {process(3.14)}")

# -----------------------------------------------------------------------------
# 10. Practical Example: Notification System
# -----------------------------------------------------------------------------

print("\n--- Practical Example: Notification System ---")

class NotificationService(ABC):
    """Abstract base for notification services."""
    
    @abstractmethod
    def send(self, recipient, message):
        """Send a notification."""
        pass
    
    @abstractmethod
    def get_status(self):
        """Get service status."""
        pass

class EmailNotification(NotificationService):
    def __init__(self, smtp_server="mail.example.com"):
        self.smtp_server = smtp_server
    
    def send(self, recipient, message):
        return f"📧 Email sent to {recipient}: '{message}'"
    
    def get_status(self):
        return f"Email service connected to {self.smtp_server}"

class SMSNotification(NotificationService):
    def __init__(self, provider="TwilioMock"):
        self.provider = provider
    
    def send(self, recipient, message):
        return f"📱 SMS sent to {recipient}: '{message[:50]}...'"
    
    def get_status(self):
        return f"SMS service using {self.provider}"

class PushNotification(NotificationService):
    def __init__(self, app_name="MyApp"):
        self.app_name = app_name
    
    def send(self, recipient, message):
        return f"🔔 Push notification to {recipient}: '{message}'"
    
    def get_status(self):
        return f"Push service for {self.app_name}"

class SlackNotification(NotificationService):
    def __init__(self, workspace="MyWorkspace"):
        self.workspace = workspace
    
    def send(self, recipient, message):
        return f"💬 Slack message to #{recipient}: '{message}'"
    
    def get_status(self):
        return f"Slack connected to {self.workspace}"

# Polymorphic notification manager
class NotificationManager:
    """Manages multiple notification services."""
    
    def __init__(self):
        self.services = []
    
    def add_service(self, service):
        self.services.append(service)
    
    def send_all(self, recipient, message):
        """Send via all services."""
        results = []
        for service in self.services:
            results.append(service.send(recipient, message))
        return results
    
    def status(self):
        """Get status of all services."""
        return [service.get_status() for service in self.services]

# Create manager and add services
manager = NotificationManager()
manager.add_service(EmailNotification())
manager.add_service(SMSNotification())
manager.add_service(PushNotification())
manager.add_service(SlackNotification())

print("Service status:")
for status in manager.status():
    print(f"  ✓ {status}")

print("\nSending notification via all channels:")
for result in manager.send_all("user123", "Your order has been shipped!"):
    print(f"  {result}")
