#!/usr/bin/env ruby
# frozen_string_literal: true

# This file is part of BigBlueButton.
#
# Copyright © BigBlueButton Inc. and by respective authors.
#
# BigBlueButton is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, either version 3.0 of the License, or (at your
# option) any later version.
#
# BigBlueButton is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with BigBlueButton. If not, see <https://www.gnu.org/licenses>.

require File.expand_path('../../lib/recordandplayback', __dir__)
require File.expand_path('../../lib/recordandplayback/edl', __dir__)
require 'active_support/core_ext/hash'
require 'optimist'
require 'yaml'
require 'nokogiri'
require 'erb'
require 'cgi'

opts = Optimist.options do
  opt :meeting_id, 'Meeting id to process', type: String
  opt :stderr, 'Log output to stderr'
end
Optimist.die :meeting_id, 'must be provided' unless opts[:meeting_id]
meeting_id = opts[:meeting_id]

start_real_time = nil
begin
  m = /-(\d+)$/.match(meeting_id)
  start_real_time = m[1].to_i
end

# Load parameters and set up paths
props = YAML.safe_load(File.open(File.expand_path('../bigbluebutton.yml', __dir__)))
video_props = YAML.safe_load(File.open(File.expand_path('../video.yml', __dir__)))
video_props['audio_offset'] = 0 if video_props['audio_offset'].nil?

recording_dir = props['recording_dir']
playback_dir = video_props['playback_dir']
process_dir = "#{recording_dir}/process/video/#{meeting_id}"
raw_archive_dir = "#{recording_dir}/raw/#{meeting_id}"
donefile = "#{recording_dir}/status/processed/#{meeting_id}-video.done"
log_file = "#{props['log_dir']}/video/process-#{meeting_id}.log"

logger = opts[:stderr] ? Logger.new($stderr) : Logger.new(log_file)
BigBlueButton.logger = logger

if File.exist?(donefile)
  logger.warn 'This processing script has already been run'
  exit 0
end

FileUtils.mkdir_p process_dir

logger.info 'Reading basic recording information'
events = Nokogiri::XML(File.open("#{raw_archive_dir}/events.xml"))
initial_timestamp = BigBlueButton::Events.first_event_timestamp(events)
final_timestamp = BigBlueButton::Events.last_event_timestamp(events)
duration = BigBlueButton::Events.get_recording_length(events)
participants = BigBlueButton::Events.get_num_participants(events)
metadata = events.at_xpath('/recording/metadata')

logger.info 'Generating video events list'

# Webcams
webcam_edl = BigBlueButton::Events.create_webcam_edl(events, raw_archive_dir, props['show_moderator_viewpoint'])
logger.debug 'Webcam EDL:'
BigBlueButton::EDL::Video.dump(webcam_edl)

# Deskshare
deskshare_edl = BigBlueButton::Events.create_deskshare_edl(events, raw_archive_dir)
logger.debug 'Deskshare EDL:'
BigBlueButton::EDL::Video.dump(deskshare_edl)

video_edl = BigBlueButton::EDL::Video.merge(webcam_edl, deskshare_edl)

logger.debug 'Merged Video EDL:'
BigBlueButton::EDL::Video.dump(video_edl)

logger.info 'Applying recording start/stop events to video'
video_edl = BigBlueButton::Events.edl_match_recording_marks_video(video_edl, events, initial_timestamp, final_timestamp)
logger.debug 'Trimmed Video EDL:'
BigBlueButton::EDL::Video.dump(video_edl)

logger.info 'Checking whether webcams were used'
have_webcams = BigBlueButton::Events.have_webcam_events(events)
if have_webcams
  logger.info('Webcams were use in this session')
else
  logger.info('No webcams were used in this session')
end

logger.info('Checking whether desktop sharing was used')
have_deskshare = BigBlueButton::Events.have_deskshare_events(events)
if have_deskshare
  logger.info('Desktop sharing was used in this session')
else
  logger.info('No desktop sharing was used in this session')
end

logger.info 'Checking whether the presentation area was used'
have_presentation = BigBlueButton::Events.have_presentation_events(events)
if have_presentation
  logger.info('Have presentation events, rendering presentation area')
else
  logger.info('No presentation events found')
end

if !have_presentation && !have_webcams
  logger.info('This recording has neither webcams or presentation')
  logger.info("Re-enabling presentation area, so the video isn't blank...")
  have_presentation = true
end

presentation_edl = nil
if have_presentation
  # The presentation video gets special treatment
  presentation_video = "#{process_dir}/presentation.mkv"
  presentation_edl = [
    {
      timestamp: 0,
      areas: { presentation: [{ filename: presentation_video, timestamp: 0 }] }
    },
    {
      timestamp: duration,
      areas: { presentation: [] }
    }
  ]
else
  presentation_edl = [
    {
      timestamp: 0,
      areas: { presentation: [] }
    }
  ]
end
logger.debug 'Presentation EDL:'
BigBlueButton::EDL::Video.dump(presentation_edl)

video_edl = BigBlueButton::EDL::Video.merge(presentation_edl, video_edl)
logger.debug 'Merged Video EDL with Presentation:'
BigBlueButton::EDL::Video.dump(video_edl)

logger.info 'Generating audio events list'
audio_edl = BigBlueButton::AudioEvents.create_audio_edl(events, raw_archive_dir)
logger.debug 'Audio EDL:'
BigBlueButton::EDL::Audio.dump(audio_edl)

logger.info 'Applying recording start/stop events to audio'
audio_edl = BigBlueButton::Events.edl_match_recording_marks_audio(audio_edl, events, initial_timestamp, final_timestamp)
logger.debug 'Trimmed Audio EDL:'
BigBlueButton::EDL::Audio.dump(audio_edl)

logger.info 'Rendering audio'
audio = BigBlueButton::EDL::Audio.render(audio_edl, "#{process_dir}/audio")

if BigBlueButton::Events.screenshare_has_audio?(events, "#{raw_archive_dir}/deskshare")
  logger.info('Generating audio events list for deskshare')
  deskshare_audio_edl = BigBlueButton::AudioEvents.create_deskshare_audio_edl(events, "#{raw_archive_dir}/deskshare")
  logger.debug('Deskshare audio EDL:')
  BigBlueButton::EDL::Audio.dump(deskshare_audio_edl)

  logger.info('Applying recording start/stop events to deskshare audio')
  deskshare_audio_edl = BigBlueButton::Events.edl_match_recording_marks_audio(
    deskshare_audio_edl, events, initial_timestamp, final_timestamp
  )
  logger.debug('Trimmed deskshare audio EDL:')
  BigBlueButton::EDL::Audio.dump(deskshare_audio_edl)

  logger.info('Rendering deskshare audio')
  deskshare_audio = BigBlueButton::EDL::Audio.render(deskshare_audio_edl, "#{process_dir}/deskshare_audio")

  logger.info('Mixing meeting audio and deskshare audio')
  audio = BigBlueButton::EDL::Audio.mixer([audio, deskshare_audio], "#{process_dir}/mixed_audio")
end

# Select the layout based on what video sections are available
layout = \
  if have_webcams
    if have_presentation || have_deskshare
      video_props['layout']
    else
      video_props['nopresentation_layout']
    end
  else
    video_props['nowebcam_layout']
  end

layout.symbolize_keys!
layout[:areas].each do |area|
  area.symbolize_keys!
  area[:name] = area[:name].to_sym
end

if have_presentation
  logger.info 'Creating presentation area video'
  presentation_area = layout[:areas].detect { |area| area[:name] == :presentation }

  BigBlueButton.execute(
    [
      'bbb-presentation-video',
      '-i', raw_archive_dir,
      '-w', presentation_area[:width].to_s, '-h', presentation_area[:height].to_s, '-r', layout[:framerate].to_s,
      '-o', presentation_video
    ],
    true
  )
end

logger.info 'Rendering video'
video = BigBlueButton::EDL::Video.render(video_edl, layout, "#{process_dir}/video")

logger.info "Encoding output files to #{video_props['formats'].length} formats"
video_props['formats'].each_with_index do |format, i|
  format.symbolize_keys!
  logger.info "  #{format[:mimetype]}"
  BigBlueButton::EDL.encode(audio, video, format, "#{process_dir}/video-#{i}", video_props['audio_offset'])
end

logger.info('Generating closed captions')
ret = BigBlueButton.exec_ret('utils/gen_webvtt', '-i', raw_archive_dir, '-o', process_dir)
raise 'Generating closed caption files failed' if ret != 0

captions = JSON.parse(File.read("#{process_dir}/captions.json"))

# Generate the support files for chat events
have_chat = false
begin
  logger.info 'Processing chat events'
  chats = BigBlueButton::Events.get_chat_events(events, initial_timestamp, final_timestamp, props)
  have_chat = true unless chats.empty?

  # Output the chat events to the popcorn events file
  popcorn_events = Nokogiri::XML::Builder.new do |xml|
    xml.popcorn do
      chats.each do |chat|
        chattimeline = {
          in: (chat[:in] / 1000.0).round(1),
          id: chat[:id],
          direction: 'down',
          name: chat[:sender],
          senderId: chat[:sender_id],
          chatEmphasizedText: chat[:chatEmphasizedText],
          senderRole: chat[:senderRole],
          message: chat[:message],
          replyToMessageId: chat[:replyToMessageId],
          lastEditedTimestamp: chat[:lastEditedTimestamp],
          target: 'chat',
        }
        chattimeline[:out] = (chat[:out] / 1000.0).round(1) unless chat[:out].nil?
        chattimeline[:reactions] = JSON.generate(chat[:reactions]) unless chat[:reactions].nil?
        xml.chattimeline(**chattimeline)
      end
    end
  end
  File.write("#{process_dir}/video.xml", popcorn_events.to_xml)
end

# Publishing support files

logger.info 'Generating index page'
index_template = "#{playback_dir}/index.html.erb"
index_erb = ERB.new(File.read(index_template))
index_erb.filename = index_template
File.write(
  "#{process_dir}/index.html",
  index_erb.result_with_hash(
    captions: captions,
    haveChat: have_chat,
    meetingName: metadata['meetingName'],
    video_props: video_props
  )
)

logger.info 'Generating metadata xml'
metadata_xml = Nokogiri::XML::Builder.new do |xml|
  xml.recording do
    xml.id(meeting_id)
    xml.state('available')
    xml.published('true')
    xml.start_time(start_real_time)
    xml.end_time(start_real_time + final_timestamp - initial_timestamp)
    xml.participants(participants)
    xml.playback do
      xml.format('video')
      xml.link("#{props['playback_protocol']}://#{props['playback_host']}/playback/video/#{meeting_id}/")
      xml.duration(duration)
    end
    xml.meta do
      metadata.attributes.each do |k, v|
        xml.method_missing(k, v)
      end
    end
  end
end
File.write("#{process_dir}/metadata.xml", metadata_xml.to_xml)

logger.info 'Copying css and js support files'
FileUtils.cp_r("#{playback_dir}/css", process_dir)
FileUtils.cp_r("#{playback_dir}/js", process_dir)
FileUtils.cp_r("#{playback_dir}/video-js", process_dir)

logger.info 'Processing successfully completed, writing done file'

File.write(donefile, "Processed #{meeting_id}")
