-- TODO add assertion for face string and enum
local function _assert(data)
	if data.SyncToAudio then
		assert(
			data.SyncToAudio.Audio ~= nil,
			"Failed to create config | expected an Audio for SyncToAudio, but received nil"
		)
		assert(
			data.SyncToAudio.StartAtAudioTime ~= nil,
			"Failed to create config | expected a StartAtAudioTime for SyncToAudio, but received nil"
		)
	else
		assert(
			data.StartTime ~= nil,
			"Failed to create config | expected config.StartTime or config.SyncToAudio, but received nil"
		)
	end

	assert(data.Adornee ~= nil, "Invalid Adornee | part expected got nil")
	assert(type(data.VideoId) == "string", ("Invalid VideoId | string expected got " .. type(data.VideoId)))
	if data.OnStart then
		assert(type(data.OnStart) == "function", ("Invalid OnStart | function expected got " .. type(data.OnStart)))
	end
	if data.OnEnd then
		assert(type(data.OnEnd) == "function", ("Invalid OnEnd | function expected got " .. type(data.OnEnd)))
	end

	if data.Face then
		assert(typeof(data.Face) == "EnumItem", ("Invalid Face Enum | enum expected got " .. typeof(data.Face)))
	end

	if data.Captions then
		local CaptionString = data.Captions.CaptionString
		assert(
			CaptionString ~= nil and type(CaptionString) == "string",
			("Invalid CaptionString | string expected got " .. type(CaptionString))
		)
	end
end

local Video = {}
Video.__index = Video
Video.Type = "Video"

Video.task = task

function Video:_constructVideo(videoId)
	self.SurfaceGui = Instance.new("SurfaceGui")
	-- TODO document this is a client only config
	local playerGui = Players.LocalPlayer:WaitForChild("PlayerGui")
	self.SurfaceGui.Adornee = self.Adornee
	self.SurfaceGui.Face = self.Face
	self.SurfaceGui.Name = self.Adornee.Name
	self.SurfaceGui.Parent = playerGui

	if self.SurfaceGuiProperties then
		for prop, value in pairs(self.SurfaceGuiProperties) do
			self.SurfaceGui[prop] = value
		end
	end

	self.VideoFrame = Instance.new("VideoFrame")
	self.VideoFrame.Video = videoId
	self.VideoFrame.Size = UDim2.new(1, 0, 1, 0)
	self.VideoFrame.Parent = self.SurfaceGui

	if self.Captions then
		self.VideoFrame:SetAttribute(enums.Attribute.Captions, self.Captions.CaptionString or "")
		self.CaptionsModule = CaptionsModule.new(self.VideoFrame)

		if self.CCButton then
			self.CCButton = self.CCButton:Clone()
		else
			self.CCButton = script.CCButton:Clone()
		end
		self.CCButton.Parent = self.SurfaceGui

		local captionLabel = self.CaptionsModule.CaptionLabel
		self.captionButtonConnection = self.CCButton.Activated:Connect(function()
			local areCaptionsVisible = captionLabel.Visible
			if areCaptionsVisible then
				self.CCButton.Image = ccOffIcon
			else
				self.CCButton.Image = ccIcon
			end
			captionLabel.Visible = not areCaptionsVisible
		end)
	end

	return self.VideoFrame
end

function Video:_calculateCurrentSoundIntensityRatio(video)
	local loudestPlayback = self.LoudestPlayback
	local loudness = video.volume
	self.LoudestPlayback = math.max(loudness, loudestPlayback)
	return loudness / self.LoudestPlayback
end

function Video.new(data)
	_assert(data)

	local self = setmetatable({}, Video)

	self.OnStart = data.OnStart or function(_config) end
	self.OnEnd = data.OnEnd or function(_config) end
	self.CurrentSoundIntensityRatio = 1
	self.LoudestPlayback = 1
	self.IsCancelled = false
	self.SyncToAudio = data.SyncToAudio
	self.VideoId = data.VideoId
	self.Adornee = data.Adornee
	self.Face = data.Face or Enum.NormalId.Top
	self.TimeLength = data.TimeLength
	self.SurfaceGuiProperties = data.SurfaceGuiProperties
	self.Captions = data.Captions
	self.CaptionsModule = data.CaptionsModule
	self.StartTime = data.StartTime

	if self.SyncToAudio then
		self.StartTime = self.SyncToAudio.Audio.StartTime + self.SyncToAudio.StartAtAudioTime
	end

	return self
end

function Video:_attemptOnStart()
	if self.OnStart ~= nil then
		SafeSpawn.new(function()
			self:OnStart()
		end)
	end
end

function Video:_attemptOnEnd()
	if self.OnEnd ~= nil then
		SafeSpawn.new(function()
			self:OnEnd()
		end)
	end
end

function Video:getSchemaProcessorTimePosition(schema)
	return schema.timePosition.Value
end

function Video:getCurrentSoundTimePosition()
	return self.SyncToAudio.Audio.Sound.TimePosition
end

function Video:Process(schemaProcessor, startTime)
	if self.SyncToAudio then
		if self.SyncToAudio.Audio ~= nil and self.SyncToAudio.Audio.IsDestroyed then
			self:_attemptOnEnd()
			return
		end
	end

	local currentTime = self:getSchemaProcessorTimePosition(schemaProcessor)
	local timeVideoFinishes = (startTime + self.TimeLength)
	if currentTime >= timeVideoFinishes then
		self:_attemptOnEnd()
		return
	end

	schemaProcessor.Maid:add(self, "SeekFlag")
	self.Video = self:_constructVideo(self.VideoId)

	if not self.Video.IsLoaded then
		print("Loading video")
		repeat
			self.task.wait()
		until self.IsCancelled or self.Video.IsLoaded
	end

	if not self.IsCancelled then
		print("Video loaded")
	end

	if self.SyncToAudio then
		while
			(self.SyncToAudio.Audio and not self.SyncToAudio.Audio.IsDestroyed and not self.IsCancelled)
			and (
				self.SyncToAudio.Audio
				and (
					self.SyncToAudio.Audio.Sound == nil
					or (self.SyncToAudio.Audio.Sound and self:getCurrentSoundTimePosition() < startTime)
				)
			)
		do
			self.task.wait()
		end
	end

	local timePositionOffset = schemaProcessor.timePosition.Value - startTime
	self.Video.TimePosition = timePositionOffset

	local videoOnEndConnection = self.Video.Ended:Connect(function()
		self:_attemptOnEnd()
	end)

	if self.captionButtonConnection then
		schemaProcessor.Maid:add(self.captionButtonConnection)
	end

	self.Video:Play()

	if self.Captions then
		SafeSpawn.new(function()
			self.CaptionsModule:Run()
			schemaProcessor.Maid:add(self.CaptionsModule, "Clean")
		end)
	end

	local intensityRatioCalculatorConnection = RunService.Heartbeat:Connect(function()
		self.CurrentSoundIntensityRatio = self:_calculateCurrentSoundIntensityRatio(self.Video)
	end)

	schemaProcessor.Maid:add(videoOnEndConnection)
	schemaProcessor.Maid:add(intensityRatioCalculatorConnection)
	schemaProcessor.Maid:add(self.Video)
	schemaProcessor.Maid:add(self.CCButton)
	schemaProcessor.Maid:add(self.SurfaceGui)

	if not self.IsCancelled then
		self:_attemptOnStart()
	end
end

function Video:SeekFlag()
	self.IsCancelled = true
end

return Video