--[[----------------------------------------------------------------------------
  A7InfoLRDialog.lua

  Baut die Bildansicht mit Overlay-Markern fuer:
   - die von der Kamera gemeldete Fokusposition
     (FocusLocation, MakerNotes-Tag 0x2027) als Fadenkreuz, und
   - die beim Autofokus aktiven Phasen-AF-Punkte
     (focalPlaneAFPointsUsed/-Area, MakerNotes-Tag 0x202A) als kleine Quadrate.

  Feldsemantik (jeweils gegen A7Info-Quellcode und reale Beispieldateien
  verifiziert):
   - FocusLocation ist ein 4er-Array [Referenzbreite, Referenzhoehe, x, y] in
     Pixelkoordinaten des Originalbilds - s. A7Info (mainwindow.cpp:
     erstelleFocusLocationImage / "Zielscheibe") sowie isInAFFocusLocationGroup()
     (von allen Vollformat-Modellen ausser A7M2 geliefert).
   - focalPlaneAFPointArea = {width, height} ist die Groesse eines AF-Punkte-
     RASTERS (typischerweise 640x428), und focalPlaneAFPointsUsed ist eine
     Liste von {x, y}-RASTERPOSITIONEN darin - KEINE Bildpixel-Koordinaten.
     Beide werden per Skalierungsfaktor (Bildgroesse / Rastergroesse) auf
     Anzeigepixel umgerechnet - identisch zu A7Info::erstellePhaseAFImage202A.

  Die Marker sind mitgelieferte Icons (assets/crosshair.png, assets/afpoint.png),
  die per margin_left/margin_top auf einem 'overlapping'-View ueber catalog_photo
  positioniert werden - kein Mogrify/ImageMagick-Kompositing (s. A7Info).
  Das haelt den Aufwand pro Anzeige klein (nur Lua-Layout plus ein paar
  kleine PNGs, kein zusaetzlicher Bildverarbeitungsprozess).

  Develop-Crop/-Rotation WERDEN beruecksichtigt (s. buildCoordinateMapper) -
  catalog_photo zeigt stets das entwickelte (beschnittene/gedrehte) Ergebnis,
  waehrend FocusLocation/AF-Punkt-Koordinaten von der Kamera relativ zum
  ORIGINAL-Sensorbild gemeldet werden (s. 'dims' unten); ohne Umrechnung
  landen Marker bei beschnittenen und/oder hochkant aufgenommenen Fotos an
  voellig falscher Position oder ausserhalb des sichtbaren Bereichs.
  CropAngle (Begradigen-Werkzeug) wird bewusst ignoriert - analog zu A7Info,
  das ebenfalls nur 0/90/270-Grad-Drehungen behandelt; ein Rest-Schraegstand
  von wenigen Grad ist ein deutlich kleineres Problem als die zuvor komplett
  falsch platzierten Marker.

  Hinweis zu zwei nicht offensichtlichen LrView-Fallstricken (s. auch
  Memory project_lrview_overlay_gotchas):
   1. background_color ist laut SDK-Referenz nur fuer catalog_photo/
      scrolled_view dokumentiert, nicht fuer den generischen 'view'-Container -
      f:view{background_color=...} zeichnet daher nichts. Marker muessen als
      Bild-Icons (f:picture) realisiert werden.
   2. margin_left/margin_top positionieren ein Kind in einem 'overlapping'-View
      nur, wenn sie auf einem umschliessenden f:view sitzen - direkt auf dem
      f:picture gesetzt werden sie ignoriert (Icon landet oben links).

  Koordinatentransformationen - getrennt fuer FocusLocation vs. Phasen-AF:

  FocusLocation-Koordinaten [refW, refH, x, y] liegen in LANDSCAPE-Sensor-
  Koordinaten: A7Info erstellt das Focus-Overlay mit den Landscape-Abmessungen
  von m_thePic (mainwindow.cpp:777) und dreht Bild + Overlay gemeinsam per
  setRotation(90) - fuer Hochkant-Fotos sind x/y also Landscape-Koordinaten,
  nicht Portrait. Abbildung: getRawMetadata("orientation")-Code ("BC"/"DA"
  usw.) -> Rotationsformel (s. buildCropLayout::mapFocusToDisplay).
  WICHTIG: getDevelopSettings().orientation gibt fuer Sony-ARW-Hochkantaufnahmen
  "AB" zurueck (LR haelt EXIF-Rotation intern separat); buildCropLayout
  verwendet daher getRawMetadata("orientation") fuer korrekte Hochkant-Dims.

  Phasen-AF-Punkt-Koordinaten sind in Sensor-RASTER-Koordinaten:
      origX = punkt.x / focalPlaneAFPointArea.width  * dims.width
      origY = punkt.y / focalPlaneAFPointArea.height * dims.height
  -> sichtbare Koordinaten (OrientationTransform/pointToVisible)
  -> Crop -> Anzeigegroesse (s. mapToDisplay).
------------------------------------------------------------------------------]]

local LrSystemInfo = import 'LrSystemInfo'
local LrTasks      = import 'LrTasks'
local LrView       = import 'LrView'
local bind         = LrView.bind

local A7InfoLRDialog = {}

-- Maximale Anzeigegroeße: 70% der LR-Fenstergroesse, aber hoechstens
-- 800x680 px damit der Dialog auf grossen Bildschirmen nicht uebergroß wird.
local _appW, _appH  = LrSystemInfo.appWindowSize()
local MAX_DISPLAY_WIDTH  = math.min(800, math.floor(_appW * 0.7))
local MAX_DISPLAY_HEIGHT = math.min(680, math.floor(_appH * 0.7))

-- Schluessel der persistenten Anzeige-Umschalter (LrPrefs.prefsForPlugin),
-- analog zu A7Info::m_focusLocationOn/m_phaseAFLocationsOn/m_trackingAFLocationOn
-- (mainwindow.h:152-155 - Face-Detection-Pendant existiert hier nicht, da dieses
-- Plugin keine Gesichtserkennungs-Marker zeichnet). Als Konstanten exportiert,
-- damit A7InfoLRMenuItem.lua (das die Checkboxen baut/die Defaults setzt) und
-- dieses Modul (das 'visible' der Marker-Views daran bindet) garantiert
-- denselben Schluessel verwenden.
A7InfoLRDialog.PREFS_SHOW_FOCUS_LOCATION = "showFocusLocation"
A7InfoLRDialog.PREFS_SHOW_PHASE_AF       = "showPhaseAfPoints"
A7InfoLRDialog.PREFS_SHOW_TRACKING_AF    = "showTrackingAf"

-- Mitgelieferte Marker-Icons sowie ihre Anker (jeweiliger Bildmittelpunkt),
-- um sie zentriert auf der berechneten Position zu platzieren.
local CROSSHAIR_ASSET  = "assets/crosshair.png"
local CROSSHAIR_SIZE   = 23
local CROSSHAIR_ANCHOR = math.floor(CROSSHAIR_SIZE / 2)

local AF_POINT_ASSET   = "assets/afpoint.png"
local AF_POINT_SIZE    = 17
local AF_POINT_ANCHOR  = math.floor(AF_POINT_SIZE / 2)

-- Legenden-Icons fuer Tracking-AF/Eye-AF (kleine Farbmuster, deren Farben mit
-- denen des nativen Overlay-PNGs uebereinstimmen - s. a7infolr-cli/main.cpp::
-- kColorTrackingAF/kColorEyeAF). Anders als Fadenkreuz/AF-Punkt werden diese
-- beiden Markertypen NICHT als einzelne Lua-Icons positioniert, sondern als
-- Teil des fertig gerenderten Overlay-Bilds (s. createPhotoView) - die Assets
-- hier dienen ausschliesslich der Legende (s. buildLegend).
local TRACKING_AF_SWATCH_ASSET = "assets/trackingaf_swatch.png"
local EYE_AF_SWATCH_ASSET      = "assets/eyeaf_swatch.png"

-- Skaliert die Originalgroesse proportional auf maximal
-- MAX_DISPLAY_WIDTH x MAX_DISPLAY_HEIGHT Pixel (keine Vergroesserung).
--
-- Exportiert (nicht mehr 'local'), weil A7InfoLRMenuItem.lua die Anzeigegroesse
-- bereits VOR dem Aufruf von A7InfoLRCli.readInfo() kennen muss: das native
-- Tracking-/Eye-AF-Overlay wird von a7infolr-cli in exakt dieser Groesse
-- gerendert (--af-overlay-size), die Markerpositionen/-groessen werden dort
-- relativ dazu berechnet (s. main.cpp::buildTrackingOverlay) - und muessen mit
-- der hier in createPhotoView verwendeten Groesse uebereinstimmen, sonst
-- stimmen Overlay und Bild nicht ueberein.
function A7InfoLRDialog.computeDisplaySize(photoWidth, photoHeight)
    local scale = math.min(MAX_DISPLAY_WIDTH / photoWidth, MAX_DISPLAY_HEIGHT / photoHeight, 1)
    return math.floor(photoWidth * scale + 0.5), math.floor(photoHeight * scale + 0.5)
end

-- Zerlegt die von a7infolr-cli gelieferte FocusLocation-Zeichenkette
-- (z.B. "6000 4000 3000 2000 ", s. TA7File::mnSony_0x2027_FocusLocationAsString)
-- in {refWidth, refHeight, x, y}. Liefert nil, wenn das Feld fehlt/ungueltig ist
-- (z.B. A7M2 - s. isInAFFocusLocationGroup).
local function parseFocusLocation(text)
    if type(text) ~= "string" then
        return nil
    end
    local nums = {}
    for n in string.gmatch(text, "%-?%d+") do
        nums[#nums + 1] = tonumber(n)
    end
    if #nums < 4 or nums[1] <= 0 or nums[2] <= 0 then
        return nil
    end
    return { refWidth = nums[1], refHeight = nums[2], x = nums[3], y = nums[4] }
end

-- Liefert {area = {width, height}, points = {{x, y}, ...}}, falls
-- focalPlaneAFPointArea/-PointsUsed eine nutzbare Rastergroesse und
-- mindestens einen Punkt enthalten, sonst nil (z.B. Manueller Fokus,
-- oder Tracking-Folgeaufnahmen, bei denen die Kamera keine Phasen-AF-Punkte
-- mehr meldet - s. Kommentar in A7Info::erstellePhaseAFImage202A).
local function parseAFPointGrid(focus)
    local area = focus.focalPlaneAFPointArea
    local points = focus.focalPlaneAFPointsUsed
    if type(area) ~= "table" or type(points) ~= "table" then
        return nil
    end
    if not (area.width and area.height and area.width > 0 and area.height > 0) then
        return nil
    end
    if #points < 1 then
        return nil
    end
    return { area = area, points = points }
end

--[[----------------------------------------------------------------------------
  Crop-/Rotations-Umrechnung (OrientationTransform/pointToVisible/Crop.ofPhoto)
  auf das hier benoetigte Minimum (s. Modulkommentar zu den zwei Transformationsstufen).

  Lightrooms 'orientation' (getDevelopSettings()) ist einer von 8 Zwei-
  Buchstaben-Codes, die Drehung (Vielfache von 90 Grad) und Spiegelung der
  ANZEIGE gegenueber dem Original kodieren - "AB" bedeutet "wie aufgenommen"
  (keine Drehung/Spiegelung). Rotation/xDelta/yDelta/mirror beschreiben die
  Transformation von "Photo Coordinates" (Ursprung unten links, Y nach oben,
  relativ zum ORIGINALBILD) nach "Visible Coordinates" (Ursprung unten links,
  Y nach oben, relativ zum ANGEZEIGTEN/gedrehten Bild).
------------------------------------------------------------------------------]]
local OrientationTransform = {
    AB = { rotate =    0, xDelta = 0, yDelta = 0, mirror = false },
    BC = { rotate =  -90, xDelta = 0, yDelta = 1, mirror = false },
    CD = { rotate = -180, xDelta = 1, yDelta = 1, mirror = false },
    DA = { rotate = -270, xDelta = 1, yDelta = 0, mirror = false },
    BA = { rotate =    0, xDelta = 0, yDelta = 0, mirror = true  },
    CB = { rotate =  -90, xDelta = 0, yDelta = 1, mirror = true  },
    DC = { rotate = -180, xDelta = 1, yDelta = 1, mirror = true  },
    AD = { rotate = -270, xDelta = 1, yDelta = 0, mirror = true  },
}

-- Dreht den Punkt p um den Ursprung (0,0) um 'angleDegrees' Grad
-- (positiv = gegen den Uhrzeigersinn) - reduzierte Variante von
-- Crop.lua::pRot, hier nur fuer Drehungen um den Ursprung benoetigt.
local function rotateAroundOrigin(p, angleDegrees)
    local a = math.rad(angleDegrees)
    local cosA, sinA = math.cos(a), math.sin(a)
    return { x = p.x * cosA - p.y * sinA, y = p.x * sinA + p.y * cosA }
end

-- Rechnet einen Punkt aus "Photo Coordinates" (Ursprung unten links, Y nach
-- oben, relativ zum ORIGINALBILD der Groesse wv x hv) in "Visible
-- Coordinates" (Ursprung unten links, Y nach oben, relativ zum ANGEZEIGTEN/
-- gedrehten Bild) um.
local function pointToVisible(p, orientation, wv, hv)
    local t = OrientationTransform[orientation] or OrientationTransform.AB
    p = rotateAroundOrigin(p, t.rotate)
    p = { x = p.x + t.xDelta * wv, y = p.y + t.yDelta * hv }
    if t.mirror then
        p.x = -p.x + wv
    end
    return p
end

--[[----------------------------------------------------------------------------
  number displayWidth, number displayHeight, function mapToDisplay
  buildCropLayout(LrPhoto photo)

  Berechnet Anzeigegroeße und Koordinaten-Mapper gemeinsam, damit die
  Anzeigegroeße korrekt aus dem SICHTBAREN Crop-Rechteck (nach Rotation)
  ermittelt wird - nicht aus getLrMetadata("croppedDimensions"), das die
  Rotation ignoriert und daher bei Hochformat-Bildern (orientation "BC"/"DA"
  usw.) falsche Dimensionen liefert.

  Rueckgabe:
    displayWidth, displayHeight  - Anzeigegroeße fuer catalog_photo (Pixel,
                                   passend zum sichtbaren Seitenverhaeltnis
                                   inkl. Rotation, innerhalb MAX_DISPLAY_*)
    mapToDisplay(camX, camY)     - Funktion: Kamerapixel-Koordinate
                                   (Referenzrahmen FocusLocation/AF-Raster:
                                   Ursprung oben links, Y nach unten, relativ
                                   zur ORIGINAL-Sensorgroesse) -> Anzeigepixel
                                   (Ursprung oben links, Y nach unten, relativ
                                   zu displayWidth x displayHeight)

  Vorgehen (s. Crop.lua::Crop.ofPhoto fuer das Vorbild der Crop-Rechteck-
  Berechnung):
   1. Crop-Rechteck-Eckpunkte (aus CropLeft/Right/Top/Bottom) nach "Visible
      Coordinates" transformieren - ergibt das sichtbare Crop-Rechteck in
      Original-Pixel-Einheiten, NACH Rotation/Spiegelung.
   2. computeDisplaySize(cropW, cropH) mit diesen sichtbaren Dimensionen ->
      Anzeigegroeße passt zum tatsaechlichen Seitenverhaeltnis von catalog_photo.
   3. Kamerakoordinate -> "Visible Coordinates" -> relative Position im Crop-
      Rechteck -> Anzeigepixel (Stufe 2 aus Modulkommentar).

  Fehlen Develop-Settings (Video, beschaedigtes Foto), wird mit orientation=AB,
  kein Crop gerechnet -> Spezialfall-Aequivalenz zur alten Skalierungsformel.

  Exportiert als A7InfoLRDialog.computeCropAwareDisplaySize (s.u.) fuer
  A7InfoLRMenuItem.lua (Overlay-Groeße vor CLI-Aufruf).
------------------------------------------------------------------------------]]
local function buildCropLayout(photo)
    local dims = photo:getRawMetadata("dimensions")
    -- Normalize to landscape sensor coordinates. getRawMetadata("dimensions")
    -- returns the dimensions as stored in the ARW EXIF header, which on some Sony
    -- camera models already reflects the display (portrait) orientation. Taking
    -- max/min ensures opw is always the sensor's long axis, matching the landscape
    -- reference frame used by FocusLocation (refWidth > refHeight always).
    local opw = math.max(dims.width, dims.height)
    local oph = math.min(dims.width, dims.height)

    -- getRawMetadata("orientation") liefert die vollstaendige Orientierung
    -- (EXIF-Drehung + eventuelle Benutzer-Rotation in LR). Fuer Sony-ARW-
    -- Hochkantaufnahmen gibt getDevelopSettings().orientation "AB" zurueck
    -- (LR haelt EXIF-Rotation intern separat), waehrend getRawMetadata die
    -- kombinierte Orientierung "BC"/"DA" liefert.
    -- Nur getRawMetadata ergibt korrekte Portrait-Abmessungen (pwv/phv,
    -- cropW/H, displayWidth/Height) und damit das richtige Overlay-Format.
    local orientation = photo:getRawMetadata("orientation") or "AB"
    local cropLeft, cropRight, cropTop, cropBottom = 0, 1, 0, 1
    -- LrTasks.pcall statt pcall: getDevelopSettings() fuehrt intern einen
    -- Lua-Yield durch (asynchrone Katalog-I/O); Standard-pcall unterstuetzt
    -- kein Yielding und wuerde mit "Yielding is not allowed" fehlschlagen.
    local ok, settings = LrTasks.pcall(function() return photo:getDevelopSettings() end)
    if ok and type(settings) == "table" then
        -- Crop-Werte aus getDevelopSettings sind relativ zum ORIGINAL (unrotierten)
        -- Sensor-Bild ("original unrotated, unmirrored image") - d.h. fuer
        -- Hochkant-Fotos in Landscape-Pixel-Koordinaten.
        -- Die Umrechnung erfolgt per pointToVisible mit der EXIF-Orientierung oben.
        cropLeft   = settings.CropLeft   or cropLeft
        cropRight  = settings.CropRight  or cropRight
        cropTop    = settings.CropTop    or cropTop
        cropBottom = settings.CropBottom or cropBottom
    end

    local fullVisible = pointToVisible({ x = opw, y = oph }, orientation, 0, 0)
    local pwv, phv = math.abs(fullVisible.x), math.abs(fullVisible.y)

    local c1v = pointToVisible({ x = cropLeft  * opw, y = (1 - cropTop)    * oph }, orientation, pwv, phv)
    local c2v = pointToVisible({ x = cropRight * opw, y = (1 - cropBottom) * oph }, orientation, pwv, phv)
    local cropMinX, cropMaxX = math.min(c1v.x, c2v.x), math.max(c1v.x, c2v.x)
    local cropMinY, cropMaxY = math.min(c1v.y, c2v.y), math.max(c1v.y, c2v.y)
    local cropW = cropMaxX - cropMinX
    local cropH = cropMaxY - cropMinY

    local displayWidth, displayHeight
    if cropW > 0 and cropH > 0 then
        displayWidth, displayHeight = A7InfoLRDialog.computeDisplaySize(cropW, cropH)
    else
        -- Entartetes Crop-Rechteck (sollte praktisch nie vorkommen)
        displayWidth, displayHeight = A7InfoLRDialog.computeDisplaySize(opw, oph)
        cropW, cropH, cropMinX, cropMinY = opw, oph, 0, 0
    end

    local function mapFocusToDisplay(location)
        local rx = location.x / location.refWidth   -- 0..1 Landscape, links->rechts
        local ry = location.y / location.refHeight  -- 0..1 Landscape, oben->unten

        local px, py
        if orientation == "BC" or orientation == "CB" then
            px = 1 - ry; py = rx           -- EXIF 6: Landscape-Unten -> Display-Links
        elseif orientation == "DA" or orientation == "AD" then
            px = ry;     py = 1 - rx       -- EXIF 8: Landscape-Oben  -> Display-Links
        elseif orientation == "CD" or orientation == "DC" then
            px = 1 - rx; py = 1 - ry       -- 180°
        else
            px = rx;     py = ry           -- "AB"/"BA": keine Drehung
        end

        local visX = px * pwv
        local visY = (1 - py) * phv
        local relX = (visX - cropMinX) / cropW
        local relY = (visY - cropMinY) / cropH
        return relX * displayWidth, (1 - relY) * displayHeight
    end

    return displayWidth, displayHeight, mapFocusToDisplay, cropLeft, cropTop, cropRight, cropBottom
end

-- Exportiert fuer A7InfoLRMenuItem.lua: liefert die korrekte Anzeigegroesse
-- (Crop + Rotation beruecksichtigt, s. buildCropLayout) - muss VOR dem CLI-
-- Aufruf bekannt sein, damit das native Tracking-/Eye-AF-Overlay in exakt
-- dieser Groesse gerendert wird (s. --af-overlay-size / buildTrackingOverlay).
function A7InfoLRDialog.computeCropAwareDisplaySize(photo)
    local dw, dh = buildCropLayout(photo)
    return dw, dh
end

-- Wie computeCropAwareDisplaySize, liefert zusaetzlich die rohen Crop-Werte
-- aus den Develop-Settings (fuer --crop an a7infolr-cli). Alle Werte 0..1,
-- Standardwerte 0,0,1,1 wenn keine Crop-Settings vorhanden.
function A7InfoLRDialog.computeDisplaySizeAndCrop(photo)
    local dw, dh, _, cl, ct, cr, cb = buildCropLayout(photo)
    return dw, dh, cl, ct, cr, cb
end

-- Fuegt ein Marker-Icon (Fadenkreuz oder AF-Punkt-Quadrat) zentriert auf
-- (displayX, displayY) als Kindelement zum Overlay-Stack hinzu. Die
-- margin_*-Werte sitzen bewusst auf dem umschliessenden f:view, nicht auf
-- dem f:picture selbst (s. Hinweis 2 oben) - sie ergeben sich aus
-- Position minus Anker (Bildmittelpunkt des Icons).
--
-- 'visible' (optional): LrView.bind(...)-Ergebnis, das die Sichtbarkeit dieses
-- Markers an einen Anzeige-Umschalter koppelt (s. A7InfoLRDialog.PREFS_SHOW_*
-- und createPhotoView) - live umschaltbar, ohne den Dialog neu aufzubauen.
-- Bleibt es weg (nil), ist der Marker wie bisher immer sichtbar.
local function addMarker(stack, f, asset, anchor, displayX, displayY, visible)
    stack[#stack + 1] = f:view {
        f:picture {
            value = _PLUGIN:resourceId(asset),
        },
        margin_left = math.floor(displayX - anchor),
        margin_top  = math.floor(displayY - anchor),
        visible     = visible,
    }
end

-- Fuegt fuer jeden Phasen-AF-Punkt ein AF-Punkt-Quadrat hinzu.
-- Das AF-Raster ist immer in LANDSCAPE-Sensor-Koordinaten gemeldet (gx=0..640,
-- gy=0..480 decken den Sensor in Landscape-Ausrichtung ab) - identisch zur
-- FocusLocation-Konvention. Die Koordinaten werden daher wie eine FocusLocation
-- mit refWidth=grid.area.width / refHeight=grid.area.height behandelt und ueber
-- mapFocusToDisplay abgebildet. Das deckt korrekt alle Orientierungen ab
-- (Landscape "AB": direktes Mapping; Hochkant "DA"/"BC": Achsentausch via
-- EXIF-Rotation - s. buildCropLayout::mapFocusToDisplay).
local function addAFPoints(stack, f, grid, mapFocusToDisplay, visible)
    for _, point in ipairs(grid.points) do
        if type(point) == "table" and point.x and point.y then
            local displayX, displayY = mapFocusToDisplay({
                refWidth  = grid.area.width,
                refHeight = grid.area.height,
                x         = point.x,
                y         = point.y,
            })
            addMarker(stack, f, AF_POINT_ASSET, AF_POINT_ANCHOR, displayX, displayY, visible)
        end
    end
end

--[[----------------------------------------------------------------------------
  public table view, table markerTypes

  A7InfoLRDialog.createPhotoView(viewFactory f, LrPhoto photo, table focus,
                                 table prefs)

  Liefert eine catalog_photo-Ansicht des Fotos, ueberlagert mit (sofern in
  'focus' vorhanden und gueltig, place = "overlapping"):
   - kleinen Quadraten an den beim Autofokus aktiven Phasen-AF-Punkten
     (focalPlaneAFPointsUsed/-Area, Marker "phaseAF"),
   - einem Fadenkreuz an der von der Kamera gemeldeten Fokusposition
     (FocusLocation, Marker "focusLocation"), und/oder
   - dem nativ von a7infolr-cli gerenderten Tracking-/Eye-AF-Overlay
     (focus.trackingOverlay, Marker "trackingAF"/"eyeAF" - s. dortigen
     Kommentar zu buildTrackingOverlay fuer die genauen Bedingungen/Formen).
  Fehlt alles (z.B. A7M2 ohne FocusLocation-Unterstuetzung, manueller Fokus
  ohne Phasen-AF-Punkte, oder Tracking-Folgeaufnahmen ohne neu gemeldete
  AF-Punkte - s. Modulkommentar), wird nur das Bild ohne Overlay geliefert.

  Das Tracking-/Eye-AF-Rechteck sitzt an derselben Stelle wie das Fadenkreuz
  (beide zentriert auf FocusLocation) und ist deutlich groesser/auffaelliger -
  das Fadenkreuz wuerde dort nur ueberlagern. Es wird daher unterdrueckt, wenn
  das native Overlay vorhanden ist (overlayImage ~= nil); der Toggle
  visibleFocusLocation hat bei Tracking-Aufnahmen entsprechend keine Wirkung.

  'prefs' ist die ueber LrPrefs.prefsForPlugin(nil) ermittelte persistente
  Einstellungstabelle (Schluessel s. PREFS_SHOW_*, von A7InfoLRMenuItem.lua mit
  Defaults vorbelegt). Jeder Marker-Layer wird per LrView.bind an seinen
  zugehoerigen Schluessel gebunden (visible = bind{key=..., object=prefs}) -
  analog zu A7Info::m_focusLocationOn/m_phaseAFLocationsOn/m_trackingAFLocationOn
  (mainwindow.h:152-155), aber dank LrView-Datenbindung *live* umschaltbar:
  ein Klick auf die Checkbox in A7InfoLRMenuItem.lua blendet den Marker sofort
  ein/aus, ohne den Dialog neu aufzubauen.

  Der zweite Rueckgabewert 'markerTypes' (Liste von Marker-Bezeichnern, s.o.)
  beschreibt, welche Markertypen in DIESER Aufnahme ueberhaupt vorkommen
  (unabhaengig vom aktuellen Umschalter-Zustand) - einzige Quelle der Wahrheit
  fuer die Legende (s. A7InfoLRDialog.buildLegend), die ihrerseits die
  Sichtbarkeit ihrer Eintraege an dieselben PREFS_SHOW_*-Schluessel bindet,
  damit Marker und Legendeneintrag stets im Gleichschritt ein-/ausgeblendet
  werden und nie auseinanderlaufen koennen.
------------------------------------------------------------------------------]]
function A7InfoLRDialog.createPhotoView(f, photo, focus, prefs)
    -- buildCropLayout liefert displayWidth/Height sowie mapFocusToDisplay, das
    -- sowohl FocusLocation- als auch Phasen-AF-Raster-Koordinaten korrekt in
    -- Anzeigepixel umrechnet (beide in Landscape-Sensor-Koordinaten, s. Modulkommentar).
    local displayWidth, displayHeight, mapFocusToDisplay = buildCropLayout(photo)

    local imageView = f:catalog_photo {
        photo  = photo,
        width  = displayWidth,
        height = displayHeight,
    }

    if not focus then
        return imageView, {}
    end

    local grid     = parseAFPointGrid(focus)
    local location = parseFocusLocation(focus.focusLocation)

    local trackingOverlay = focus.trackingOverlay
    local overlayImage, overlayMarkers
    if type(trackingOverlay) == "table" then
        overlayImage   = trackingOverlay.image
        overlayMarkers = (type(trackingOverlay.markers) == "table") and trackingOverlay.markers or {}
    end
    overlayMarkers = overlayMarkers or {}

    if not (grid or location or overlayImage) then
        return imageView, {}
    end

    local stack = { imageView, place = "overlapping" }
    local markerTypes = {}

    -- Live-Sichtbarkeitsbindungen - eine pro Umschalter-Kategorie, einmal
    -- erzeugt und auf alle zugehoerigen Marker-Layer angewendet (mehrere
    -- Phasen-AF-Quadrate teilen sich dieselbe Bindung).
    local visibleFocusLocation = bind { key = A7InfoLRDialog.PREFS_SHOW_FOCUS_LOCATION, object = prefs }
    local visiblePhaseAF       = bind { key = A7InfoLRDialog.PREFS_SHOW_PHASE_AF,       object = prefs }
    local visibleTrackingAF    = bind { key = A7InfoLRDialog.PREFS_SHOW_TRACKING_AF,    object = prefs }

    if grid then
        addAFPoints(stack, f, grid, mapFocusToDisplay, visiblePhaseAF)
        markerTypes[#markerTypes + 1] = "phaseAF"
    end

    if location and not overlayImage then
        local displayX, displayY = mapFocusToDisplay(location)
        addMarker(stack, f, CROSSHAIR_ASSET, CROSSHAIR_ANCHOR,
            displayX, displayY, visibleFocusLocation)
        markerTypes[#markerTypes + 1] = "focusLocation"
    end

    if overlayImage then
        -- Bewusst OHNE umschliessendes f:view mit margin_left/top = 0: ein
        -- direkt im 'overlapping'-Stack platziertes f:picture faellt laut
        -- Gotcha 2 (s. Modulkommentar/Memory project_lrview_overlay_gotchas)
        -- ohnehin auf die Standardposition oben-links (0,0) zurueck - genau
        -- das wollen wir hier fuer ein bildfuellendes Overlay in Anzeigegroesse.
        -- 'visible' ist eine generische View-Eigenschaft (anders als
        -- background_color, s. Gotcha 1) - sie greift direkt auf f:picture.
        stack[#stack + 1] = f:picture { value = overlayImage, visible = visibleTrackingAF }
        for _, marker in ipairs(overlayMarkers) do
            markerTypes[#markerTypes + 1] = marker
        end
    end

    return f:view(stack), markerTypes
end

--[[----------------------------------------------------------------------------
  public table or nil

  A7InfoLRDialog.buildLegend(viewFactory f, table markerTypes, table prefs)

  Baut eine Legende (Spalte aus Icon+Beschriftung-Zeilen) fuer genau die
  Marker-Typen, die laut 'markerTypes' (zweiter Rueckgabewert von
  createPhotoView) in DIESER Aufnahme vorkommen - liefert nil, wenn die Liste
  leer ist (kein Overlay vorhanden), damit der Aufrufer in dem Fall keinen
  leeren Abschnitt einbaut.

  Jede Zeile bindet ihre Sichtbarkeit an denselben PREFS_SHOW_*-Schluessel wie
  der zugehoerige Marker-Layer in createPhotoView (Tracking-AF/Eye-AF teilen
  sich PREFS_SHOW_TRACKING_AF, genau wie A7Info::m_trackingAFLocationOn beide
  Rechtecke gemeinsam schaltet) - blendet der Nutzer einen Markertyp per
  Checkbox aus, verschwindet dessen Legendeneintrag live im selben Moment,
  ohne dass beide je auseinanderlaufen koennten.

  Reihenfolge ist fest (nicht die Fundreihenfolge in 'markerTypes'), damit die
  Legende bei wiederholten Aufrufen stets gleich aussieht.
------------------------------------------------------------------------------]]
function A7InfoLRDialog.buildLegend(f, markerTypes, prefs)
    if type(markerTypes) ~= "table" or #markerTypes == 0 then
        return nil
    end

    local present = {}
    for _, marker in ipairs(markerTypes) do
        present[marker] = true
    end

    -- Argumenttabelle fuer f:column wird direkt befuellt (kein table.unpack/
    -- unpack noetig - in Lightrooms Lua 5.1 hiesse die Funktion 'unpack', s.
    -- Hinweis zu Lua-5.1-Kompatibilitaet im Kopfkommentar von JsonDecode.lua).
    local columnArgs = { spacing = f:control_spacing() / 2 }
    local function addRow(asset, title, prefsKey)
        columnArgs[#columnArgs + 1] = f:row {
            spacing = f:label_spacing(),
            visible = bind { key = prefsKey, object = prefs },
            f:picture { value = _PLUGIN:resourceId(asset) },
            f:static_text { title = title },
        }
    end

    if present.focusLocation then
        addRow(CROSSHAIR_ASSET,
            "Cross-hair: focus position reported by camera",
            A7InfoLRDialog.PREFS_SHOW_FOCUS_LOCATION)
    end
    if present.phaseAF then
        addRow(AF_POINT_ASSET,
            "Square: active phase-AF point",
            A7InfoLRDialog.PREFS_SHOW_PHASE_AF)
    end
    if present.trackingAF then
        addRow(TRACKING_AF_SWATCH_ASSET,
            "Rectangle with lines: tracking-AF shot",
            A7InfoLRDialog.PREFS_SHOW_TRACKING_AF)
    end
    if present.eyeAF then
        addRow(EYE_AF_SWATCH_ASSET,
            "Small rectangle: Eye AF / face detection",
            A7InfoLRDialog.PREFS_SHOW_TRACKING_AF)
    end

    if #columnArgs < 1 then
        return nil
    end

    table.insert(columnArgs, 1, f:static_text { title = "Legend:" })
    return f:column(columnArgs)
end

return A7InfoLRDialog
