// <copyright file="MediaStreamRepository.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>

namespace Jellyfin.Server.Implementations.Item;

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using Microsoft.EntityFrameworkCore;

/// <summary>
/// Repository for obtaining MediaStreams.
/// </summary>
public class MediaStreamRepository : IMediaStreamRepository
{
    private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
    private readonly IServerApplicationHost _serverApplicationHost;
    private readonly ILocalizationManager _localization;

    /// <summary>
    /// Initializes a new instance of the <see cref="MediaStreamRepository"/> class.
    /// </summary>
    /// <param name="dbProvider">The EFCore db factory.</param>
    /// <param name="serverApplicationHost">The Application host.</param>
    /// <param name="localization">The Localisation Provider.</param>
    public MediaStreamRepository(IDbContextFactory<JellyfinDbContext> dbProvider, IServerApplicationHost serverApplicationHost, ILocalizationManager localization)
    {
        _dbProvider = dbProvider;
        _serverApplicationHost = serverApplicationHost;
        _localization = localization;
    }

    /// <inheritdoc />
    public async Task SaveMediaStreamsAsync(Guid id, IReadOnlyList<MediaStream> streams, CancellationToken cancellationToken = default)
    {
        await using var context = _dbProvider.CreateDbContext();

        // Use the execution strategy to handle transactional consistency and retries
        // This is required for PostgreSQL NpgsqlRetryingExecutionStrategy
        var executionStrategy = context.Database.CreateExecutionStrategy();
        await executionStrategy.ExecuteAsync(async () =>
        {
            await context.MediaStreamInfos
                .Where(e => e.ItemId.Equals(id))
                .ExecuteDeleteAsync(cancellationToken)
                .ConfigureAwait(false);

            await context.MediaStreamInfos
                .AddRangeAsync(streams.Select(f => Map(f, id)), cancellationToken)
                .ConfigureAwait(false);

            await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
        }).ConfigureAwait(false);
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<MediaStream>> GetMediaStreamsAsync(MediaStreamQuery filter, CancellationToken cancellationToken = default)
    {
        await using var context = _dbProvider.CreateDbContext();
        var streams = await TranslateQuery(context.MediaStreamInfos.AsNoTracking(), filter)
            .Include(e => e.AudioDetails)
            .Include(e => e.VideoDetails)
            .ToArrayAsync(cancellationToken)
            .ConfigureAwait(false);

        return streams.Select(Map).ToArray();
    }

    private string? GetPathToSave(string? path)
    {
        if (path is null)
        {
            return null;
        }

        return _serverApplicationHost.ReverseVirtualPath(path);
    }

    private string? RestorePath(string? path)
    {
        if (path is null)
        {
            return null;
        }

        return _serverApplicationHost.ExpandVirtualPath(path);
    }

    private IQueryable<MediaStreamInfo> TranslateQuery(IQueryable<MediaStreamInfo> query, MediaStreamQuery filter)
    {
        query = query.Where(e => e.ItemId.Equals(filter.ItemId));
        if (filter.Index.HasValue)
        {
            query = query.Where(e => e.StreamIndex == filter.Index);
        }

        if (filter.Type.HasValue)
        {
            var typeValue = (MediaStreamTypeEntity)filter.Type.Value;
            query = query.Where(e => e.StreamType == typeValue);
        }

        return query.OrderBy(e => e.StreamIndex);
    }

    private MediaStream Map(MediaStreamInfo entity)
    {
        var dto = new MediaStream();
        dto.Index = entity.StreamIndex;
        dto.Type = (MediaStreamType)entity.StreamType;

        dto.IsAVC = entity.IsAvc;
        dto.Codec = entity.Codec;

        var language = entity.Language;

        // Check if the language has multiple three letter ISO codes
        // if yes choose the first as that is the ISO 639-2/T code we're needing
        if (language != null && _localization.TryGetISO6392TFromB(language, out string? isoT))
        {
            language = isoT;
        }

        dto.Language = language;

        dto.Profile = entity.Profile;
        dto.Path = RestorePath(entity.Path);
        dto.BitRate = entity.BitRate;
        dto.IsDefault = entity.IsDefault;
        dto.IsForced = entity.IsForced;
        dto.IsExternal = entity.IsExternal;
        dto.Level = entity.Level;
        dto.CodecTag = entity.CodecTag;
        dto.Comment = entity.Comment;
        dto.Title = entity.Title;
        dto.TimeBase = entity.TimeBase;
        dto.CodecTimeBase = entity.CodecTimeBase;

        if (entity.AudioDetails is { } audio)
        {
            dto.ChannelLayout = audio.ChannelLayout;
            dto.Channels = audio.Channels;
            dto.SampleRate = audio.SampleRate;
            dto.IsHearingImpaired = audio.IsHearingImpaired.GetValueOrDefault();
        }

        if (entity.VideoDetails is { } video)
        {
            dto.AspectRatio = video.AspectRatio;
            dto.IsInterlaced = video.IsInterlaced.GetValueOrDefault();
            dto.Height = video.Height;
            dto.Width = video.Width;
            dto.AverageFrameRate = video.AverageFrameRate;
            dto.RealFrameRate = video.RealFrameRate;
            dto.PixelFormat = video.PixelFormat;
            dto.BitDepth = video.BitDepth;
            dto.IsAnamorphic = video.IsAnamorphic;
            dto.RefFrames = video.RefFrames;
            dto.NalLengthSize = video.NalLengthSize;
            dto.ColorPrimaries = video.ColorPrimaries;
            dto.ColorSpace = video.ColorSpace;
            dto.ColorTransfer = video.ColorTransfer;
            dto.DvVersionMajor = video.DvVersionMajor;
            dto.DvVersionMinor = video.DvVersionMinor;
            dto.DvProfile = video.DvProfile;
            dto.DvLevel = video.DvLevel;
            dto.RpuPresentFlag = video.RpuPresentFlag;
            dto.ElPresentFlag = video.ElPresentFlag;
            dto.BlPresentFlag = video.BlPresentFlag;
            dto.DvBlSignalCompatibilityId = video.DvBlSignalCompatibilityId;
            dto.Rotation = video.Rotation;
            dto.Hdr10PlusPresentFlag = video.Hdr10PlusPresentFlag;
        }

        if (dto.Type is MediaStreamType.Audio or MediaStreamType.Subtitle)
        {
            dto.LocalizedDefault = _localization.GetLocalizedString("Default");
            dto.LocalizedExternal = _localization.GetLocalizedString("External");

            if (!string.IsNullOrEmpty(dto.Language))
            {
                var culture = _localization.FindLanguageInfo(dto.Language);
                dto.LocalizedLanguage = culture?.DisplayName;
            }

            if (dto.Type is MediaStreamType.Subtitle)
            {
                dto.LocalizedUndefined = _localization.GetLocalizedString("Undefined");
                dto.LocalizedForced = _localization.GetLocalizedString("Forced");
                dto.LocalizedHearingImpaired = _localization.GetLocalizedString("HearingImpaired");
            }
        }

        return dto;
    }

    private MediaStreamInfo Map(MediaStream dto, Guid itemId)
    {
        var entity = new MediaStreamInfo
        {
            Item = null!,
            ItemId = itemId,
            StreamIndex = dto.Index,
            StreamType = (MediaStreamTypeEntity)dto.Type,
            IsAvc = dto.IsAVC,

            Codec = dto.Codec,
            Language = dto.Language,
            Profile = dto.Profile,
            Path = GetPathToSave(dto.Path) ?? dto.Path,
            BitRate = dto.BitRate,
            IsDefault = dto.IsDefault,
            IsForced = dto.IsForced,
            IsExternal = dto.IsExternal,
            Level = dto.Level.HasValue ? (float)dto.Level : null,
            CodecTag = dto.CodecTag,
            Comment = dto.Comment,
            Title = dto.Title,
            TimeBase = dto.TimeBase,
            CodecTimeBase = dto.CodecTimeBase,
        };

        if (dto.Type == MediaStreamType.Audio)
        {
            entity.AudioDetails = new AudioStreamDetails
            {
                ItemId = itemId,
                StreamIndex = dto.Index,
                ChannelLayout = dto.ChannelLayout,
                Channels = dto.Channels,
                SampleRate = dto.SampleRate,
                IsHearingImpaired = dto.IsHearingImpaired,
            };
        }
        else if (dto.Type == MediaStreamType.Video)
        {
            entity.VideoDetails = new VideoStreamDetails
            {
                ItemId = itemId,
                StreamIndex = dto.Index,
                AspectRatio = dto.AspectRatio,
                IsInterlaced = dto.IsInterlaced,
                Height = dto.Height,
                Width = dto.Width,
                AverageFrameRate = dto.AverageFrameRate,
                RealFrameRate = dto.RealFrameRate,
                PixelFormat = dto.PixelFormat,
                BitDepth = dto.BitDepth,
                IsAnamorphic = dto.IsAnamorphic,
                RefFrames = dto.RefFrames,
                NalLengthSize = dto.NalLengthSize,
                ColorPrimaries = dto.ColorPrimaries,
                ColorSpace = dto.ColorSpace,
                ColorTransfer = dto.ColorTransfer,
                DvVersionMajor = dto.DvVersionMajor,
                DvVersionMinor = dto.DvVersionMinor,
                DvProfile = dto.DvProfile,
                DvLevel = dto.DvLevel,
                RpuPresentFlag = dto.RpuPresentFlag,
                ElPresentFlag = dto.ElPresentFlag,
                BlPresentFlag = dto.BlPresentFlag,
                DvBlSignalCompatibilityId = dto.DvBlSignalCompatibilityId,
                Rotation = dto.Rotation,
                Hdr10PlusPresentFlag = dto.Hdr10PlusPresentFlag,
            };
        }

        return entity;
    }
}
