--[[
Krabby Social Framing Prep

Created by Krabby Paddy
https://krabbypaddy.com

Creates social-format timelines from selected Media Pool clips, detects scene
cuts, and optionally builds a Smart Reframe V2 layer above the original V1.

Install:
  macOS:
    ~/Library/Application Support/Blackmagic Design/DaVinci Resolve/Fusion/Scripts/Utility/

Run from Resolve:
  Workspace > Scripts > Utility > KrabbySocialFramingPrep
]]

local WINDOW_ID = "KrabbySocialFramingPrepWindow"
local LOG_PATH = (os.getenv("HOME") or ".") .. "/Desktop/KrabbySocialFramingPrep.log"

local formats = {
  {
    id = "vertical",
    label = "9:16 Vertical",
    suffix = "9x16",
    numerator = 9,
    denominator = 16,
    checkboxId = "format_vertical",
  },
  {
    id = "portrait",
    label = "4:5 Portrait",
    suffix = "4x5",
    numerator = 4,
    denominator = 5,
    checkboxId = "format_portrait",
  },
  {
    id = "square",
    label = "1:1 Square",
    suffix = "1x1",
    numerator = 1,
    denominator = 1,
    checkboxId = "format_square",
  },
}

local resolve = nil
if Resolve then
  resolve = Resolve()
end

if not resolve and bmd then
  resolve = bmd.scriptapp("Resolve")
end

if not resolve then
  error("Could not connect to DaVinci Resolve.")
end

local fusion = resolve:Fusion()
if not fusion and bmd then
  fusion = bmd.scriptapp("Fusion")
end

if not fusion then
  error("Could not connect to Fusion UI Manager.")
end

local ui = fusion.UIManager
local dispatcher = bmd.UIDispatcher(ui)
local win = nil
local logLines = {}
local exportLogToDesktop = false

local function appendLog(message)
  local line = os.date("%Y-%m-%d %H:%M:%S") .. "  " .. tostring(message)
  table.insert(logLines, line)

  if exportLogToDesktop then
    local file = io.open(LOG_PATH, "a")

    if not file then
      return
    end

    file:write(line .. "\n")
    file:close()
  end
end

local function resetLog()
  logLines = {
    "Krabby Social Framing Prep log",
    os.date("%Y-%m-%d %H:%M:%S"),
    "",
  }

  if exportLogToDesktop then
    local file = io.open(LOG_PATH, "w")

    if not file then
      return
    end

    file:write(table.concat(logLines, "\n") .. "\n")
    file:close()
  end
end

local function rawCount(source)
  local count = 0

  for _ in pairs(source or {}) do
    count = count + 1
  end

  return count
end

local function logResolveObject(label, object)
  appendLog(label .. " type=" .. type(object) .. " value=" .. tostring(object))
end

local function getContext()
  local projectManager = resolve:GetProjectManager()
  local project = projectManager:GetCurrentProject()

  if not project then
    error("Open a Resolve project before running this script.")
  end

  local mediaPool = project:GetMediaPool()

  if not mediaPool then
    error("Could not access the project Media Pool.")
  end

  return projectManager, project, mediaPool
end

local function addMediaPoolClip(clips, seen, candidate)
  if candidate == nil then
    return
  end

  local candidateType = type(candidate)
  if candidateType == "number" or candidateType == "string" or candidateType == "boolean" then
    return
  end

  local ok, codec = pcall(function()
    return candidate:GetClipProperty("Video Codec")
  end)

  if not ok or not codec or codec == "" then
    return
  end

  local clipKey = tostring(candidate)
  local okId, mediaId = pcall(function()
    return candidate:GetMediaId()
  end)

  if okId and mediaId and mediaId ~= "" then
    clipKey = mediaId
  end

  if seen[clipKey] then
    return
  end

  seen[clipKey] = true
  table.insert(clips, candidate)
end

local function collectMediaPoolClips(source)
  local clips = {}
  local seen = {}

  for key, value in pairs(source or {}) do
    addMediaPoolClip(clips, seen, value)
    addMediaPoolClip(clips, seen, key)
  end

  return clips
end

local function selectedMediaPoolClips(mediaPool)
  return collectMediaPoolClips(mediaPool:GetSelectedClips() or {})
end

local function addTimelineItem(items, seen, candidate)
  if candidate == nil then
    return
  end

  local candidateType = type(candidate)
  if candidateType == "number" or candidateType == "string" or candidateType == "boolean" then
    return
  end

  local ok = pcall(function()
    return candidate:GetName()
  end)

  if not ok then
    return
  end

  local itemKey = tostring(candidate)
  if seen[itemKey] then
    return
  end

  seen[itemKey] = true
  table.insert(items, candidate)
end

local function collectTimelineItems(source)
  local items = {}
  local seen = {}

  for key, value in pairs(source or {}) do
    addTimelineItem(items, seen, value)
    addTimelineItem(items, seen, key)
  end

  return items
end

local function getTimelineItemsInTrack(timeline, trackType, trackIndex)
  local ok, items = pcall(function()
    if timeline.GetItemsInTrack then
      return timeline:GetItemsInTrack(trackType, trackIndex)
    end

    return nil
  end)

  if ok and items then
    local collected = collectTimelineItems(items)
    appendLog(
      "GetItemsInTrack "
        .. trackType
        .. trackIndex
        .. " raw="
        .. rawCount(items)
        .. " collected="
        .. #collected
    )
    return collected
  end

  appendLog("GetItemsInTrack failed/empty for " .. trackType .. trackIndex .. ": " .. tostring(items))

  ok, items = pcall(function()
    return timeline:GetItemListInTrack(trackType, trackIndex)
  end)

  if ok and items then
    local collected = collectTimelineItems(items)
    appendLog(
      "GetItemListInTrack "
        .. trackType
        .. trackIndex
        .. " raw="
        .. rawCount(items)
        .. " collected="
        .. #collected
    )
    return collected
  end

  appendLog("GetItemListInTrack failed/empty for " .. trackType .. trackIndex .. ": " .. tostring(items))

  return {}
end

local function safeClipName(clip)
  local name = clip:GetName() or clip:GetClipProperty("File Name") or "Selected Clip"
  return string.gsub(name, "[/\\:%*%?\"<>|]", "_")
end

local function timelineNameForClip(clip, format)
  return string.format("%s_%s", safeClipName(clip), format.suffix)
end

local function timelineNameExists(project, timelineName)
  local timelineCount = project:GetTimelineCount()

  for index = 1, timelineCount do
    local timeline = project:GetTimelineByIndex(index)

    if timeline and timeline:GetName() == timelineName then
      return true
    end
  end

  return false
end

local function uniqueTimelineName(project, baseName, reservedNames)
  if not timelineNameExists(project, baseName) and not reservedNames[baseName] then
    reservedNames[baseName] = true
    return baseName
  end

  local index = 2

  while true do
    local candidate = string.format("%s_%02d", baseName, index)

    if not timelineNameExists(project, candidate) and not reservedNames[candidate] then
      reservedNames[candidate] = true
      return candidate
    end

    index = index + 1
  end
end

local function evenRound(value)
  local rounded = math.floor(value + 0.5)

  if rounded % 2 ~= 0 then
    rounded = rounded + 1
  end

  return rounded
end

local function parseDimensions(value)
  if type(value) ~= "string" then
    return nil
  end

  local width, height = string.match(value, "(%d+)%s*[xX]%s*(%d+)")

  if not width or not height then
    return nil
  end

  return tonumber(width), tonumber(height)
end

local function sourceDimensionsForClip(clip, project)
  local width = nil
  local height = nil

  local ok, properties = pcall(function()
    return clip:GetClipProperty()
  end)

  if ok and type(properties) == "table" then
    local possibleKeys = {
      "Resolution",
      "Video Resolution",
      "Frame Size",
      "Dimensions",
    }

    for _, key in ipairs(possibleKeys) do
      width, height = parseDimensions(properties[key])

      if width and height then
        return width, height
      end
    end
  end

  if project then
    width = tonumber(project:GetSetting("timelineResolutionWidth"))
    height = tonumber(project:GetSetting("timelineResolutionHeight"))
  end

  if width and height then
    return width, height
  end

  return 1920, 1080
end

local function outputDimensionsForClip(clip, project, format)
  local _, sourceHeight = sourceDimensionsForClip(clip, project)
  local width = evenRound(sourceHeight * format.numerator / format.denominator)

  return width, sourceHeight
end

local function sourceFrameRangeForClip(clip)
  local ok, frames = pcall(function()
    return clip:GetClipProperty("Frames")
  end)

  local frameCount = tonumber(frames)

  if ok and frameCount and frameCount > 0 then
    return 0, frameCount - 1
  end

  return 0, 0
end

local function setProjectTimelineResolution(project, width, height)
  local widthSet = project:SetSetting("timelineResolutionWidth", tostring(width))
  local heightSet = project:SetSetting("timelineResolutionHeight", tostring(height))

  appendLog(
    "Project resolution "
      .. tostring(width)
      .. "x"
      .. tostring(height)
      .. " widthSet="
      .. tostring(widthSet)
      .. " heightSet="
      .. tostring(heightSet)
  )

  return widthSet and heightSet
end

local function enableCustomTimelineSettings(timeline)
  local settingNames = {
    "useCustomSettings",
    "timelineUseCustomSettings",
    "useCustomTimelineSettings",
    "timelineUseProjectSettings",
  }

  for _, settingName in ipairs(settingNames) do
    pcall(function()
      if settingName == "timelineUseProjectSettings" then
        timeline:SetSetting(settingName, "0")
      else
        timeline:SetSetting(settingName, "1")
      end
    end)
  end
end

local function setTimelineResolution(timeline, width, height)
  enableCustomTimelineSettings(timeline)

  local widthSet = timeline:SetSetting("timelineResolutionWidth", tostring(width))
  local heightSet = timeline:SetSetting("timelineResolutionHeight", tostring(height))

  appendLog(
    "Timeline resolution "
      .. tostring(width)
      .. "x"
      .. tostring(height)
      .. " widthSet="
      .. tostring(widthSet)
      .. " heightSet="
      .. tostring(heightSet)
  )

  return widthSet and heightSet
end

local function clipFillZoomForTimelineItem(item, targetWidth, targetHeight)
  local mediaPoolItem = item:GetMediaPoolItem()

  if not mediaPoolItem then
    return 1
  end

  local sourceWidth, sourceHeight = sourceDimensionsForClip(mediaPoolItem, nil)
  local widthScale = targetWidth / sourceWidth
  local heightScale = targetHeight / sourceHeight

  if widthScale == 0 then
    return 1
  end

  local zoom = math.max(widthScale, heightScale) / widthScale

  if zoom < 1 then
    return 1
  end

  return zoom
end

local function fillTimelineItems(items, targetWidth, targetHeight)
  appendLog("Filling " .. #items .. " timeline item(s) for " .. targetWidth .. "x" .. targetHeight)

  for _, item in ipairs(items) do
    local zoom = clipFillZoomForTimelineItem(item, targetWidth, targetHeight)

    pcall(function()
      item:SetProperty("ZoomX", zoom)
      item:SetProperty("ZoomY", zoom)
    end)
  end
end

local function makeV2ReframeLayer(mediaPool, timeline, sourceClip, targetWidth, targetHeight)
  appendLog("Begin V2 creation for " .. tostring(targetWidth) .. "x" .. tostring(targetHeight))
  appendLog("Initial video track count=" .. tostring(timeline:GetTrackCount("video")))
  local timelineStartFrame = 0
  local okTimelineStart, startFrameValue = pcall(function()
    return timeline:GetStartFrame()
  end)

  if okTimelineStart and tonumber(startFrameValue) then
    timelineStartFrame = tonumber(startFrameValue)
  end

  appendLog("Timeline start frame=" .. tostring(timelineStartFrame))

  if timeline:GetTrackCount("video") < 2 then
    local added = timeline:AddTrack("video")
    appendLog("Add V2 track returned " .. tostring(added))
  end

  appendLog("Post-add video track count=" .. tostring(timeline:GetTrackCount("video")))
  timeline:SetTrackName("video", 1, "Original framing")
  timeline:SetTrackName("video", 2, "Smart Reframe")

  local v1Items = getTimelineItemsInTrack(timeline, "video", 1)
  appendLog("V1 item count=" .. tostring(#v1Items))
  local duplicateClipInfos = {}

  for index, item in ipairs(v1Items) do
    logResolveObject("V1 item " .. index, item)

    local okMedia, mediaPoolItem = pcall(function()
      return item:GetMediaPoolItem()
    end)

    if okMedia and mediaPoolItem then
      local okInfo, clipInfo = pcall(function()
        local sourceStart = item:GetSourceStartFrame()
        local sourceEnd = item:GetSourceEndFrame()
        local recordFrame = item:GetStart(false)

        return {
          mediaPoolItem = mediaPoolItem,
          startFrame = sourceStart,
          endFrame = sourceEnd,
          recordFrame = recordFrame,
          trackIndex = 2,
          mediaType = 1,
        }
      end)

      if okInfo and clipInfo.endFrame > clipInfo.startFrame then
        table.insert(duplicateClipInfos, clipInfo)
        appendLog(
          "Prepared cut duplicate "
            .. index
            .. " start="
            .. tostring(clipInfo.startFrame)
            .. " end="
            .. tostring(clipInfo.endFrame)
            .. " record="
            .. tostring(clipInfo.recordFrame)
        )
      elseif okInfo then
        appendLog(
          "Skipped zero/invalid duration V1 item "
            .. index
            .. " start="
            .. tostring(clipInfo.startFrame)
            .. " end="
            .. tostring(clipInfo.endFrame)
        )
      else
        appendLog("Could not read source range for V1 item " .. index .. ": " .. tostring(clipInfo))
      end
    else
      appendLog("Could not get MediaPoolItem for V1 item " .. index .. ": " .. tostring(mediaPoolItem))
    end
  end

  if #duplicateClipInfos == 0 then
    local startFrame, endFrame = sourceFrameRangeForClip(sourceClip)
    appendLog("Using whole-clip V2 fallback start=" .. tostring(startFrame) .. " end=" .. tostring(endFrame))

    duplicateClipInfos = {
      {
        mediaPoolItem = sourceClip,
        startFrame = startFrame,
        endFrame = endFrame,
        recordFrame = timelineStartFrame,
        trackIndex = 2,
        mediaType = 1,
      },
    }
  end

  appendLog("Appending " .. #duplicateClipInfos .. " clipInfo item(s) to V2")

  local okAppend, appended = pcall(function()
    return mediaPool:AppendToTimeline(duplicateClipInfos)
  end)

  appendLog(
    "AppendToTimeline ok="
      .. tostring(okAppend)
      .. " rawType="
      .. type(appended)
      .. " rawCount="
      .. tostring(rawCount(appended))
      .. " rawValue="
      .. tostring(appended)
  )

  local v2Items = {}

  if okAppend and appended then
    v2Items = collectTimelineItems(appended)
  end

  if #v2Items == 0 and #duplicateClipInfos > 0 then
    appendLog("Batch append produced no items; trying one-by-one append fallback")

    for index, clipInfo in ipairs(duplicateClipInfos) do
      local okSingle, singleAppend = pcall(function()
        return mediaPool:AppendToTimeline({ clipInfo })
      end)

      appendLog(
        "Single append "
          .. index
          .. " ok="
          .. tostring(okSingle)
          .. " rawType="
          .. type(singleAppend)
          .. " rawCount="
          .. tostring(rawCount(singleAppend))
          .. " rawValue="
          .. tostring(singleAppend)
      )

      if okSingle and singleAppend then
        local singleItems = collectTimelineItems(singleAppend)

        for _, item in ipairs(singleItems) do
          table.insert(v2Items, item)
        end
      end
    end
  end

  appendLog("Collected V2 item count=" .. tostring(#v2Items))
  appendLog("Visible video2 item count after append=" .. tostring(#getTimelineItemsInTrack(timeline, "video", 2)))
  fillTimelineItems(v2Items, targetWidth, targetHeight)

  local v2Count = 0
  local reframedCount = 0

  for index, item in ipairs(v2Items) do
    v2Count = v2Count + 1
    local ok, reframed = pcall(function()
      return item:SmartReframe()
    end)

    appendLog("SmartReframe item " .. index .. " ok=" .. tostring(ok) .. " result=" .. tostring(reframed))

    if ok and reframed then
      reframedCount = reframedCount + 1
    end
  end

  appendLog("End V2 creation: v2Count=" .. tostring(v2Count) .. " reframedCount=" .. tostring(reframedCount))

  return v2Count, reframedCount
end

local function createSocialTimeline(project, mediaPool, clip, format, useSmartReframe, reservedTimelineNames)
  local timelineName = uniqueTimelineName(
    project,
    timelineNameForClip(clip, format),
    reservedTimelineNames
  )
  local width, height = outputDimensionsForClip(clip, project, format)

  appendLog("Create timeline " .. timelineName .. " target=" .. width .. "x" .. height)
  setProjectTimelineResolution(project, width, height)

  local timeline = mediaPool:CreateTimelineFromClips(timelineName, { clip })

  if not timeline then
    error(string.format("Could not create timeline '%s'.", timelineName))
  end

  logResolveObject("Created timeline " .. timelineName, timeline)
  project:SetCurrentTimeline(timeline)
  setTimelineResolution(timeline, width, height)
  local detectResult = timeline:DetectSceneCuts()
  appendLog("DetectSceneCuts returned " .. tostring(detectResult))
  fillTimelineItems(getTimelineItemsInTrack(timeline, "video", 1), width, height)

  local v2Count = 0
  local reframedCount = 0

  if useSmartReframe then
    v2Count, reframedCount = makeV2ReframeLayer(mediaPool, timeline, clip, width, height)
  end

  return {
    timeline = timeline,
    name = timelineName,
    width = width,
    height = height,
    v2Count = v2Count,
    reframedCount = reframedCount,
  }
end

local function item(id)
  return win:Find(id)
end

local function setItemText(id, text)
  local target = item(id)
  target.PlainText = text
  target.Text = text
end

local function setStatus(message)
  setItemText("status", message)
end

local function setResult(message)
  setItemText("result_summary", message)
end

local function setLogVisible(isVisible)
  local logBox = item("status")

  if logBox then
    logBox.Visible = isVisible
  end
end

local function isChecked(id)
  local target = item(id)

  if not target then
    return false
  end

  return target.Checked
end

local function describeClips(clips, emptyMessage)
  if #clips == 0 then
    return emptyMessage
  end

  local lines = {
    string.format("%d final clip%s ready:", #clips, #clips == 1 and "" or "s"),
  }

  local maxCount = math.min(#clips, 8)
  for index = 1, maxCount do
    table.insert(lines, "- " .. safeClipName(clips[index]))
  end

  if #clips > 8 then
    table.insert(lines, string.format("- ...and %d more", #clips - 8))
  end

  return table.concat(lines, "\n")
end

local function refreshSelectedClipSummary()
  local ok, result = pcall(function()
    local _, _, mediaPool = getContext()
    return selectedMediaPoolClips(mediaPool)
  end)

  if not ok then
    setItemText("clip_summary", result)
    return
  end

  local clips = result

  setItemText(
    "clip_summary",
    describeClips(
      clips,
      "No Media Pool clip selected. Select one or more final exports and refresh."
    )
  )
end

local function selectedFormatsFromUI()
  local selected = {}

  for _, format in ipairs(formats) do
    if item(format.checkboxId).Checked then
      table.insert(selected, format)
    end
  end

  return selected
end

local function runSetup()
  local currentProject = nil
  local originalWidth = nil
  local originalHeight = nil
  exportLogToDesktop = isChecked("export_log")
  resetLog()
  appendLog("Run started")

  local ok, message = pcall(function()
    local projectManager, project, mediaPool = getContext()
    currentProject = project
    originalWidth = project:GetSetting("timelineResolutionWidth")
    originalHeight = project:GetSetting("timelineResolutionHeight")
    appendLog("Original project resolution=" .. tostring(originalWidth) .. "x" .. tostring(originalHeight))

    local clips = selectedMediaPoolClips(mediaPool)
    local selectedFormats = selectedFormatsFromUI()
    local useSmartReframe = item("smart_reframe").Checked
    appendLog("Selected clip count=" .. tostring(#clips))
    appendLog("Selected format count=" .. tostring(#selectedFormats))
    appendLog("Smart Reframe enabled=" .. tostring(useSmartReframe))

    if #clips == 0 then
      return "Select one or more final exports in the Media Pool first."
    end

    if #selectedFormats == 0 then
      return "Choose at least one social format."
    end

    local totalTimelines = 0
    local notes = {}
    local createdResults = {}
    local reservedTimelineNames = {}

    for _, clip in ipairs(clips) do
      for _, format in ipairs(selectedFormats) do
        local result = createSocialTimeline(
          project,
          mediaPool,
          clip,
          format,
          useSmartReframe,
          reservedTimelineNames
        )
        totalTimelines = totalTimelines + 1
        table.insert(createdResults, result)

        table.insert(
          notes,
          string.format(
            "%s (%dx%d): V2 clips %d, Smart Reframe %d",
            result.name,
            result.width,
            result.height,
            result.v2Count,
            result.reframedCount
          )
        )
      end
    end

    if originalWidth and originalHeight then
      setProjectTimelineResolution(project, originalWidth, originalHeight)
    end

    for _, result in ipairs(createdResults) do
      project:SetCurrentTimeline(result.timeline)
      setTimelineResolution(result.timeline, result.width, result.height)
    end

    projectManager:SaveProject()

    local summary = {}
    local startIndex = math.max(1, #notes - 7)
    for index = startIndex, #notes do
      table.insert(summary, notes[index])
    end

    appendLog("Run complete: created " .. tostring(totalTimelines) .. " timeline(s)")

    local successSummary = string.format(
      "Done. Created %d timeline%s from %d final%s and %d selected format%s.",
      totalTimelines,
      totalTimelines == 1 and "" or "s",
      #clips,
      #clips == 1 and "" or "s",
      #selectedFormats,
      #selectedFormats == 1 and "" or "s"
    )

    setResult(successSummary)

    local detailMessage = string.format(
      "Created %d timeline%s.\n%s",
      totalTimelines,
      totalTimelines == 1 and "" or "s",
      table.concat(summary, "\n")
    )

    if exportLogToDesktop then
      detailMessage = detailMessage .. "\nLog exported: " .. LOG_PATH
    else
      detailMessage = detailMessage .. "\nDesktop log export was off."
    end

    return detailMessage
  end)

  if not ok and currentProject and originalWidth and originalHeight then
    setProjectTimelineResolution(currentProject, originalWidth, originalHeight)
  end

  if ok then
    setStatus(message)
  else
    appendLog("Run failed: " .. tostring(message))
    setResult("Could not finish. Open log details for the Resolve message.")
    setStatus(message)
  end
end

local existingWindow = ui:FindWindow(WINDOW_ID)
if existingWindow then
  existingWindow:Show()
  existingWindow:Raise()
  return
end

win = dispatcher:AddWindow(
  {
    ID = WINDOW_ID,
    Geometry = { 240, 80, 620, 760 },
    WindowTitle = "Krabby Social Framing Prep",
  },
  ui:VGroup {
    Spacing = 9,

    ui:Label {
      Text = "Krabby Social Framing Prep",
      Font = ui:Font { PixelSize = 22, Bold = true },
      StyleSheet = "color: #f0f0f0;",
      Weight = 0,
    },

    ui:Label {
      Text = "Select final exports in the Media Pool, choose formats, then build scene-cut social timelines.",
      WordWrap = true,
      StyleSheet = "color: #b8b8b8;",
      Weight = 0,
    },

    ui:TextEdit {
      ID = "clip_summary",
      ReadOnly = true,
      AcceptRichText = false,
      MinimumSize = { 560, 64 },
      MaximumSize = { 16777215, 82 },
      StyleSheet = "background-color: #20242b; border: 1px solid #4b5563; border-radius: 4px; color: #e7e7e7;",
      Weight = 0,
    },

    ui:HGroup {
      Weight = 0,
      ui:Button {
        ID = "refresh",
        Text = "Refresh Media Pool selection",
        MinimumSize = { 280, 30 },
        StyleSheet = "QPushButton { border: 1px solid #5b6472; border-radius: 14px; padding: 6px 14px; color: #d8d8d8; } QPushButton:hover { background-color: #333945; }",
      },
      ui:HGap(0, 1),
    },

    ui:Label {
      Text = "Formats",
      Font = ui:Font { PixelSize = 13, Bold = true },
      StyleSheet = "color: #f0f0f0;",
      Weight = 0,
    },

    ui:VGroup {
      Spacing = 4,
      Weight = 0,
      ui:CheckBox {
        ID = "format_vertical",
        Text = "9:16 Vertical - keep source height",
        Checked = true,
      },
      ui:CheckBox {
        ID = "format_portrait",
        Text = "4:5 Portrait - keep source height",
        Checked = true,
      },
      ui:CheckBox {
        ID = "format_square",
        Text = "1:1 Square - keep source height",
        Checked = true,
      },
    },

    ui:Label {
      Text = "Options",
      Font = ui:Font { PixelSize = 13, Bold = true },
      StyleSheet = "color: #f0f0f0;",
      Weight = 0,
    },

    ui:CheckBox {
      ID = "smart_reframe",
      Text = "Duplicate V1 to V2 and run Smart Reframe on V2 clips",
      Checked = true,
      Weight = 0,
    },

    ui:HGroup {
      Weight = 0,
      ui:HGap(32, 0),
      ui:Label {
        Text = "V1 keeps the original framing. V2 is the AI pass.",
        WordWrap = true,
        StyleSheet = "color: #b8b8b8;",
        Weight = 1,
      },
    },

    ui:CheckBox {
      ID = "export_log",
      Text = "Export log to Desktop",
      Checked = false,
      Weight = 0,
    },

    ui:Label {
      Text = "Result",
      Font = ui:Font { PixelSize = 13, Bold = true },
      StyleSheet = "color: #f0f0f0;",
      Weight = 0,
    },

    ui:TextEdit {
      ID = "result_summary",
      ReadOnly = true,
      AcceptRichText = false,
      MinimumSize = { 560, 44 },
      MaximumSize = { 16777215, 58 },
      StyleSheet = "background-color: #173c2a; border: 1px solid #2fa56f; border-radius: 5px; color: #ecfff5;",
      Weight = 0,
    },

    ui:HGroup {
      Weight = 0,
      ui:Button {
        ID = "run",
        Text = "Create timelines",
        MinimumSize = { 220, 36 },
        StyleSheet = "QPushButton { background-color: #2fa56f; border: 1px solid #48c58d; border-radius: 16px; padding: 8px 16px; color: #0b1710; font-weight: 700; } QPushButton:hover { background-color: #3fcf8f; }",
      },
      ui:Button {
        ID = "close",
        Text = "Close",
        MinimumSize = { 160, 36 },
        StyleSheet = "QPushButton { border: 1px solid #5b6472; border-radius: 16px; padding: 8px 16px; color: #d8d8d8; } QPushButton:hover { background-color: #333945; }",
      },
      ui:HGap(0, 1),
    },

    ui:CheckBox {
      ID = "show_details",
      Text = "Show details",
      Checked = false,
      Weight = 0,
    },

    ui:TextEdit {
      ID = "status",
      ReadOnly = true,
      AcceptRichText = false,
      MinimumSize = { 560, 72 },
      MaximumSize = { 16777215, 96 },
      Visible = false,
      StyleSheet = "background-color: #20242b; border: 1px solid #4b5563; border-radius: 4px; color: #d8d8d8;",
      Weight = 0,
    },

    ui:HGroup {
      Weight = 0,
      ui:Label {
        Text = "KRABBY\nPADDY",
        Font = ui:Font { PixelSize = 15, Bold = true },
        StyleSheet = "color: #f2f2f2; letter-spacing: 2px;",
        Weight = 0,
      },
      ui:VGroup {
        Weight = 0,
        ui:Label {
          Text = "KrabbyPaddy.com",
          Font = ui:Font { PixelSize = 10, Bold = true },
          StyleSheet = "color: #d8d8d8;",
          Weight = 0,
        },
        ui:Label {
          Text = "All rights reserved. For editorial workflow prep only.",
          Font = ui:Font { PixelSize = 9 },
          StyleSheet = "color: #9ca3af;",
          Weight = 0,
        },
      },
      ui:HGap(0, 1),
    },
  }
)

win.On[WINDOW_ID].Close = function()
  dispatcher:ExitLoop()
end

win.On.refresh.Clicked = function()
  refreshSelectedClipSummary()
end

win.On.run.Clicked = function()
  setResult("Working... Resolve may show Smart Reframe progress while this runs.")
  if isChecked("export_log") then
    setStatus("Running. Log will be exported to " .. LOG_PATH)
  else
    setStatus("Running. Enable details to review this run.")
  end
  runSetup()
end

win.On.close.Clicked = function()
  dispatcher:ExitLoop()
end

win.On.show_details.Clicked = function()
  setLogVisible(item("show_details").Checked)
end

refreshSelectedClipSummary()
setResult("Ready. Select formats, then create timelines.")
setStatus("Details will appear here after a run. Enable Export log to Desktop if you need a file.")
setLogVisible(false)

win:Show()
dispatcher:RunLoop()
win:Hide()
