using SameBeat.Domain.Enumy;
using SameBeat.Domain.Wyjatki;
namespace SameBeat.Domain.Encje;
///
/// Wydarzenie -- pojedyncza "impreza" (np. cykliczna, powtarzalna nazwa), w ramach ktorej moga
/// odbywac sie kolejne sesje na zywo ().
///
public class Wydarzenie
{
public Guid Id { get; private set; }
public string Nazwa { get; private set; } = string.Empty;
public StatusWydarzenia Status { get; private set; }
public DateTime UtworzoneUtc { get; private set; }
/// Uklad sceny glownej (ETAP 3) -- okresla dostepne sloty, patrz .
public UkladSceny UkladSceny { get; private set; }
// ---- Muzyka (ETAP 4) --------------------------------------------------------------------
// Muzyka NIE idzie przez LiveKit/SignalR jako dzwiek -- kazda przegladarka (host i kazdy widz)
// samodzielnie odtwarza TEN SAM plik audio spod Utwor.Url, a ponizsze pola to tylko "zegar"
// (kiedy sie zaczelo, od ktorego miejsca), do ktorego klienci synchronizuja swoja pozycje
// odtwarzania. To jak radio internetowe -- backend nigdy nie przesyla samego dzwieku.
/// Aktualnie wybrany utwor (biblioteka ). Null = nic nie jest ustawione (StanOdtwarzania=Zatrzymana).
public Guid? UtworId { get; private set; }
/// Chwila UTC, w ktorej "zegar" ruszyl od PozycjaStartowaMs. Null, gdy nie gra teraz (Zatrzymana/Wstrzymana).
public DateTime? CzasStartuUtc { get; private set; }
/// Pozycja w utworze (ms), od ktorej liczy sie zegar od CzasStartuUtc -- albo pozycja, na ktorej zatrzymano sie przy pauzie.
public int PozycjaStartowaMs { get; private set; }
public StanOdtwarzania StanOdtwarzania { get; private set; }
/// Glosnosc master (0.0-1.0) -- mnoznik, ktory kazdy klient stosuje do wlasnego odtwarzacza.
public float GlosnoscMaster { get; private set; }
// ---- Mikrofon (ETAP 5) --------------------------------------------------------------------
// ZYWY stan (do wyswietlenia "komu teraz wolno mowic" i do autorytatywnego liczenia czasu przez
// serwer) -- w odroznieniu od HistoriaMikrofonu, ktora jest logiem/audytem, nie zywym stanem
// (zgodnie z zasada "baza przechowuje stan biznesowy, nie kazdy pakiet"). Wzorzec identyczny
// jak przy muzyce: pola tutaj + metody domenowe, serwis aplikacji odpowiada za koordynacje
// z innymi encjami (uczestnik musi byc NaScenie) i za twarde wymuszenie przez LiveKit.
/// Uczestnik, ktory ma teraz przyznany mikrofon. Null = nikt.
public Guid? AktywnyMikrofonUczestnikId { get; private set; }
///
/// Chwila UTC, w ktorej AKTUALNIE przyznany mikrofon wygasa -- to jest AUTORYTATYWNE zrodlo
/// prawdy o czasie (serwer, nie przegladarka). Klient dostaje ta wartosc wylacznie do lokalnego
/// odliczania na ekranie; fizyczne odebranie prawa do publikacji audio robi backend, patrz
/// SerwisAdministracjiLiveKit.
///
public DateTime? AktywnyMikrofonKoniecUtc { get; private set; }
private Wydarzenie()
{
}
private Wydarzenie(Guid id, string nazwa, DateTime utworzoneUtc)
{
Id = id;
Nazwa = nazwa;
Status = StatusWydarzenia.Offline;
UtworzoneUtc = utworzoneUtc;
UkladSceny = UkladSceny.Grid4;
UtworId = null;
CzasStartuUtc = null;
PozycjaStartowaMs = 0;
StanOdtwarzania = StanOdtwarzania.Zatrzymana;
GlosnoscMaster = 0.9f;
AktywnyMikrofonUczestnikId = null;
AktywnyMikrofonKoniecUtc = null;
}
public static Wydarzenie Utworz(string nazwa)
{
if (string.IsNullOrWhiteSpace(nazwa))
{
throw new NieprawidlowyStanEncjiWyjatek("Nazwa wydarzenia nie moze byc pusta.");
}
return new Wydarzenie(Guid.NewGuid(), nazwa.Trim(), DateTime.UtcNow);
}
public void PrzejdzNaLive() => Status = StatusWydarzenia.Live;
public void PrzejdzNaOffline() => Status = StatusWydarzenia.Offline;
///
/// Zmienia uklad sceny. Korekta "sierocych" przypisan (sloty o numerze wiekszym niz dopuszcza
/// nowy uklad) NIE jest robiona tutaj -- to logika miedzy-encyjna (Wydarzenie + SlotSceny),
/// wykonuje ja serwis aplikacji, ktory ma dostep do obu.
///
public void ZmienUklad(UkladSceny nowyUklad) => UkladSceny = nowyUklad;
/// Przelacza "zegar" na dany utwor od podanej pozycji -- ruszamy teraz (CzasStartuUtc=DateTime.UtcNow), stan Odtwarzana.
public void OdtworzOd(Guid utworId, int pozycjaMs)
{
if (utworId == Guid.Empty)
{
throw new NieprawidlowyStanEncjiWyjatek("Nie mozna odtworzyc pustego identyfikatora utworu.");
}
if (pozycjaMs < 0)
{
throw new NieprawidlowyStanEncjiWyjatek("Pozycja startowa utworu nie moze byc ujemna.");
}
UtworId = utworId;
PozycjaStartowaMs = pozycjaMs;
CzasStartuUtc = DateTime.UtcNow;
StanOdtwarzania = StanOdtwarzania.Odtwarzana;
}
///
/// Wstrzymuje zegar -- przelicza aktualna pozycje (PozycjaStartowaMs + uplyw czasu od
/// CzasStartuUtc) i zapisuje ja jako nowa PozycjaStartowaMs, zeby Wznow() mogl kontynuowac
/// dokladnie stamtad. Idempotentne: wywolanie, gdy juz nie gra, nic nie robi (host mogl
/// kliknac pauze dwa razy albo klient mial nieaktualny stan).
///
public void Wstrzymaj()
{
if (StanOdtwarzania != StanOdtwarzania.Odtwarzana)
{
return;
}
if (CzasStartuUtc is DateTime czasStartu)
{
var uplynelMs = (int)Math.Max(0, (DateTime.UtcNow - czasStartu).TotalMilliseconds);
PozycjaStartowaMs += uplynelMs;
}
CzasStartuUtc = null;
StanOdtwarzania = StanOdtwarzania.Wstrzymana;
}
/// Wznawia od zapisanej PozycjaStartowaMs -- zegar rusza teraz (CzasStartuUtc=DateTime.UtcNow).
public void Wznow()
{
CzasStartuUtc = DateTime.UtcNow;
StanOdtwarzania = StanOdtwarzania.Odtwarzana;
}
/// Zatrzymuje odtwarzanie calkowicie -- brak wybranego utworu, zegar wyzerowany (koniec playlisty).
public void Zatrzymaj()
{
UtworId = null;
CzasStartuUtc = null;
PozycjaStartowaMs = 0;
StanOdtwarzania = StanOdtwarzania.Zatrzymana;
}
/// Ustawia glosnosc master. Zakres 0.0-1.0 -- poza nim rzuca (druga linia obrony, SerwisMuzyki waliduje wczesniej przez WynikOperacji).
public void UstawGlosnosc(float wartosc)
{
if (wartosc < 0f || wartosc > 1f)
{
throw new NieprawidlowyStanEncjiWyjatek("Glosnosc musi byc w zakresie 0.0-1.0.");
}
GlosnoscMaster = wartosc;
}
///
/// Przyznaje mikrofon uczestnikowi na dokladnie okreslony czas -- koniec liczony TERAZ
/// (DateTime.UtcNow + czasSekundy), zeby zaden posredni krok (kolejka requestu, zapis do bazy)
/// nie "kradl" uczestnikowi czasu. Czy uczestnik faktycznie jest NaScenie sprawdza serwis
/// aplikacji (Wydarzenie nie zna statusu UczestnikOczekujacy) -- tutaj pilnujemy tylko wlasnego,
/// prostszego niezmiennika: co najwyzej jeden aktywny mikrofon na raz (host musi najpierw
/// przerwac poprzedni, patrz SerwisMikrofonu -- swiadome uproszczenie zamiast automatycznego
/// przerywania).
///
public void PrzyznajMikrofon(Guid uczestnikId, int czasSekundy)
{
if (uczestnikId == Guid.Empty)
{
throw new NieprawidlowyStanEncjiWyjatek("Nie mozna przyznac mikrofonu pustemu identyfikatorowi uczestnika.");
}
if (czasSekundy <= 0)
{
throw new NieprawidlowyStanEncjiWyjatek("Czas przyznania mikrofonu musi byc dodatni.");
}
if (AktywnyMikrofonUczestnikId is not null)
{
throw new NieprawidlowyStanEncjiWyjatek("Mikrofon jest juz przyznany innemu uczestnikowi -- najpierw go przerwij.");
}
AktywnyMikrofonUczestnikId = uczestnikId;
AktywnyMikrofonKoniecUtc = DateTime.UtcNow.AddSeconds(czasSekundy);
}
///
/// Odbiera mikrofon -- badz to dlatego, ze host przerwal recznie, badz ze serwerowy timer
/// wykryl uplyw AktywnyMikrofonKoniecUtc. Idempotentne: wywolanie, gdy nikt nie ma teraz
/// mikrofonu, nic nie robi (host mogl kliknac "przerwij" tuz po tym, jak timer juz go odebral).
///
public void OdbierzMikrofon()
{
AktywnyMikrofonUczestnikId = null;
AktywnyMikrofonKoniecUtc = null;
}
}