// EXERCISE 01 — BROKEN ON PURPOSE. Do not copy this shape into real code.
//
// 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.
//
// Symptom as reported by QA: "memory climbs while browsing folders and never
// comes back down, even after we navigate away and the cache is emptied."
//
// 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.
//
// Build and run:
//   swiftc -swift-version 6 -O thumbcache.swift -o /tmp/thumb_broken
//   /tmp/thumb_broken
//
// This file compiles with ZERO warnings under Swift 6 strict concurrency,
// which is the point: the compiler has nothing to say about object lifetime.
// Every measurement is printed as key=value so a script can assert on it.

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. The closure assigned to it captures self. That edge,
    // plus this stored property, closes a cycle ARC cannot break.
    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.
        onComplete = {
            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()
