// EXERCISE 01 — REPAIRED.
//
// A thumbnail pipeline. Each Thumbnailer decodes one image and reports back
// through a completion handler it stores ON ITSELF so the work can be retried.
//
// The repair: the stored closure holds self WEAKLY, so the reference graph is
// no longer a cycle and ARC releases every Thumbnailer when the cache drops
// its last strong reference.
//
// The fixture browses ROUNDS folders in a row, because ONE round does not
// distinguish the defect from healthy behaviour: free() returns memory to the
// allocator, not to the OS, so a single round's footprint looks the same
// either way. Growth ACROSS rounds is the discriminating measurement, and it
// is what this repair flattens.
//
// Build and run:
//   swiftc -swift-version 6 -O thumbcache.swift -o /tmp/thumb_fixed
//   /tmp/thumb_fixed
//
// The observable behaviour — record count and checksum — is unchanged. Only
// the lifetime is. Every measurement is printed as key=value.

import Darwin

// ---------------------------------------------------------------- footprint
// phys_footprint is the number macOS itself uses for per-process memory
// limits. It is not "virtual size" and it is not "resident size".
func footprintBytes() -> UInt64 {
    var info = task_vm_info_data_t()
    var count = mach_msg_type_number_t(
        MemoryLayout<task_vm_info_data_t>.size / MemoryLayout<natural_t>.size)
    let kr = withUnsafeMutablePointer(to: &info) {
        $0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
            task_info(mach_task_self_, task_flavor_t(TASK_VM_INFO), $0, &count)
        }
    }
    return kr == KERN_SUCCESS ? UInt64(info.phys_footprint) : 0
}

// ------------------------------------------------------------------ fixture
let PIXELS = 64 * 1024        // 64 KiB of "decoded pixels" per thumbnail
let COUNT  = 4_000            // thumbnails per folder
let ROUNDS = 8                // folders browsed, one after another

/// Shared bookkeeping, passed explicitly rather than held in a global.
final class Counters {
    var live = 0
    var deinits = 0
    var records = 0
    var checksum: UInt64 = 0
}

final class Thumbnailer {
    let index: Int
    let counters: Counters
    var pixels: [UInt8]

    // Stored on self, but the closure assigned to it captures self weakly,
    // so this property no longer closes a cycle.
    var onComplete: (() -> Void)?

    init(index: Int, counters: Counters) {
        self.index = index
        self.counters = counters
        self.pixels = [UInt8](repeating: UInt8(index & 0xff), count: PIXELS)
        counters.live += 1
    }

    deinit {
        counters.live -= 1
        counters.deinits += 1
    }

    func begin() {
        // The retry handler needs the decoded pixels, so it reaches for self
        // — but weakly. If the Thumbnailer is gone there is nothing left to
        // retry, which is exactly the right semantics here.
        onComplete = { [weak self] in
            guard let self else { return }
            self.counters.checksum &+= UInt64(self.pixels[0]) &+ UInt64(self.index)
            self.counters.records += 1
        }
        onComplete?()
    }
}

/// Browse one folder: build a cache of thumbnails, then navigate away.
func browseOneFolder(counters: Counters) {
    var cache: [Thumbnailer] = []
    cache.reserveCapacity(COUNT)
    for i in 0..<COUNT {
        let t = Thumbnailer(index: i, counters: counters)
        t.begin()
        cache.append(t)
    }
    // Navigating away: the cache is emptied and every strong reference this
    // scope held is dropped.
    cache.removeAll(keepingCapacity: false)
}

// --------------------------------------------------------------------- main
func run() {
    let counters = Counters()
    var afterFirst: UInt64 = 0
    var afterLast: UInt64 = 0

    for round in 1...ROUNDS {
        browseOneFolder(counters: counters)
        let f = footprintBytes()
        if round == 1 { afterFirst = f }
        if round == ROUNDS { afterLast = f }
        print("round=\(round) liveObjects=\(counters.live) footprintKB=\(f / 1024)")
    }

    // Growth AFTER the first round is the discriminating number. The first
    // round always costs something; a healthy program does not keep paying.
    let growthKB = afterLast > afterFirst ? (afterLast - afterFirst) / 1024 : 0
    let perRoundKB = UInt64(COUNT * PIXELS / 1024)

    print("created=\(COUNT * ROUNDS)")
    print("deinits=\(counters.deinits)")
    print("live=\(counters.live)")
    print("records=\(counters.records)")
    print("checksum=\(counters.checksum)")
    print("growthKB=\(growthKB)")
    print("oneRoundPixelsKB=\(perRoundKB)")
    print("verdict=\(counters.live == 0 ? "clean" : "leaked")")
}

run()
