using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;

namespace Emby.Server.Implementations.ScheduledTasks.Tasks;

/// <summary>
/// The audio normalization task.
/// </summary>
public partial class AudioNormalizationTask : IScheduledTask
{
    private readonly IItemRepository _itemRepository;
    private readonly ILibraryManager _libraryManager;
    private readonly IMediaEncoder _mediaEncoder;
    private readonly IApplicationPaths _applicationPaths;
    private readonly ILocalizationManager _localization;
    private readonly ILogger<AudioNormalizationTask> _logger;

    /// <summary>
    /// Initializes a new instance of the <see cref="AudioNormalizationTask"/> class.
    /// </summary>
    /// <param name="itemRepository">Instance of the <see cref="IItemRepository"/> interface.</param>
    /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
    /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
    /// <param name="applicationPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
    /// <param name="localizationManager">Instance of the <see cref="ILocalizationManager"/> interface.</param>
    /// <param name="logger">Instance of the <see cref="ILogger{AudioNormalizationTask}"/> interface.</param>
    public AudioNormalizationTask(
        IItemRepository itemRepository,
        ILibraryManager libraryManager,
        IMediaEncoder mediaEncoder,
        IApplicationPaths applicationPaths,
        ILocalizationManager localizationManager,
        ILogger<AudioNormalizationTask> logger)
    {
        _itemRepository = itemRepository;
        _libraryManager = libraryManager;
        _mediaEncoder = mediaEncoder;
        _applicationPaths = applicationPaths;
        _localization = localizationManager;
        _logger = logger;
    }

    /// <inheritdoc />
    public string Name => _localization.GetLocalizedString("TaskAudioNormalization");

    /// <inheritdoc />
    public string Description => _localization.GetLocalizedString("TaskAudioNormalizationDescription");

    /// <inheritdoc />
    public string Category => _localization.GetLocalizedString("TasksLibraryCategory");

    /// <inheritdoc />
    public string Key => "AudioNormalization";

    [GeneratedRegex(@"^\s+I:\s+(.*?)\s+LUFS")]
    private static partial Regex LUFSRegex();

    /// <inheritdoc />
    public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
    {
        var numComplete = 0;
        var libraries = _libraryManager.RootFolder.Children.Where(library => _libraryManager.GetLibraryOptions(library).EnableLUFSScan).ToArray();
        double percent = 0;

        foreach (var library in libraries)
        {
            var albums = _libraryManager.GetItemList(new InternalItemsQuery { IncludeItemTypes = [BaseItemKind.MusicAlbum], Parent = library, Recursive = true });

            double nextPercent = numComplete + 1;
            nextPercent /= libraries.Length;
            nextPercent -= percent;
            // Split the progress for this single library into two halves: album gain and track gain.
            // The first half will be for album gain, the second half for track gain.
            nextPercent /= 2;
            var albumComplete = 0;

            foreach (var a in albums)
            {
                if (!a.NormalizationGain.HasValue && !a.LUFS.HasValue)
                {
                    // Album gain
                    var albumTracks = ((MusicAlbum)a).Tracks.Where(x => x.IsFileProtocol).ToList();

                    // Skip albums that don't have multiple tracks, album gain is useless here
                    if (albumTracks.Count > 1)
                    {
                        _logger.LogInformation("Calculating LUFS for album: {Album} with id: {Id}", a.Name, a.Id);
                        var tempDir = _applicationPaths.TempDirectory;
                        Directory.CreateDirectory(tempDir);
                        var tempFile = Path.Join(tempDir, a.Id + ".concat");
                        var inputLines = albumTracks.Select(x => string.Format(CultureInfo.InvariantCulture, "file '{0}'", x.Path.Replace("'", @"'\''", StringComparison.Ordinal)));
                        await File.WriteAllLinesAsync(tempFile, inputLines, cancellationToken).ConfigureAwait(false);
                        try
                        {
                            a.LUFS = await CalculateLUFSAsync(
                                string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile),
                                OperatingSystem.IsWindows(), // Wait for process to exit on Windows before we try deleting the concat file
                                cancellationToken).ConfigureAwait(false);
                        }
                        finally
                        {
                            File.Delete(tempFile);
                        }
                    }
                }

                // Update sub-progress for album gain
                albumComplete++;
                double albumPercent = albumComplete;
                albumPercent /= albums.Count;

                progress.Report(100 * (percent + (albumPercent * nextPercent)));
            }

            // Update progress to start at the track gain percent calculation
            percent += nextPercent;

            _itemRepository.SaveItems(albums, cancellationToken);

            // Track gain
            var tracks = _libraryManager.GetItemList(new InternalItemsQuery { MediaTypes = [MediaType.Audio], IncludeItemTypes = [BaseItemKind.Audio], Parent = library, Recursive = true });

            var tracksComplete = 0;
            foreach (var t in tracks)
            {
                if (!t.NormalizationGain.HasValue && !t.LUFS.HasValue && t.IsFileProtocol)
                {
                    t.LUFS = await CalculateLUFSAsync(
                        string.Format(CultureInfo.InvariantCulture, "-i \"{0}\"", t.Path.Replace("\"", "\\\"", StringComparison.Ordinal)),
                        false,
                        cancellationToken).ConfigureAwait(false);
                }

                // Update sub-progress for track gain
                tracksComplete++;
                double trackPercent = tracksComplete;
                trackPercent /= tracks.Count;

                progress.Report(100 * (percent + (trackPercent * nextPercent)));
            }

            _itemRepository.SaveItems(tracks, cancellationToken);

            // Update progress
            numComplete++;
            percent = numComplete;
            percent /= libraries.Length;

            progress.Report(100 * percent);
        }

        progress.Report(100.0);
    }

    /// <inheritdoc />
    public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
    {
        yield return new TaskTriggerInfo
        {
            Type = TaskTriggerInfoType.IntervalTrigger,
            IntervalTicks = TimeSpan.FromHours(24).Ticks
        };
    }

    private async Task<float?> CalculateLUFSAsync(string inputArgs, bool waitForExit, CancellationToken cancellationToken)
    {
        var args = $"-hide_banner {inputArgs} -af ebur128=framelog=verbose -f null -";

        using (var process = new Process()
        {
            StartInfo = new ProcessStartInfo
            {
                FileName = _mediaEncoder.EncoderPath,
                Arguments = args,
                RedirectStandardOutput = false,
                RedirectStandardError = true
            },
        })
        {
            try
            {
                _logger.LogDebug("Starting ffmpeg with arguments: {Arguments}", args);
                process.Start();
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error starting ffmpeg with arguments: {Arguments}", args);
                return null;
            }

            using var reader = process.StandardError;
            float? lufs = null;
            await foreach (var line in reader.ReadAllLinesAsync(cancellationToken).ConfigureAwait(false))
            {
                Match match = LUFSRegex().Match(line);
                if (match.Success)
                {
                    lufs = float.Parse(match.Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat);
                    break;
                }
            }

            if (lufs is null)
            {
                _logger.LogError("Failed to find LUFS value in output");
            }

            if (waitForExit)
            {
                await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
            }

            return lufs;
        }
    }
}
