components_video_VideoNotification.bs

import "pkg:/source/roku_modules/log/LogMixin.brs"
import "pkg:/source/utils/misc.bs"

sub init()
  m.log = new log.Logger("VideoNotification")
  m.actionButton = m.top.findNode("actionButton")
  m.fadeInAnimation = m.top.findNode("fadeInAnimation")
  m.fadeOutAnimation = m.top.findNode("fadeOutAnimation")
  m.autoDismissTimer = m.top.findNode("autoDismissTimer")

  m.top.observeField("text", "onTextChanged")
  m.actionButton.observeField("isReady", "onActionButtonReady")
  m.autoDismissTimer.observeField("fire", "onAutoDismissTimerFired")
  m.fadeOutAnimation.observeField("state", "onFadeOutAnimationState")
end sub

sub onTextChanged()
  m.actionButton.text = m.top.text
end sub

' Reposition when TextButton finishes rendering with its actual width
sub onActionButtonReady()
  if m.top.state = "showing" or m.top.state = "paused"
    positionBottomRight()
  end if
end sub

' Show the notification: position bottom-right with 5% safe zone, fade in, set focus.
' Safe to call while already showing — restarts the auto-dismiss timer and repositions
' for updated text/autoDismissSeconds (e.g. contiguous segment transitions).
sub show()
  m.log.info("Showing notification", m.top.text)
  m.top.action = ""

  ' Stop any in-progress timers and animations
  m.autoDismissTimer.control = "stop"
  m.fadeOutAnimation.control = "stop"

  ' Only fade in and grab focus when not already visible
  if m.top.state <> "showing"
    m.top.state = "showing"
    m.actionButton.visible = true
    m.fadeInAnimation.control = "start"
    m.actionButton.setFocus(true)
  end if

  ' Reposition in case text width changed
  positionBottomRight()

  ' Start auto-dismiss timer if configured
  if m.top.autoDismissSeconds > 0
    m.log.verbose("Auto-dismiss timer started", m.top.autoDismissSeconds)
    m.autoDismissTimer.duration = m.top.autoDismissSeconds
    m.autoDismissTimer.control = "start"
  end if
end sub

' Hide the notification without triggering "dismissed" action.
' Used when OSD opens to temporarily pause the notification so it can be re-shown later.
sub hide()
  if m.top.state <> "showing" then return
  m.log.verbose("Notification paused (hidden for OSD)")
  m.top.state = "paused"
  m.autoDismissTimer.control = "stop"
  m.fadeInAnimation.control = "stop"
  m.actionButton.visible = false
  m.actionButton.opacity = 0
end sub

' Dismiss the notification: fade out and release focus
sub dismiss()
  if m.top.state = "hidden" or m.top.state = "dismissing" then return

  ' If paused (hidden for OSD), skip fade-out — button is already invisible
  if m.top.state = "paused"
    m.log.verbose("Dismissed from paused state")
    m.top.state = "hidden"
    if m.top.action = ""
      m.top.action = "dismissed"
    end if
    return
  end if

  m.log.info("Dismissing notification")
  m.top.state = "dismissing"
  m.autoDismissTimer.control = "stop"
  m.fadeInAnimation.control = "stop"
  m.fadeOutAnimation.control = "start"
  ' Focus stays on button during fade-out to prevent key events going nowhere.
  ' Focus is released in onFadeOutAnimationState when animation completes.
end sub

' Called when fade-out animation state changes.
' Guard with m.top.state = "dismissing" so externally stopped animations
' (e.g., show() interrupting a fade-out) don't incorrectly transition to hidden.
sub onFadeOutAnimationState()
  if m.fadeOutAnimation.state = "stopped" and m.top.state = "dismissing"
    m.log.verbose("Fade-out complete, state -> hidden")
    m.actionButton.visible = false
    ' Focus management is the parent's responsibility via the action observer.
    ' Calling setFocus(false) here would race with the parent's setFocus(true)
    ' and can leave nothing focused on some Roku firmware versions.
    m.top.state = "hidden"
    if m.top.action = ""
      m.top.action = "dismissed"
    end if
  end if
end sub

sub onAutoDismissTimerFired()
  dismiss()
end sub

' Position the notification button in the bottom-right corner with 5% safe zone
sub positionBottomRight()
  boundingRect = m.actionButton.localBoundingRect()

  ' Use boundingRect height if valid and button has rendered (height > 35)
  if isValid(boundingRect.height) and boundingRect.height > 35
    buttonHeight = boundingRect.height
  else
    buttonHeight = 83 ' default height matching existing Next Episode button
  end if

  if isValid(boundingRect.width) and boundingRect.width > 0
    buttonWidth = boundingRect.width
  else
    buttonWidth = 250 ' reasonable default
  end if

  m.top.translation = [
    1920 - (1920 * 0.05) - buttonWidth,
    1080 - (1080 * 0.05) - buttonHeight
  ]
end sub

' Clean up all references and observers
sub onDestroy()
  m.log.verbose("Destroying notification")
  m.top.state = "hidden"
  m.autoDismissTimer.control = "stop"
  m.autoDismissTimer.unobserveField("fire")
  m.fadeOutAnimation.unobserveField("state")
  m.actionButton.unobserveField("isReady")
  m.top.unobserveField("text")

  m.actionButton = invalid
  m.fadeInAnimation = invalid
  m.fadeOutAnimation = invalid
  m.autoDismissTimer = invalid
end sub

function onKeyEvent(key as string, press as boolean) as boolean
  if m.top.state <> "showing" then return false
  if not press then return false

  if key = "OK"
    m.log.info("Notification activated via OK press")
    m.autoDismissTimer.control = "stop"
    m.top.action = "activated"
    dismiss()
    return true
  end if

  ' Non-OK keys: return false so the parent (VideoPlayerView) can decide
  ' whether to hide() (pausable for OSD) or dismiss() (permanent).
  return false
end function