# What an Object Knows and What It Can Do
# attributes and methods — data and behavior inside a class
# redhorndev.com

# ─────────────────────────────────────────────
# Instance attributes — unique per object
# ─────────────────────────────────────────────

class Animal:
    def __init__(self, name, species, size, health, age):
        self.name    = name
        self.species = species
        self.size    = size
        self.health  = health
        self.age     = age
        self.status  = "available"    # default — not passed in

lassie = Animal("Lassie", "dog", "medium", "healthy", 4)

print(lassie.name)      # Lassie
print(lassie.status)    # available — default value

lassie.status = "adopted"
print(lassie.status)    # adopted — modified directly

# ─────────────────────────────────────────────
# Class attributes — shared by all objects
# ─────────────────────────────────────────────

class Animal:
    shelter = "Safe Paws"       # class attribute

    def __init__(self, name, species):
        self.name    = name     # instance attribute
        self.species = species

lassie   = Animal("Lassie",   "dog")
whiskers = Animal("Whiskers", "cat")

print(lassie.shelter)       # Safe Paws
print(whiskers.shelter)     # Safe Paws — same for all

print(lassie.name)          # Lassie
print(whiskers.name)        # Whiskers — unique per object

# ─────────────────────────────────────────────
# Methods — what the object can do
# ─────────────────────────────────────────────

class Animal:
    def __init__(self, name, species, size, health, age):
        self.name    = name
        self.species = species
        self.size    = size
        self.health  = health
        self.age     = age
        self.status  = "available"

    def describe(self):
        print(f"{self.name} is a {self.age} year old, {self.size} sized {self.species}.")
        print(f"Health: {self.health}. Status: {self.status}.")

    def adopt(self):
        self.status = "adopted"
        print(f"{self.name} has been adopted!")

    def update_health(self, new_health):
        self.health = new_health
        print(f"{self.name}'s health updated to: {self.health}.")

    def is_available(self):
        return self.status == "available"    # returns True or False

# ─────────────────────────────────────────────
# Using the class
# ─────────────────────────────────────────────

lassie = Animal("Lassie", "dog", "medium", "ill", 4)

lassie.describe()
# Lassie is a 4 year old, medium sized dog.
# Health: ill. Status: available.

lassie.update_health("healthy")
# Lassie's health updated to: healthy.

print(lassie.is_available())    # True

lassie.adopt()
# Lassie has been adopted!

print(lassie.is_available())    # False

lassie.describe()
# Lassie is a 4 year old, medium sized dog.
# Health: healthy. Status: adopted.

# ─────────────────────────────────────────────
# Multiple objects — independent state
# ─────────────────────────────────────────────

lassie   = Animal("Lassie",   "dog", "medium", "healthy", 4)
whiskers = Animal("Whiskers", "cat", "small",  "healthy", 2)
rex      = Animal("Rex",      "dog", "big",    "ill",     6)

lassie.adopt()                      # only lassie changes
print(lassie.is_available())        # False
print(whiskers.is_available())      # True  — unaffected
print(rex.is_available())           # True  — unaffected

# ─────────────────────────────────────────────
# Quick reference
# ─────────────────────────────────────────────

# self.attr = value         — instance attribute (unique per object)
# ClassName.attr = value    — class attribute (shared by all)
# obj.attr                  — access attribute
# obj.method()              — call method
# obj.method(arg)           — method with argument
#
# methods always take self as first parameter
# self is passed automatically — don't pass it manually
# methods can read, modify, and return attributes
