source_GridView_MoviePresenter.bs

' MoviePresenter: Presenter for movie library views
'
' Handles movie-specific:
' - View options (Movies Presentation, Movies Grid, Studios, Genres)
' - Sort/filter options with dynamic filters from API
' - Movie metadata display (title, year, rating, runtime, overview, logo)
' - Critic and community ratings
' - Logo loading

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

class MoviePresenter extends GridPresenterBase

  ' Task for loading filter options from API
  private getFiltersTask

  ' Cached filter options from API
  private apiFilters

  ' Counter for generating unique divider IDs
  private dividerCount

  sub new()
    super()
    m.log = log.Logger("MoviePresenter")
    m.getFiltersTask = invalid
    m.apiFilters = invalid
    m.dividerCount = 0
  end sub

  ' Uses presentation backdrop only for Movies (Presentation) view
  override function getBackdropMode() as string
    if isValid(m.view) and isValid(m.view.top) and isValid(m.view.top.currentView)
      viewLower = LCase(m.view.top.currentView)
      if viewLower = "movies"
        return "presentation"
      end if
    end if
    return "fullscreen"
  end function

  override sub onInit(view as object)
    super.onInit(view)
    ' Create task nodes for logo and filter loading
    ' Store tasks on view (component scope) so observer callbacks work
    m.view.loadLogoTask = CreateObject("roSGNode", "LoadItemsTask2")
    m.view.getFiltersTask = CreateObject("roSGNode", "GetFiltersTask")
  end sub

  ' Show presentation info only for Movies (Presentation) view
  override function shouldShowPresentationInfo(viewMode as string) as boolean
    lowerView = LCase(viewMode)
    return lowerView = "movies"
  end function

  override function getOptions(parentItem as object) as object
    options = {
      views: [
        { "Title": tr("Movies (Presentation)"), "Name": "Movies" },
        { "Title": tr("Movies (Grid)"), "Name": "MoviesGrid" },
        { "Title": tr("Studios"), "Name": "Studios" },
        { "Title": tr("Genres"), "Name": "Genres" }
      ],
      sort: [
        { "Title": tr("TITLE"), "Name": "SortName" },
        { "Title": tr("IMDB_RATING"), "Name": "CommunityRating" },
        { "Title": tr("CRITIC_RATING"), "Name": "CriticRating" },
        { "Title": tr("DATE_ADDED"), "Name": "DateCreated" },
        { "Title": tr("DATE_PLAYED"), "Name": "DatePlayed" },
        { "Title": tr("OFFICIAL_RATING"), "Name": "OfficialRating" },
        { "Title": tr("PLAY_COUNT"), "Name": "PlayCount" },
        { "Title": tr("RELEASE_DATE"), "Name": "PremiereDate" },
        { "Title": tr("RUNTIME"), "Name": "Runtime" },
        { "Title": tr("Random"), "Name": "Random" }
      ],
      filter: [
        { "Title": tr("All"), "Name": "All" },
        { "Title": tr("Favorites"), "Name": "Favorites" },
        { "Title": tr("Played"), "Name": "Played" },
        { "Title": tr("Unplayed"), "Name": "Unplayed" },
        { "Title": tr("Resumable"), "Name": "Resumable" }
      ]
    }

    ' Adjust options for specific views
    if isValid(parentItem) and isValid(parentItem.json) and parentItem.json.type = "Genre"
      ' Genre view has limited options
      options.views = [
        { "Title": tr("Movies (Presentation)"), "Name": "Movies" },
        { "Title": tr("Movies (Grid)"), "Name": "MoviesGrid" }
      ]
    end if

    ' Add dynamic filters from API if loaded
    if isValid(m.apiFilters)
      if isValid(m.apiFilters.Genres)
        options.filter.push({ "Title": tr("Genres"), "Name": "Genres", "Options": m.apiFilters.Genres, "Delimiter": "|", "CheckedState": [] })
      end if
      if isValid(m.apiFilters.OfficialRatings)
        options.filter.push({ "Title": tr("Parental Ratings"), "Name": "OfficialRatings", "Options": m.apiFilters.OfficialRatings, "Delimiter": "|", "CheckedState": [] })
      end if
      if isValid(m.apiFilters.Years)
        options.filter.push({ "Title": tr("Years"), "Name": "Years", "Options": m.apiFilters.Years, "Delimiter": ",", "CheckedState": [] })
      end if
    end if

    return options
  end function

  override function getGridConfig(viewMode as string) as object
    lowerView = LCase(viewMode)
    ' userSettings = m.view.global.user.settings

    if lowerView = "studios"
      ' Studios view - scaleToFit for logos
      return {
        translation: [96, 102],
        itemSize: [264, 396],
        rowHeights: [396],
        numRows: "3",
        numColumns: "6",
        imageDisplayMode: "scaleToFit"
      }
    else if lowerView = "moviesgrid"
      ' Full grid view - rowHeights > itemSize to always leave space for titles
      return {
        translation: [96, 102],
        itemSize: [264, 396],
        rowHeights: [396],
        numRows: "3",
        numColumns: "6",
        imageDisplayMode: "scaleToZoom"
      }
    else if lowerView = "genres"
      ' Genres view - smaller posters
      return {
        translation: [96, 102],
        itemSize: [230, 315],
        rowHeights: [315],
        numRows: "3",
        numColumns: "7",
        imageDisplayMode: "scaleToZoom"
      }
    else
      ' Default: Presentation view - shows metadata panel, fewer rows
      return {
        translation: [96, 650],
        itemSize: [264, 396],
        rowHeights: [396],
        numRows: "2",
        numColumns: "6",
        imageDisplayMode: "scaleToZoom"
      }
    end if
  end function

  override sub configureLoadTask(task as object, parentItem as object, viewMode as string)
    lowerView = LCase(viewMode)

    task.itemType = "Movie"
    task.itemId = parentItem.Id
    task.additionalFields = "Taglines,Genres"

    if lowerView = "studios"
      task.view = "Networks"
      task.studioIds = ""
    else if lowerView = "genres"
      task.view = "Genres"
      task.studioIds = parentItem.Id
    else
      task.view = "Movies"
      task.studioIds = ""
      task.genreIds = ""
    end if

    ' Handle genre/studio parent items
    if isValid(parentItem) and isValid(parentItem.json) and isValid(parentItem.json.type)
      if parentItem.json.type = "Studio"
        task.studioIds = parentItem.id
        task.itemId = parentItem.parentFolder
        task.genreIds = ""
      else if parentItem.json.type = "Genre"
        task.genreIds = parentItem.id
        task.itemId = parentItem.parentFolder
        task.studioIds = ""
      end if
    end if

    ' Load dynamic filters from API (called once per library load)
    m.loadFilters(parentItem)
  end sub

  ' Load dynamic filters from API
  sub loadFilters(parentItem as object)
    if not isValid(m.view.getFiltersTask) then return

    ' Use "onPresenterFiltersLoaded" - bridge function in BaseGridView that forwards to presenter
    m.view.getFiltersTask.observeField("filters", "onPresenterFiltersLoaded")
    m.view.getFiltersTask.params = {
      userid: m.view.global.user.id,
      parentid: parentItem.Id,
      includeitemtypes: "Movie"
    }
    m.view.getFiltersTask.control = "RUN"
  end sub

  ' Called when filters are loaded from API (via onPresenterFiltersLoaded bridge in BaseGridView)
  sub onFiltersLoaded(event as object)
    m.apiFilters = event.getData()
    if isValid(m.view) and isValid(m.view.getFiltersTask)
      m.view.getFiltersTask.unobserveField("filters")
    end if
  end sub

  override sub onItemFocused(item as object, _currentView as string)
    if not isValid(m.view) or not isValid(item) then return

    ' Extract item data
    itemData = item.json
    if not isValid(itemData) then return

    ' Get info nodes from view
    infoGroup = m.view.top.findNode("presentationInfo")
    if not isValid(infoGroup) then return

    movieLogo = infoGroup.findNode("movieLogo")
    selectedMovieName = infoGroup.findNode("selectedMovieName")
    movieInfoGroup = infoGroup.findNode("movieInfoGroup")
    movieGenresGroup = infoGroup.findNode("movieGenres")
    movieDescriptionGroup = infoGroup.findNode("movieDescription")

    ' Hide logo initially
    if isValid(movieLogo) then movieLogo.visible = false

    ' Set movie title
    if isValid(itemData.Name) and isValid(selectedMovieName)
      selectedMovieName.text = itemData.Name
    end if

    ' Dynamically populate movieInfoGroup with available metadata
    if isValid(movieInfoGroup)
      ' Clear existing children
      movieInfoGroup.removeChildrenIndex(movieInfoGroup.getChildCount(), 0)
      m.dividerCount = 0

      ' Production Year
      if isValid(itemData.ProductionYear)
        yearNode = m.createInfoLabelNode("selectedMovieProductionYear")
        yearNode.text = str(itemData.ProductionYear).trim()
        m.displayInfoNode(movieInfoGroup, yearNode)
      end if

      ' Official Rating
      if isValid(itemData.OfficialRating) and itemData.OfficialRating <> ""
        ratingNode = m.createInfoLabelNode("selectedMovieOfficialRating")
        ratingNode.text = itemData.OfficialRating
        m.displayInfoNode(movieInfoGroup, ratingNode)
      end if

      ' Community Rating (if enabled)
      if m.view.global.user.settings.uiMoviesShowRatings
        if isValid(itemData.CommunityRating)
          communityRating = CreateObject("roSGNode", "CommunityRating")
          communityRating.id = "communityRatingDisplay"
          communityRating.rating = itemData.CommunityRating
          communityRating.iconSize = 28
          m.displayInfoNode(movieInfoGroup, communityRating)
        end if

        ' Critic Rating (if enabled)
        if isValid(itemData.CriticRating)
          criticRating = CreateObject("roSGNode", "CriticRating")
          criticRating.id = "criticRatingDisplay"
          criticRating.rating = itemData.CriticRating
          criticRating.iconSize = 28
          m.displayInfoNode(movieInfoGroup, criticRating)
        end if
      end if

      ' Runtime
      if type(itemData.RunTimeTicks) = "LongInteger"
        runtime = int(itemData.RunTimeTicks / 600000000.0 + 0.5) ' Round to nearest minute
        runtimeNode = m.createInfoLabelNode("runtime")
        runtimeNode.text = str(runtime).trim() + " mins"
        m.displayInfoNode(movieInfoGroup, runtimeNode)
      end if
    end if

    ' Dynamically populate movieGenres with genre labels
    if isValid(movieGenresGroup)
      ' Clear existing children
      movieGenresGroup.removeChildrenIndex(movieGenresGroup.getChildCount(), 0)

      ' Add genres concatenated with " / "
      if isValid(itemData.genres) and itemData.genres.count() > 0
        genreNode = CreateObject("roSGNode", "LabelPrimaryMedium")
        genreNode.id = "movieGenres"
        genreNode.horizAlign = "left"
        genreNode.vertAlign = "center"
        genreNode.width = 900
        genreNode.height = 0
        genreNode.text = itemData.genres.join(" / ")
        genreNode.bold = true
        movieGenresGroup.appendChild(genreNode)
      end if
    end if

    ' Dynamically populate movieDescription with tagline and overview
    if isValid(movieDescriptionGroup)
      ' Clear existing children
      movieDescriptionGroup.removeChildrenIndex(movieDescriptionGroup.getChildCount(), 0)

      ' Tagline (stored in taglines array)
      if isValid(itemData.taglines) and itemData.taglines.count() > 0 and itemData.taglines[0] <> ""
        taglineNode = CreateObject("roSGNode", "LabelPrimaryMedium")
        taglineNode.id = "selectedMovieTagline"
        taglineNode.width = 900
        taglineNode.text = itemData.taglines[0]
        movieDescriptionGroup.appendChild(taglineNode)
      end if

      ' Overview
      if isValid(itemData.Overview) and itemData.Overview <> ""
        overviewNode = CreateObject("roSGNode", "LabelPrimarySmall")
        overviewNode.id = "selectedMovieOverview"
        overviewNode.wrap = true
        overviewNode.lineSpacing = 9
        overviewNode.height = 250
        overviewNode.width = 800
        overviewNode.ellipsisText = "..."
        overviewNode.text = itemData.Overview
        movieDescriptionGroup.appendChild(overviewNode)
      end if
    end if

    ' Load logo (use uppercase Id to match raw API JSON)
    m.loadLogo(itemData.Id)
  end sub

  ' Load movie logo image
  private sub loadLogo(itemId as string)
    if not isValid(m.view) or not isValid(m.view.loadLogoTask) then return

    m.view.loadLogoTask.unobserveField("content")
    m.view.loadLogoTask.itemId = itemId
    m.view.loadLogoTask.itemType = "LogoImage"
    ' Use "onPresenterLogoLoaded" - bridge function in BaseGridView that forwards to presenter
    m.view.loadLogoTask.observeField("content", "onPresenterLogoLoaded")
    m.view.loadLogoTask.control = "RUN"
  end sub

  ' Called when logo is loaded (via onPresenterLogoLoaded bridge in BaseGridView)
  sub onLogoLoaded(event as object)
    data = event.getData()
    if isValid(m.view) and isValid(m.view.loadLogoTask)
      m.view.loadLogoTask.unobserveField("content")
      m.view.loadLogoTask.content = []
    end if

    if not isValid(m.view) then return

    infoGroup = m.view.top.findNode("presentationInfo")
    if not isValid(infoGroup) then return

    movieLogo = infoGroup.findNode("movieLogo")
    selectedMovieName = infoGroup.findNode("selectedMovieName")

    ' Always show the movie title
    if isValid(selectedMovieName)
      selectedMovieName.visible = true
    end if

    ' Show logo if available
    if isValid(data) and data.Count() > 0
      if isValid(movieLogo)
        movieLogo.uri = data[0]
        movieLogo.visible = true
      end if
    end if
  end sub

  ' Create a label node for movieInfoGroup
  private function createInfoLabelNode(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

  ' Create a bullet divider node for movieInfoGroup
  private function createInfoDividerNode() as object
    m.dividerCount++
    dividerNode = CreateObject("roSGNode", "LabelPrimarySmall")
    dividerNode.id = "divider" + m.dividerCount.toStr()
    dividerNode.horizAlign = "left"
    dividerNode.vertAlign = "center"
    dividerNode.width = 0
    dividerNode.height = 40
    dividerNode.text = "•"
    dividerNode.bold = true
    return dividerNode
  end function

  ' Add a node to movieInfoGroup with divider if needed
  private sub displayInfoNode(infoGroup as object, node as object)
    if not isValid(node) or not isValid(infoGroup) then return

    ' Add divider if this isn't the first child
    if infoGroup.getChildCount() > 0
      dividerNode = m.createInfoDividerNode()
      infoGroup.appendChild(dividerNode)
    end if

    infoGroup.appendChild(node)
  end sub

  override sub destroy()
    if isValid(m.view) and isValid(m.view.loadLogoTask)
      m.view.loadLogoTask.control = "stop"
      m.view.loadLogoTask.unobserveField("content")
      m.view.loadLogoTask = invalid
    end if
    if isValid(m.view) and isValid(m.view.getFiltersTask)
      m.view.getFiltersTask.control = "stop"
      m.view.getFiltersTask.unobserveField("filters")
      m.view.getFiltersTask = invalid
    end if
    m.apiFilters = invalid
    super.destroy()
  end sub

end class