// visao.swift — o Vision do macOS para o motion do PrimoCode (sem MediaPipe, sem instalar nada).
//
//   visao recortar <entrada> <saida.png>   recorte da pessoa (VNGeneratePersonSegmentationRequest, .accurate) → PNG com alfa
//   visao ler <quadro.png> [...]           texto (VNRecognizeTextRequest) e rostos (VNDetectFaceRectanglesRequest) de cada quadro
//
// Saída: UMA linha de JSON no stdout. Caixas em pixels, origem no canto de CIMA à esquerda: [x0, y0, x1, y1].
// O lib/estudio/visao.js compila isto uma vez (swiftc) em ~/.primocode/bin e chama sob demanda.
import Foundation
import Vision
import CoreImage
import ImageIO
import UniformTypeIdentifiers

func sair(_ o: [String: Any]) -> Never {
    let d = (try? JSONSerialization.data(withJSONObject: o, options: [])) ?? Data("{\"ok\":false}".utf8)
    FileHandle.standardOutput.write(d)
    FileHandle.standardOutput.write(Data("\n".utf8))
    exit(0)
}

func carregar(_ caminho: String) -> CGImage? {
    guard let src = CGImageSourceCreateWithURL(URL(fileURLWithPath: caminho) as CFURL, nil) else { return nil }
    let opc: [CFString: Any] = [kCGImageSourceCreateThumbnailFromImageAlways: true, kCGImageSourceCreateThumbnailWithTransform: true, kCGImageSourceThumbnailMaxPixelSize: 4096]
    return CGImageSourceCreateThumbnailAtIndex(src, 0, opc as CFDictionary) ?? CGImageSourceCreateImageAtIndex(src, 0, nil)
}

func caixa(_ r: CGRect, _ w: Int, _ h: Int) -> [Double] {
    // o Vision usa coordenadas normalizadas com a origem EMBAIXO
    let W = Double(w), H = Double(h)
    return [Double(r.minX) * W, (1 - Double(r.maxY)) * H, Double(r.maxX) * W, (1 - Double(r.minY)) * H].map { ($0 * 10).rounded() / 10 }
}

func recortar(_ entrada: String, _ saida: String) -> Never {
    guard let img = carregar(entrada) else { sair(["ok": false, "error": "não abri a foto"]) }
    let req = VNGeneratePersonSegmentationRequest()
    req.qualityLevel = .accurate
    req.outputPixelFormat = kCVPixelFormatType_OneComponent8
    let h = VNImageRequestHandler(cgImage: img, options: [:])
    do { try h.perform([req]) } catch { sair(["ok": false, "error": "o Vision falhou: \(error.localizedDescription)"]) }
    guard let masc = req.results?.first?.pixelBuffer else { sair(["ok": false, "semPessoa": true, "error": "sem pessoa"]) }
    // quanto da foto é pessoa (sem pessoa → o recorte não serve)
    CVPixelBufferLockBaseAddress(masc, .readOnly)
    let mw = CVPixelBufferGetWidth(masc), mh = CVPixelBufferGetHeight(masc), linha = CVPixelBufferGetBytesPerRow(masc)
    var soma = 0
    if let base = CVPixelBufferGetBaseAddress(masc) {
        let p = base.assumingMemoryBound(to: UInt8.self)
        for y in 0..<mh { for x in 0..<mw where p[y * linha + x] > 127 { soma += 1 } }
    }
    CVPixelBufferUnlockBaseAddress(masc, .readOnly)
    let cobertura = Double(soma) / Double(max(1, mw * mh))
    if cobertura < 0.02 { sair(["ok": false, "semPessoa": true, "cobertura": cobertura, "error": "sem pessoa na foto"]) }
    // a máscara (menor) vai pro tamanho da foto e vira o alfa
    let ci = CIImage(cgImage: img)
    var mci = CIImage(cvPixelBuffer: masc)
    mci = mci.transformed(by: CGAffineTransform(scaleX: ci.extent.width / mci.extent.width, y: ci.extent.height / mci.extent.height))
    let vazio = CIImage(color: CIColor(red: 0, green: 0, blue: 0, alpha: 0)).cropped(to: ci.extent)
    guard let f = CIFilter(name: "CIBlendWithMask") else { sair(["ok": false, "error": "sem CIBlendWithMask"]) }
    f.setValue(ci, forKey: kCIInputImageKey)
    f.setValue(vazio, forKey: kCIInputBackgroundImageKey)
    f.setValue(mci, forKey: kCIInputMaskImageKey)
    guard let out = f.outputImage else { sair(["ok": false, "error": "a máscara não saiu"]) }
    let ctx = CIContext(options: nil)
    guard let cg = ctx.createCGImage(out, from: ci.extent, format: .RGBA8, colorSpace: CGColorSpace(name: CGColorSpace.sRGB)) else { sair(["ok": false, "error": "não montei o PNG"]) }
    guard let dst = CGImageDestinationCreateWithURL(URL(fileURLWithPath: saida) as CFURL, UTType.png.identifier as CFString, 1, nil) else { sair(["ok": false, "error": "não gravei o PNG"]) }
    CGImageDestinationAddImage(dst, cg, nil)
    if !CGImageDestinationFinalize(dst) { sair(["ok": false, "error": "não gravei o PNG"]) }
    sair(["ok": true, "motor": "vision", "cobertura": (cobertura * 1000).rounded() / 1000, "w": img.width, "h": img.height])
}

func ler(_ arquivos: [String]) -> Never {
    var res: [[String: Any]] = []
    for a in arquivos {
        guard let img = carregar(a) else { res.append(["arquivo": a, "ok": false]); continue }
        let w = img.width, h = img.height
        let t = VNRecognizeTextRequest()
        t.recognitionLevel = .accurate
        t.usesLanguageCorrection = false
        t.recognitionLanguages = ["pt-BR", "en-US"]
        t.minimumTextHeight = 0.012
        let r = VNDetectFaceRectanglesRequest()
        let hd = VNImageRequestHandler(cgImage: img, options: [:])
        try? hd.perform([t, r])
        var textos: [[String: Any]] = []
        for o in t.results ?? [] {
            guard let c = o.topCandidates(1).first else { continue }
            textos.append(["texto": c.string, "conf": (Double(c.confidence) * 100).rounded() / 100, "caixa": caixa(o.boundingBox, w, h)])
        }
        let rostos = (r.results ?? []).map { caixa($0.boundingBox, w, h) }
        res.append(["arquivo": a, "ok": true, "w": w, "h": h, "textos": textos, "rostos": rostos])
    }
    sair(["ok": true, "quadros": res])
}

let A = CommandLine.arguments
if A.count >= 4 && A[1] == "recortar" { recortar(A[2], A[3]) }
if A.count >= 3 && A[1] == "ler" { ler(Array(A.dropFirst(2))) }
sair(["ok": false, "error": "uso: visao recortar <entrada> <saida.png> | visao ler <quadro.png>..."])
