using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Windows;
using Microsoft.Win32;
using ytLive.Helpers;
using ytLive.Models;
using ytLive.Services;
using ytLive.Services.Encoder;
namespace ytLive.ViewModels;
public partial class MainViewModel : ViewModelBase
{
private readonly ChatOverlayLayer _chatLayer;
private readonly AlertOverlayLayer _alertLayer;
/// The live chat message buffer — the same collection the
/// owns; here so XAML can bind it and the
/// left panel can subscribe to its changes.
public ObservableCollection ChatMessages => _chatLayer.Messages;
/// The Add → YouTube Chat menu item: enabled only while no chat layer exists
/// anywhere — one chat overlay per layout.
public bool CanAddYouTubeChat
=> StagedScene != null
&& !Scenes.Any(s => s.Elements.OfType().Any(e => e.Type == SourceType.ChatBox));
/// The Add → Stream Alerts menu item: one alert zone per layout
/// (multiple zones would double-play every event).
public bool CanAddAlerts
=> StagedScene != null
&& !Scenes.Any(s => s.Elements.OfType().Any(e => e.Type == SourceType.AlertBox));
public bool ShowChatInactiveMessage => !IsLive;
private void OnChatMessageReceived(ChatMessage message)
{
_chatLayer.OnMessageReceived(message, Scenes);
_alertLayer.OnMessageReceived(message, Scenes);
}
private void UpdateChatBoxPreview(Source chatBox, IReadOnlyList? messages = null)
=> _chatLayer.UpdatePreview(chatBox, messages);
private VideoFrame? RenderChatBox(Source chatBox)
=> _chatLayer.RenderFrame(chatBox);
private void UpdateAlertBoxPreview(Source alertBox)
=> _alertLayer.UpdatePreview(alertBox);
private VideoFrame? RenderAlertBox(Source alertBox)
=> _alertLayer.RenderFrame(alertBox);
private void StartMockChatPreview()
=> _chatLayer.StartMockPreview(Scenes);
private void StopMockChatPreview()
=> _chatLayer.StopMockPreview();
private void UpdateActiveChatBoxPreviews()
=> _chatLayer.UpdateConfiguredBoxes(Scenes);
private void UpdateActiveAlertBoxPreviews()
=> _alertLayer.UpdateConfiguredBoxes(Scenes);
// ─── TASK 47: the alert clip (video) ───
private string? _alertDefaultVideoId;
private string? _defaultAlertClipScratch;
/// Runtime clip resolution for one alert box: the Creator's OWN video
/// is a plain path on disk (never stored — the 2026-09-26 decision); the shipped
/// llama placeholder lives as a BLOB in the internal Asset table and is
/// materialized to a temp file once. Null → the six celebratory animations.
/// Called from the output pump's alert frame path.
private string? ResolveAlertClipPath(Source box)
{
if (!box.AlertUseDefaultVideo
&& !string.IsNullOrWhiteSpace(box.AlertVideoPath)
&& File.Exists(box.AlertVideoPath))
return box.AlertVideoPath;
return MaterializeDefaultAlertClip();
}
private string? MaterializeDefaultAlertClip()
{
if (_defaultAlertClipScratch != null && File.Exists(_defaultAlertClipScratch))
return _defaultAlertClipScratch;
_alertDefaultVideoId ??= _layoutStore.GetAlertDefaultVideoId();
if (_alertDefaultVideoId == null) return null;
var bytes = _layoutStore.GetAssetBytes(_alertDefaultVideoId);
if (bytes == null) return null;
var scratch = Path.Combine(Path.GetTempPath(), $"ytLive-alert-{_alertDefaultVideoId}.mp4");
try
{
File.WriteAllBytes(scratch, bytes);
_defaultAlertClipScratch = scratch;
return scratch;
}
catch (Exception ex)
{
AppLog.Write(ex, "Alert default materialization failed");
return null;
}
}
/// One decoder per play (the refcounted MediaVideoSourceManager sessions
/// can't restart per replay). The clip is full-box in video + 48k stereo audio;
/// both steams pace themselves to real time inside the decoder — the frame-rate
/// probe is what makes the VIDEO pace (without it every frame dumps in ~1-2s and
/// the box freezes on the last frame for the rest of the audio).
private IAlertClipDecoder AlertClipDecoderFor(string path, int width, int height) =>
new AlertClipDecoder(
path, width, height,
new FfmpegLocator(),
() => new FfmpegDecodeProcess(),
frameRateProbe: new FfmpegFrameRateProbe(new FfmpegLocator(), () => new FfmpegDecodeProcess()));
/// Stamp the shipped llama placeholder into the Asset table at startup
/// (idempotent — hash-keyed UpsertAsset dedupes). Best-effort: if it fails the
/// alert box simply falls back to the six animations.
private void StampDefaultAlertVideo()
{
try
{
_alertDefaultVideoId = _layoutStore.GetAlertDefaultVideoId();
if (!string.IsNullOrWhiteSpace(_alertDefaultVideoId)) return;
var bytes = ReadAlertDefaultVideoResource();
if (bytes is null or { Length: 0 }) return;
_alertDefaultVideoId = _layoutStore.EnsureAlertDefaultVideo(bytes);
}
catch (Exception ex)
{
AppLog.Write(ex, "Alert default video stamp failed");
}
}
private static byte[]? ReadAlertDefaultVideoResource()
{
try
{
var uri = new Uri("pack://application:,,,/Assets/alert-default.mp4", UriKind.Absolute);
using var stream = Application.GetResourceStream(uri)?.Stream;
if (stream == null) return null;
using var ms = new MemoryStream();
stream.CopyTo(ms);
return ms.ToArray();
}
catch (Exception ex)
{
AppLog.Write(ex, "Alert default video resource missing");
return null;
}
}
private void BrowseAlertVideo(Source? box)
{
if (box == null || box.Type != SourceType.AlertBox) return;
var dialog = new OpenFileDialog
{
Title = "Choose the alert clip (mp4)",
Filter = "Video files (*.mp4;*.mov;*.webm;*.mkv)|*.mp4;*.mov;*.webm;*.mkv|All files (*.*)|*.*"
};
if (dialog.ShowDialog() != true) return;
box.AlertVideoPath = dialog.FileName;
box.AlertUseDefaultVideo = false;
}
private void ResetAlertVideo(Source? box)
{
if (box == null || box.Type != SourceType.AlertBox) return;
box.AlertVideoPath = null;
box.AlertShowTicker = true;
box.AlertUseDefaultVideo = true;
}
}