components_video_OSD.bs

import "pkg:/source/utils/misc.bs"

sub init()
  m.inactivityTimer = m.top.findNode("inactivityTimer")
  m.endsAtTime = m.top.findNode("endsAtTime")
  m.videoLogo = m.top.findNode("videoLogo")
  m.videoTitle = m.top.findNode("videoTitle")
  m.videoSubtitleGroup = m.top.findNode("videoSubtitleGroup")
  m.videoPlayPause = m.top.findNode("videoPlayPause")
  m.videoPositionTime = m.top.findNode("videoPositionTime")
  m.videoRemainingTime = m.top.findNode("videoRemainingTime")
  m.progressBar = m.top.findNode("progressBar")
  m.progressBarBackground = m.top.findNode("progressBarBackground")
  m.clock = m.top.findNode("clock")

  if isValid(m.clock)
    m.clock.observeField("minutes", "setEndsAtText")
  end if
  m.top.observeField("json", "setFields")
  m.top.observeField("visible", "onVisibleChanged")
  m.top.observeField("hasFocus", "onFocusChanged")
  m.top.observeField("progressPercentage", "onProgressPercentageChanged")
  m.top.observeField("playbackState", "onPlaybackStateChanged")

  m.isFirstRun = true
  m.defaultButtonIndex = 1
  m.focusedButtonIndex = 1
  m.subtitleDividerCount = 0

  m.buttonMenuRight = m.top.findNode("buttonMenuRight")
  m.buttonMenuLeft = m.top.findNode("buttonMenuLeft")
  m.buttonMenuLeft.buttonFocused = m.defaultButtonIndex
end sub

' setFields: Parses video metadata from JSON and populates OSD component fields
'
' This function processes video data to populate all OSD display fields including title,
' ratings, runtime, and content type classification (isMovie/isSeries flags).
'
' Content Type Detection:
' The isMovie and isSeries flags determine which user settings control ratings display:
' - isMovie=true: Uses uiMoviesShowRatings setting
' - isSeries=true: Uses uiTvShowsDisableCommunityRating setting
'
' Detection Priority (applied in order):
' 1. API flags: Uses IsMovie/IsSeries from Jellyfin server if provided
' 2. Flag validation: Ensures mutual exclusivity (both cannot be true)
' 3. Type-based: Recording types always classified as series content
' 4. Metadata heuristic: Presence of SeriesName/SeasonNumber indicates series
' 5. Default fallback: Assumes movie content if no other indicators
'
' @return {void}
sub setFields()
  if not isValid(m.top.json) or m.top.json = "" then return

  videoData = parseJson(m.top.json)
  ' print "OSD videoData = ", videoData
  mediaStreams = videoData.MediaStreams
  m.top.json = ""

  if isValid(videoData.Type)
    m.top.type = videoData.Type
  end if

  ' Set isMovie and isSeries flags using the following priority:
  ' 1. API flags (IsMovie, IsSeries) from Jellyfin server if available
  ' 2. Type-based detection for Recording types (treated as series-like content)
  ' 3. Heuristic fallback using series/episode metadata presence
  '
  ' Note: These flags are mutually exclusive - content cannot be both a movie and series.
  ' Recording types are consistently treated as series content throughout the codebase
  ' (see OSD.bs:297, Main.bs:266, backdropUtils.bs:33, etc.)
  '
  ' For TvChannel and Program types, check CurrentProgram if available; for others, check top-level
  checkData = invalid
  if (m.top.type = "TvChannel" or m.top.type = "Program" or m.top.type = "LiveTvProgram") and isValid(videoData.CurrentProgram)
    checkData = videoData.CurrentProgram
  else
    checkData = videoData
  end if

  if isValid(checkData)
    ' Try using API flags first
    if isValid(checkData.IsMovie)
      m.top.isMovie = checkData.IsMovie
    end if
    if isValid(checkData.IsSeries)
      m.top.isSeries = checkData.IsSeries
    end if

    ' Validate mutual exclusivity: both flags cannot be true
    if m.top.isMovie and m.top.isSeries
      ' API provided conflicting flags - prioritize IsSeries as it's more specific
      m.top.isMovie = false
    end if

    ' Fallback heuristic: if no flags provided, infer from metadata
    if not m.top.isMovie and not m.top.isSeries
      ' Recording types are episode-like content (same as Episode handling elsewhere)
      if m.top.type = "Recording"
        m.top.isSeries = true
        ' If it has series/episode metadata, it's a series
      else if isValid(checkData.SeriesName) or isValid(checkData.SeasonNumber) or isValid(checkData.ParentIndexNumber)
        m.top.isSeries = true
        ' Otherwise, assume it's movie content
      else
        m.top.isMovie = true
      end if
    end if
  end if

  if isValid(videoData.logoImage) and videoData.logoImage <> ""
    m.top.videoLogo = videoData.logoImage
  end if

  if isValid(videoData.Name)
    m.top.videoTitle = videoData.Name
  end if

  if isValid(videoData.Overview)
    m.top.overview = videoData.Overview
  end if

  if isValid(videoData.SeriesStudio)
    m.top.seriesStudio = videoData.SeriesStudio
  end if

  if isValid(videoData.HasSubtitles)
    m.top.hasSubtitles = videoData.HasSubtitles
  end if

  if isValid(videoData.Chapters) and videoData.Chapters.Count() > 0
    m.top.hasChapters = true
  end if

  if isValid(videoData.SeriesName)
    m.top.seriesName = videoData.SeriesName
  end if

  if isValid(videoData.ParentIndexNumber)
    m.top.seasonNumber = videoData.ParentIndexNumber
  end if

  if isValid(videoData.IndexNumber)
    m.top.episodeNumber = videoData.IndexNumber
  end if

  if isValid(videoData.IndexNumberEnd)
    m.top.episodeNumberEnd = videoData.IndexNumberEnd
  end if

  ' For TvChannel/Program, ratings come from CurrentProgram; for others, top-level
  if (m.top.type = "TvChannel" or m.top.type = "Program" or m.top.type = "LiveTvProgram") and isValid(videoData.CurrentProgram)
    if isValid(videoData.CurrentProgram.CommunityRating)
      m.top.communityRating = videoData.CurrentProgram.CommunityRating
    end if
    if isValid(videoData.CurrentProgram.CriticRating)
      m.top.criticRating = videoData.CurrentProgram.CriticRating
    end if
  else
    if isValid(videoData.CommunityRating)
      m.top.communityRating = videoData.CommunityRating
    end if
    if isValid(videoData.CriticRating)
      m.top.criticRating = videoData.CriticRating
    end if
  end if

  if isValid(videoData.OfficialRating)
    m.top.officialRating = videoData.OfficialRating
  end if

  if isValid(videoData.PremiereDate)
    m.top.premiereDate = videoData.PremiereDate
  end if

  if isValid(videoData.RunTimeTicks)
    m.top.runTimeTicks = videoData.RunTimeTicks
    m.top.runTimeMinutes = ticksToMinutes(m.top.runTimeTicks)
  end if

  if isValid(videoData.ProductionYear)
    m.top.productionYear = videoData.ProductionYear
  end if

  if isValid(videoData.CurrentProgram)
    m.top.runTimeTicks = videoData.CurrentProgram.RunTimeTicks
    m.top.runTimeMinutes = ticksToMinutes(m.top.runTimeTicks)

    m.top.currentProgram = FormatJson(videoData.CurrentProgram)
  end if

  numVideoStreams = 0
  numAudioStreams = 0

  if isValid(mediaStreams)
    for each stream in mediaStreams
      if stream.Type = "Video"
        numVideoStreams++
      else if stream.Type = "Audio"
        numAudioStreams++
      end if
    end for
  end if

  m.top.numVideoStreams = numVideoStreams
  m.top.numAudioStreams = numAudioStreams

  setButtonStates()
  populateData()
end sub

sub populateData()
  setVideoLogoGroup()
  setVideoTitle()
  setVideoSubTitle()
end sub

' setButtonStates: Disable previous/next buttons if needed and remove any other unneeded buttons
sub setButtonStates()
  queueCount = m.global.queueManager.callFunc("getCount")
  queueIndex = m.global.queueManager.callFunc("getPosition")

  ' Disable these buttons as needed

  ' Item Previous
  if queueCount = 1 or queueIndex = 0
    itemPrevious = m.buttonMenuLeft.findNode("itemBack")
    itemPrevious.enabled = false
  end if
  ' Item Next
  if queueIndex + 1 >= queueCount
    itemNext = m.buttonMenuLeft.findNode("itemNext")
    itemNext.enabled = false
  end if

  ' Remove these buttons as needed

  ' Audio Track
  if m.top.numAudioStreams < 2
    m.buttonMenuLeft.removeChild(m.buttonMenuLeft.findNode("showAudioMenu"))
  end if
  ' Subtitles
  if not m.top.hasSubtitles
    m.buttonMenuLeft.removeChild(m.buttonMenuLeft.findNode("showSubtitleMenu"))
  end if
  ' Chapters
  if not m.top.hasChapters
    m.buttonMenuLeft.removeChild(m.buttonMenuLeft.findNode("chapterList"))
  end if
end sub

sub setEndsAtText()
  if m.global.user.settings.uiDesignHideClock
    endsAtText = m.top.findNode("endsAtText")
    endsAtText.visible = false
    m.endsAtTime.text = ""
    return
  end if

  ' if type is tvchannel, set endsAtTime to current program enddate
  if m.top.type = "TvChannel" and m.top.currentProgram <> ""
    currentProgram = ParseJson(m.top.currentProgram)
    if isValid(currentProgram.EndDate) and currentProgram.EndDate <> ""
      ' Parse the EndDate and format as time only
      endDateTime = CreateObject("roDateTime")
      endDateTime.fromISO8601String(currentProgram.EndDate)
      endDateTime.toLocalTime()
      m.endsAtTime.text = formatTime(endDateTime)
      return
    end if
  end if

  ' for all other types, calculate endsAtTime based on remainingPositionTime
  date = CreateObject("roDateTime")
  endTime = int(m.top.remainingPositionTime)
  date.fromSeconds(date.asSeconds() + endTime)
  date.toLocalTime()

  m.endsAtTime.text = formatTime(date)
end sub

sub setVideoLogoGroup()
  m.videoLogo.uri = m.top.videoLogo

  if m.top.currentProgram = "" then return
  currentProgram = ParseJson(m.top.currentProgram)

  if isValidAndNotEmpty(currentProgram) and m.top.type = "TvChannel"
    ' set video title to current program name
    if isValid(currentProgram.ImageTags) and isValidAndNotEmpty(currentProgram.ImageTags.Primary)
      imgParams = {
        maxHeight: 300,
        maxWidth: 500,
        quality: 90,
        tag: currentProgram.ImageTags.Primary
      }

      m.videoLogo.uri = buildURL(Substitute("/items/{0}/images/Primary/0", currentProgram.id), imgParams)
    else
      ' Set empty URI when no valid program image tags exist
      m.videoLogo.uri = ""
    end if
  end if
end sub

sub setVideoTitle()
  m.videoTitle.text = m.top.videoTitle

  if m.top.currentProgram = "" then return
  currentProgram = ParseJson(m.top.currentProgram)

  if isValidAndNotEmpty(currentProgram) and m.top.type = "TvChannel"
    if isValid(currentProgram.Name)
      m.videoTitle.text = currentProgram.Name
    end if
  end if
end sub

sub setVideoSubTitle()
  ' start fresh by removing all subtitle nodes
  m.videoSubtitleGroup.removeChildrenIndex(m.videoSubtitleGroup.getChildCount(), 0)

  airDateNodeCreated = false

  ' EPISODE
  if m.top.type = "Episode" or m.top.type = "Recording"
    ' Title
    if m.top.seriesName <> ""
      m.videoTitle.text = m.top.seriesName
    end if

    ' episodeInfo
    episodeInfoText = ""
    '
    ' Season number
    if isValid(m.top.seasonNumber)
      episodeInfoText = episodeInfoText + `${tr("S")}${m.top.seasonNumber}`
    else
      episodeInfoText = episodeInfoText + `${tr("S")}?`
    end if
    ' Episode number
    if isValid(m.top.episodeNumber)
      episodeInfoText = episodeInfoText + `${tr("E")}${m.top.episodeNumber}`
    else
      episodeInfoText = episodeInfoText + `${tr("E")}??`
    end if
    ' Episode number end
    if isValid(m.top.episodeNumberEnd) and m.top.episodeNumberEnd <> 0 and m.top.episodeNumberEnd > m.top.episodeNumber
      ' add entry for every episode eg. S6:E1E2
      for i = m.top.episodeNumber + 1 to m.top.episodeNumberEnd
        episodeInfoText = episodeInfoText + `${tr("E")}${m.top.episodeNumberEnd}`
      end for
    end if
    ' Episode name
    if isValid(m.top.videoTitle) and m.top.videoTitle <> ""
      episodeInfoText = episodeInfoText + ` - ${m.top.videoTitle}`
    end if

    if episodeInfoText <> ""
      episodeInfoNode = createSubtitleLabelNode("episodeInfo")
      episodeInfoNode.text = episodeInfoText
      displaySubtitleNode(episodeInfoNode)
    end if
  else if m.top.type = "Movie"
    ' videoAirDate
    if isValid(m.top.productionYear) and m.top.productionYear > 0
      airDateNodeCreated = true

      productionYearNode = createSubtitleLabelNode("productionYear")
      productionYearNode.text = m.top.productionYear.toStr().trim()

      displaySubtitleNode(productionYearNode)
    end if
  else if m.top.type = "TvChannel"
    ' TvChannel
    if isValid(m.top.currentProgram) and m.top.currentProgram <> ""
      currentProgram = ParseJson(m.top.currentProgram)

      if isValidAndNotEmpty(currentProgram)
        ' Channel Number i.e. "CH 300"
        if isValid(currentProgram.ChannelNumber) and currentProgram.ChannelNumber <> ""
          currentChannelNumber = createSubtitleLabelNode("currentChannelNumber")
          currentChannelNumber.text = tr("CH") + ` ${currentProgram.ChannelNumber}`

          displaySubtitleNode(currentChannelNumber)
        end if
        ' Channel Name
        if isValid(currentProgram.Name)
          currentChannelName = createSubtitleLabelNode("currentChannelName")
          currentChannelName.text = currentProgram.ChannelName

          displaySubtitleNode(currentChannelName)
        end if
      end if
    end if
  end if

  ' append these to all video types
  '
  userSettings = m.global.user.settings

  ' Official Rating
  if isValid(m.top.officialRating) and m.top.officialRating <> ""
    officialRatingNode = createSubtitleLabelNode("officialRating")
    officialRatingNode.text = m.top.officialRating
    displaySubtitleNode(officialRatingNode)
  end if

  ' Determine if ratings should be shown based on content type and user settings
  showRatings = false
  if m.top.isMovie
    ' Movie content - respect uiMoviesShowRatings setting
    showRatings = userSettings.uiMoviesShowRatings
  else if m.top.isSeries
    ' Series content - respect uiTvShowsDisableCommunityRating setting
    showRatings = not userSettings.uiTvShowsDisableCommunityRating
  else
    ' Unknown/other content types - show if metadata exists
    showRatings = true
  end if

  if showRatings
    ' communityRating (star + rating)
    if isValid(m.top.communityRating) and m.top.communityRating <> 0
      communityRatingNode = CreateObject("roSGNode", "CommunityRating")
      communityRatingNode.id = "communityRating"
      communityRatingNode.rating = m.top.communityRating
      communityRatingNode.iconSize = 30
      displaySubtitleNode(communityRatingNode)
    end if

    ' criticRating (tomato + rating)
    if isValid(m.top.criticRating) and m.top.criticRating <> 0
      criticRatingNode = CreateObject("roSGNode", "CriticRating")
      criticRatingNode.id = "criticRating"
      criticRatingNode.rating = m.top.criticRating
      criticRatingNode.iconSize = 30
      displaySubtitleNode(criticRatingNode)
    end if
  end if

  ' videoAirDate if needed
  if not airDateNodeCreated and isValid(m.top.premiereDate) and m.top.premiereDate <> ""
    premiereDateNode = createSubtitleLabelNode("videoAirDate")
    premiereDateNode.text = formatIsoDateVideo(m.top.premiereDate)
    displaySubtitleNode(premiereDateNode)
  end if

  ' videoRunTime
  if isValid(m.top.runTimeMinutes) and m.top.runTimeMinutes <> 0
    runTimeNode = createSubtitleLabelNode("videoRunTime")

    if m.top.runTimeMinutes < 2
      runTimeText = `${m.top.runTimeMinutes} ` + tr("min")
    else
      runTimeText = `${m.top.runTimeMinutes} ` + tr("mins")
    end if

    runTimeNode.text = runTimeText
    displaySubtitleNode(runTimeNode)
  end if

end sub

sub onProgressPercentageChanged()
  ' change progress bar for live tv
  if m.top.type = "TvChannel"
    m.videoPositionTime.text = secondsToTimestamp(m.top.positionTime, true)
    m.videoRemainingTime.text = tr("LIVE")
    m.progressBar.width = m.progressBarBackground.width ' set to full width
  else
    m.videoPositionTime.text = secondsToTimestamp(m.top.positionTime, true)
    m.videoRemainingTime.text = "-" + secondsToTimestamp(m.top.remainingPositionTime, true)
    m.progressBar.width = m.progressBarBackground.width * m.top.progressPercentage
  end if

  setEndsAtText()
end sub

sub onPlaybackStateChanged()
  if LCase(m.top.playbackState) = "playing"
    m.videoPlayPause.icon = "pkg:/images/icons/pause.png"
    return
  end if

  m.videoPlayPause.icon = "pkg:/images/icons/play.png"
end sub

sub resetFocusToDefaultButton()
  ' Remove focus from previously selected button
  for each child in m.buttonMenuLeft.getChildren(-1, 0)
    if isValid(child)
      child.setFocus(false)
    end if
  end for

  for each child in m.buttonMenuRight.getChildren(-1, 0)
    if isValid(child)
      child.setFocus(false)
    end if
  end for

  ' Set focus back to the default button
  m.buttonMenuLeft.setFocus(true)
  m.focusedButtonIndex = m.defaultButtonIndex
  m.buttonMenuLeft.getChild(m.defaultButtonIndex).setFocus(true)
  m.buttonMenuLeft.buttonFocused = m.defaultButtonIndex
end sub

sub onVisibleChanged()
  if m.top.visible
    resetFocusToDefaultButton()

    if m.top.playbackState <> "paused"
      m.inactivityTimer.observeField("fire", "inactiveCheck")
      m.inactivityTimer.control = "start"
    end if
  else
    m.inactivityTimer.control = "stop"
    m.inactivityTimer.unobserveField("fire")
  end if
end sub

sub onFocusChanged()
  if m.top.hasfocus
    m.buttonMenuLeft.setFocus(true)
  end if
end sub

' inactiveCheck: Checks if the time since last keypress is greater than or equal to the allowed inactive time of the menu.
sub inactiveCheck()
  ' If user is currently seeing a dialog box, ignore inactive check
  if m.global.sceneManager.callFunc("isDialogOpen")
    return
  end if

  deviceInfo = CreateObject("roDeviceInfo")
  if deviceInfo.timeSinceLastKeypress() >= m.top.inactiveTimeout
    m.top.action = "hide"
  end if
end sub

sub onButtonSelected()
  if m.buttonMenuLeft.isInFocusChain()
    selectedButton = m.buttonMenuLeft.getChild(m.buttonMenuLeft.buttonFocused)
  else if m.buttonMenuRight.isInFocusChain()
    selectedButton = m.buttonMenuRight.getChild(m.buttonMenuRight.buttonFocused)
  else
    return
  end if

  if LCase(selectedButton.id) = "chapterlist"
    m.top.showChapterList = not m.top.showChapterList
  end if

  m.top.action = selectedButton.id
end sub

function createSubtitleLabelNode(labelId as string) as object
  labelNode = CreateObject("roSGNode", "LabelPrimaryMedium")
  labelNode.id = labelId
  labelNode.horizAlign = "left"
  labelNode.vertAlign = "center"
  labelNode.width = 0
  labelNode.height = 0
  labelNode.bold = true

  return labelNode
end function

function createSubtitleDividerNode() as object
  m.subtitleDividerCount++

  labelNode = CreateObject("roSGNode", "LabelPrimarySmall")
  labelNode.id = "divider" + m.subtitleDividerCount.toStr()
  labelNode.horizAlign = "left"
  labelNode.vertAlign = "center"
  labelNode.width = 0
  labelNode.height = 40
  labelNode.text = "•"
  labelNode.bold = true

  return labelNode
end function

sub displaySubtitleNode(node as object)
  if not isValid(node) then return

  subtitleChildrenCount = m.videoSubtitleGroup.getChildCount()
  if subtitleChildrenCount > 0
    ' add a divider
    dividerNode = createSubtitleDividerNode()
    m.videoSubtitleGroup.appendChild(dividerNode)
  end if

  m.videoSubtitleGroup.appendChild(node)
end sub

sub OnScreenShown()
  if m.isFirstRun
    m.isFirstRun = false
  else
    m.clock.callFunc("resetTime")
  end if
end sub

function onKeyEvent(key as string, press as boolean) as boolean
  if not press then return false

  if key = "play"
    m.top.action = "videoplaypause"
    return true
  end if

  if key = "OK"
    onButtonSelected()
    return true
  end if

  if key = "back" and m.top.visible
    m.top.action = "hide"

    return true
  end if

  if (key = "rewind" or key = "fastforward") and m.top.visible
    m.top.action = "hide"

    return false
  end if

  return false
end function