# Student ID: 012498637
# Student Name: Zakaria Benmoulay
# Course: C950 - Data Structures and Algorithms II

from datetime import datetime, timedelta


SPEED_MPH   = 18                           # all trucks travel at 18 mph
HUB_ADDRESS = "Salt Lake City, UT 84107"  # WGU hub — matches the distance table


class Truck:
    # Let's initialize the truck with assigned packages and starting conditions.
    def __init__(self, truck_id, packages, depart_time, package_table,
                 address_list, distance_matrix):

        self.truck_id        = truck_id
        self.package_ids     = list(packages)   # copy so original list is not modified
        self.depart_time     = depart_time
        self.package_table   = package_table
        self.address_list    = address_list
        self.distance_matrix = distance_matrix

        # Simulation state — updated as deliver_all() runs
        self.current_address = HUB_ADDRESS   # truck starts at the hub
        self.current_time    = depart_time   # clock starts at departure time
        self.total_miles     = 0.0           # accumulates mileage during delivery
        self.route           = []            # list of (package_id, address, arrival_time)


    # Distance and time helpers
    def _distance(self, addr_a, addr_b):

        def find_index(addr):
            addr_clean = addr.strip().lower()

            # Pass 1: exact match
            for i, a in enumerate(self.address_list):
                if a.strip().lower() == addr_clean:
                    return i

            # Pass 2: substring match (handles abbreviations and extra words)
            for i, a in enumerate(self.address_list):
                if addr_clean in a.strip().lower() or a.strip().lower() in addr_clean:
                    return i

            # Pass 3: match on the first two tokens (street number + street name)
            tokens = addr_clean.split()
            if tokens:
                prefix = " ".join(tokens[:2])
                for i, a in enumerate(self.address_list):
                    if a.strip().lower().startswith(prefix):
                        return i

            raise ValueError(f"Address not found in distance table: '{addr}'")

        i = find_index(addr_a)
        j = find_index(addr_b)
        return self.distance_matrix[i][j]

    def _travel_time(self, miles):

        hours = miles / SPEED_MPH
        return timedelta(hours=hours)


    # Core nearest-neighbor delivery algorithm
    def deliver_all(self, return_to_hub=False):

        # Step 1 — Mark all packages on this truck as En Route at departure time
        for pid in self.package_ids:
            pkg = self.package_table.lookup(pid)
            if pkg:
                pkg.mark_en_route(self.depart_time, self.truck_id)

        # Work from a copy so the original package_ids list is preserved
        remaining = list(self.package_ids)

        # Step 2 — Nearest-neighbor delivery loop
        while remaining:

            # Inner scan: find the package with the shortest distance
            # from the truck's current location
            nearest_pid  = None
            nearest_dist = float("inf")  # start with infinity so any real distance wins

            for pid in remaining:
                pkg = self.package_table.lookup(pid)
                if pkg is None:
                    continue

                # Get the correct address for this package at this time
                # (handles Package 9's address correction at 10:20 AM)
                address = self._resolve_address(pid, self.current_time)
                dist    = self._distance(self.current_address, address)

                # Update nearest if this package is closer than the current best
                if dist < nearest_dist:
                    nearest_dist = dist
                    nearest_pid  = pid

            if nearest_pid is None:
                break  # safety check — should not happen with valid data

            # Step 2b — Drive to the nearest package's address
            pkg          = self.package_table.lookup(nearest_pid)
            dest_address = self._resolve_address(nearest_pid, self.current_time)
            travel       = self._travel_time(nearest_dist)

            self.current_time    += travel         # advance the simulated clock
            self.total_miles     += nearest_dist   # accumulate mileage
            self.current_address  = dest_address   # move truck to new location

            # Step 2c — Mark the package as delivered at the current time
            pkg.mark_delivered(self.current_time)
            self.route.append((nearest_pid, dest_address, self.current_time))

            # Step 2d — Remove from remaining list
            remaining.remove(nearest_pid)

        # Step 3 — Return to hub if needed (for driver reassignment to Truck 3)
        if return_to_hub:
            dist_home = self._distance(self.current_address, HUB_ADDRESS)
            self.total_miles  += dist_home
            self.current_time += self._travel_time(dist_home)
            self.current_address = HUB_ADDRESS

    # Package 9 address correction
    def _resolve_address(self, package_id, current_time):

        pkg = self.package_table.lookup(package_id)

        if package_id == 9:
            # Define the correction time as 10:20 AM on the simulation date
            correction_time = self.depart_time.replace(
                hour=10, minute=20, second=0, microsecond=0
            )
            if current_time >= correction_time:
                return "410 S State St"   # corrected address after 10:20 AM
            else:
                return pkg.address        # wrong address before 10:20 AM

        return pkg.address  # all other packages return their address unchanged


    # Route summary display
    def print_route(self):

        print(f"\n=== Truck {self.truck_id} Route ===")
        print(f"Departed hub : {self.depart_time.strftime('%I:%M %p')}")
        for pid, addr, arrival in self.route:
            pkg      = self.package_table.lookup(pid)
            deadline = pkg.deadline if pkg else "?"
            print(
                f"  Pkg {pid:>2} -> {addr:<40} "
                f"Arrived: {arrival.strftime('%I:%M %p')}  "
                f"Deadline: {deadline}"
            )
        print(f"Total miles  : {self.total_miles:.2f}")
        print(f"Finished at  : {self.current_time.strftime('%I:%M %p')}")
