using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Emby.Naming.Common;
using Emby.Naming.ExternalFiles;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging;

namespace MediaBrowser.Providers.MediaInfo
{
    /// <summary>
    /// Resolves external files for <see cref="Video"/>.
    /// </summary>
    public abstract class MediaInfoResolver
    {
        /// <summary>
        /// The <see cref="ExternalPathParser"/> instance.
        /// </summary>
        private readonly ExternalPathParser _externalPathParser;

        /// <summary>
        /// The <see cref="IMediaEncoder"/> instance.
        /// </summary>
        private readonly IMediaEncoder _mediaEncoder;

        private readonly ILogger _logger;
        private readonly IFileSystem _fileSystem;

        /// <summary>
        /// The <see cref="NamingOptions"/> instance.
        /// </summary>
        private readonly NamingOptions _namingOptions;

        /// <summary>
        /// The <see cref="DlnaProfileType"/> of the files this resolver should resolve.
        /// </summary>
        private readonly DlnaProfileType _type;

        /// <summary>
        /// Initializes a new instance of the <see cref="MediaInfoResolver"/> class.
        /// </summary>
        /// <param name="logger">The logger.</param>
        /// <param name="localizationManager">The localization manager.</param>
        /// <param name="mediaEncoder">The media encoder.</param>
        /// <param name="fileSystem">The file system.</param>
        /// <param name="namingOptions">The <see cref="NamingOptions"/> object containing FileExtensions, MediaDefaultFlags, MediaForcedFlags and MediaFlagDelimiters.</param>
        /// <param name="type">The <see cref="DlnaProfileType"/> of the parsed file.</param>
        protected MediaInfoResolver(
            ILogger logger,
            ILocalizationManager localizationManager,
            IMediaEncoder mediaEncoder,
            IFileSystem fileSystem,
            NamingOptions namingOptions,
            DlnaProfileType type)
        {
            _logger = logger;
            _mediaEncoder = mediaEncoder;
            _fileSystem = fileSystem;
            _namingOptions = namingOptions;
            _type = type;
            _externalPathParser = new ExternalPathParser(namingOptions, localizationManager, _type);
        }

        /// <summary>
        /// Retrieves the external streams for the provided video.
        /// </summary>
        /// <param name="video">The <see cref="Video"/> object to search external streams for.</param>
        /// <param name="startIndex">The stream index to start adding external streams at.</param>
        /// <param name="directoryService">The directory service to search for files.</param>
        /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param>
        /// <param name="cancellationToken">The cancellation token.</param>
        /// <returns>The external streams located.</returns>
        public async Task<IReadOnlyList<MediaStream>> GetExternalStreamsAsync(
            Video video,
            int startIndex,
            IDirectoryService directoryService,
            bool clearCache,
            CancellationToken cancellationToken)
        {
            if (!video.IsFileProtocol)
            {
                return Array.Empty<MediaStream>();
            }

            var pathInfos = GetExternalFiles(video, directoryService, clearCache);

            if (!pathInfos.Any())
            {
                return Array.Empty<MediaStream>();
            }

            var mediaStreams = new List<MediaStream>();

            foreach (var pathInfo in pathInfos)
            {
                if (!pathInfo.Path.AsSpan().EndsWith(".strm", StringComparison.OrdinalIgnoreCase))
                {
                    try
                    {
                        var mediaInfo = await GetMediaInfo(pathInfo.Path, _type, cancellationToken).ConfigureAwait(false);

                        if (mediaInfo.MediaStreams.Count == 1)
                        {
                            MediaStream mediaStream = mediaInfo.MediaStreams[0];

                            if ((mediaStream.Type == MediaStreamType.Audio && _type == DlnaProfileType.Audio)
                                || (mediaStream.Type == MediaStreamType.Subtitle && _type == DlnaProfileType.Subtitle))
                            {
                                mediaStream.Index = startIndex++;
                                mediaStream.IsDefault = pathInfo.IsDefault || mediaStream.IsDefault;
                                mediaStream.IsForced = pathInfo.IsForced || mediaStream.IsForced;
                                mediaStream.IsHearingImpaired = pathInfo.IsHearingImpaired || mediaStream.IsHearingImpaired;

                                mediaStreams.Add(MergeMetadata(mediaStream, pathInfo));
                            }
                        }
                        else
                        {
                            foreach (MediaStream mediaStream in mediaInfo.MediaStreams)
                            {
                                if ((mediaStream.Type == MediaStreamType.Audio && _type == DlnaProfileType.Audio)
                                    || (mediaStream.Type == MediaStreamType.Subtitle && _type == DlnaProfileType.Subtitle))
                                {
                                    mediaStream.Index = startIndex++;

                                    mediaStreams.Add(MergeMetadata(mediaStream, pathInfo));
                                }
                            }
                        }
                    }
                    catch (Exception ex)
                    {
                        _logger.LogError(ex, "Error getting external streams from {Path}", pathInfo.Path);

                        continue;
                    }
                }
            }

            return mediaStreams;
        }

        /// <summary>
        /// Retrieves the external streams for the provided audio.
        /// </summary>
        /// <param name="audio">The <see cref="Audio"/> object to search external streams for.</param>
        /// <param name="startIndex">The stream index to start adding external streams at.</param>
        /// <param name="directoryService">The directory service to search for files.</param>
        /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param>
        /// <returns>The external streams located.</returns>
        public IReadOnlyList<MediaStream> GetExternalStreams(
            Audio audio,
            int startIndex,
            IDirectoryService directoryService,
            bool clearCache)
        {
            if (!audio.IsFileProtocol)
            {
                return Array.Empty<MediaStream>();
            }

            var pathInfos = GetExternalFiles(audio, directoryService, clearCache);

            if (pathInfos.Count == 0)
            {
                return Array.Empty<MediaStream>();
            }

            var mediaStreams = new MediaStream[pathInfos.Count];

            for (var i = 0; i < pathInfos.Count; i++)
            {
                mediaStreams[i] = new MediaStream
                {
                    Type = MediaStreamType.Lyric,
                    Path = pathInfos[i].Path,
                    Language = pathInfos[i].Language,
                    Index = startIndex++
                };
            }

            return mediaStreams;
        }

        /// <summary>
        /// Returns the external file infos for the given video.
        /// </summary>
        /// <param name="video">The <see cref="Video"/> object to search external files for.</param>
        /// <param name="directoryService">The directory service to search for files.</param>
        /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param>
        /// <returns>The external file paths located.</returns>
        public IReadOnlyList<ExternalPathParserResult> GetExternalFiles(
            Video video,
            IDirectoryService directoryService,
            bool clearCache)
        {
            if (!video.IsFileProtocol)
            {
                return Array.Empty<ExternalPathParserResult>();
            }

            // Check if video folder exists
            string folder = video.ContainingFolderPath;
            if (!_fileSystem.DirectoryExists(folder))
            {
                return Array.Empty<ExternalPathParserResult>();
            }

            var files = directoryService.GetFilePaths(folder, clearCache, true).ToList();
            files.Remove(video.Path);
            var internalMetadataPath = video.GetInternalMetadataPath();
            if (_fileSystem.DirectoryExists(internalMetadataPath))
            {
                files.AddRange(directoryService.GetFilePaths(internalMetadataPath, clearCache, true));
            }

            if (files.Count == 0)
            {
                return Array.Empty<ExternalPathParserResult>();
            }

            var externalPathInfos = new List<ExternalPathParserResult>();
            ReadOnlySpan<char> prefix = video.FileNameWithoutExtension;
            foreach (var file in files)
            {
                var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(file.AsSpan());
                if (fileNameWithoutExtension.Length >= prefix.Length
                    && prefix.Equals(fileNameWithoutExtension[..prefix.Length], StringComparison.OrdinalIgnoreCase)
                    && (fileNameWithoutExtension.Length == prefix.Length || _namingOptions.MediaFlagDelimiters.Contains(fileNameWithoutExtension[prefix.Length])))
                {
                    var externalPathInfo = _externalPathParser.ParseFile(file, fileNameWithoutExtension[prefix.Length..].ToString());

                    if (externalPathInfo is not null)
                    {
                        externalPathInfos.Add(externalPathInfo);
                    }
                }
            }

            return externalPathInfos;
        }

        /// <summary>
        /// Returns the external file infos for the given audio.
        /// </summary>
        /// <param name="audio">The <see cref="Audio"/> object to search external files for.</param>
        /// <param name="directoryService">The directory service to search for files.</param>
        /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param>
        /// <returns>The external file paths located.</returns>
        public IReadOnlyList<ExternalPathParserResult> GetExternalFiles(
            Audio audio,
            IDirectoryService directoryService,
            bool clearCache)
        {
            if (!audio.IsFileProtocol)
            {
                return Array.Empty<ExternalPathParserResult>();
            }

            string folder = audio.ContainingFolderPath;
            var files = directoryService.GetFilePaths(folder, clearCache, true).ToList();
            files.Remove(audio.Path);
            var internalMetadataPath = audio.GetInternalMetadataPath();
            if (_fileSystem.DirectoryExists(internalMetadataPath))
            {
                files.AddRange(directoryService.GetFilePaths(internalMetadataPath, clearCache, true));
            }

            if (files.Count == 0)
            {
                return Array.Empty<ExternalPathParserResult>();
            }

            var externalPathInfos = new List<ExternalPathParserResult>();
            ReadOnlySpan<char> prefix = audio.FileNameWithoutExtension;
            foreach (var file in files)
            {
                var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(file.AsSpan());
                if (fileNameWithoutExtension.Length >= prefix.Length
                    && prefix.Equals(fileNameWithoutExtension[..prefix.Length], StringComparison.OrdinalIgnoreCase)
                    && (fileNameWithoutExtension.Length == prefix.Length || _namingOptions.MediaFlagDelimiters.Contains(fileNameWithoutExtension[prefix.Length])))
                {
                    var externalPathInfo = _externalPathParser.ParseFile(file, fileNameWithoutExtension[prefix.Length..].ToString());

                    if (externalPathInfo is not null)
                    {
                        externalPathInfos.Add(externalPathInfo);
                    }
                }
            }

            return externalPathInfos;
        }

        /// <summary>
        /// Returns the media info of the given file.
        /// </summary>
        /// <param name="path">The path to the file.</param>
        /// <param name="type">The <see cref="DlnaProfileType"/>.</param>
        /// <param name="cancellationToken">The cancellation token to cancel operation.</param>
        /// <returns>The media info for the given file.</returns>
        private Task<Model.MediaInfo.MediaInfo> GetMediaInfo(string path, DlnaProfileType type, CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();

            return _mediaEncoder.GetMediaInfo(
                new MediaInfoRequest
                {
                    MediaType = type,
                    MediaSource = new MediaSourceInfo
                    {
                        Path = path,
                        Protocol = MediaProtocol.File
                    }
                },
                cancellationToken);
        }

        /// <summary>
        /// Merges path metadata into stream metadata.
        /// </summary>
        /// <param name="mediaStream">The <see cref="MediaStream"/> object.</param>
        /// <param name="pathInfo">The <see cref="ExternalPathParserResult"/> object.</param>
        /// <returns>The modified mediaStream.</returns>
        private MediaStream MergeMetadata(MediaStream mediaStream, ExternalPathParserResult pathInfo)
        {
            mediaStream.Path = pathInfo.Path;
            mediaStream.IsExternal = true;
            mediaStream.Title = string.IsNullOrEmpty(mediaStream.Title) ? (string.IsNullOrEmpty(pathInfo.Title) ? null : pathInfo.Title) : mediaStream.Title;
            mediaStream.Language = string.IsNullOrEmpty(mediaStream.Language) ? (string.IsNullOrEmpty(pathInfo.Language) ? null : pathInfo.Language) : mediaStream.Language;

            return mediaStream;
        }
    }
}