using MediaBrowser.Common.Events;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Logging;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace MediaBrowser.Server.Implementations.Session
{
    /// 
    /// Class SessionManager
    /// 
    public class SessionManager : ISessionManager
    {
        /// 
        /// The _user data repository
        /// 
        private readonly IUserDataRepository _userDataRepository;
        /// 
        /// The _user repository
        /// 
        private readonly IUserRepository _userRepository;
        /// 
        /// The _logger
        /// 
        private readonly ILogger _logger;
        /// 
        /// Gets or sets the configuration manager.
        /// 
        /// The configuration manager.
        private readonly IServerConfigurationManager _configurationManager;
        /// 
        /// The _active connections
        /// 
        private readonly ConcurrentDictionary _activeConnections =
            new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase);
        /// 
        /// Occurs when [playback start].
        /// 
        public event EventHandler PlaybackStart;
        /// 
        /// Occurs when [playback progress].
        /// 
        public event EventHandler PlaybackProgress;
        /// 
        /// Occurs when [playback stopped].
        /// 
        public event EventHandler PlaybackStopped;
        /// 
        /// Initializes a new instance of the  class.
        /// 
        /// The user data repository.
        /// The configuration manager.
        /// The logger.
        /// The user repository.
        public SessionManager(IUserDataRepository userDataRepository, IServerConfigurationManager configurationManager, ILogger logger, IUserRepository userRepository)
        {
            _userDataRepository = userDataRepository;
            _configurationManager = configurationManager;
            _logger = logger;
            _userRepository = userRepository;
        }
        /// 
        /// Gets all connections.
        /// 
        /// All connections.
        public IEnumerable Sessions
        {
            get { return _activeConnections.Values.OrderByDescending(c => c.LastActivityDate).ToList(); }
        }
        /// 
        /// The _true task result
        /// 
        private readonly Task _trueTaskResult = Task.FromResult(true);
        /// 
        /// Logs the user activity.
        /// 
        /// Type of the client.
        /// The app version.
        /// The device id.
        /// Name of the device.
        /// The user.
        /// Task.
        /// 
        /// user
        public async Task LogConnectionActivity(string clientType, string appVersion, string deviceId, string deviceName, User user)
        {
            if (string.IsNullOrEmpty(clientType))
            {
                throw new ArgumentNullException("clientType");
            }
            if (string.IsNullOrEmpty(appVersion))
            {
                throw new ArgumentNullException("appVersion");
            }
            if (string.IsNullOrEmpty(deviceId))
            {
                throw new ArgumentNullException("deviceId");
            }
            if (string.IsNullOrEmpty(deviceName))
            {
                throw new ArgumentNullException("deviceName");
            }
            if (user != null && user.Configuration.IsDisabled)
            {
                throw new UnauthorizedAccessException(string.Format("The {0} account is currently disabled. Please consult with your administrator.", user.Name));
            }
            var activityDate = DateTime.UtcNow;
            var session = GetSessionInfo(clientType, appVersion, deviceId, deviceName, user);
            
            session.LastActivityDate = activityDate;
            if (user == null)
            {
                return session;
            }
            var lastActivityDate = user.LastActivityDate;
            user.LastActivityDate = activityDate;
            // Don't log in the db anymore frequently than 10 seconds
            if (lastActivityDate.HasValue && (activityDate - lastActivityDate.Value).TotalSeconds < 10)
            {
                return session;
            }
            // Save this directly. No need to fire off all the events for this.
            await _userRepository.SaveUser(user, CancellationToken.None).ConfigureAwait(false);
            return session;
        }
        /// 
        /// Updates the now playing item id.
        /// 
        /// The session.
        /// The item.
        /// if set to true [is paused].
        /// The current position ticks.
        private void UpdateNowPlayingItem(SessionInfo session, BaseItem item, bool isPaused, long? currentPositionTicks = null)
        {
            session.IsPaused = isPaused;
            session.NowPlayingPositionTicks = currentPositionTicks;
            session.NowPlayingItem = item;
            session.LastActivityDate = DateTime.UtcNow;
        }
        /// 
        /// Removes the now playing item id.
        /// 
        /// The session.
        /// The item.
        private void RemoveNowPlayingItem(SessionInfo session, BaseItem item)
        {
            if (session.NowPlayingItem != null && session.NowPlayingItem.Id == item.Id)
            {
                session.NowPlayingItem = null;
                session.NowPlayingPositionTicks = null;
                session.IsPaused = null;
            }
        }
        /// 
        /// Gets the connection.
        /// 
        /// Type of the client.
        /// The app version.
        /// The device id.
        /// Name of the device.
        /// The user.
        /// SessionInfo.
        private SessionInfo GetSessionInfo(string clientType, string appVersion, string deviceId, string deviceName, User user)
        {
            var key = clientType + deviceId + appVersion;
            var connection = _activeConnections.GetOrAdd(key, keyName => new SessionInfo
            {
                Client = clientType,
                DeviceId = deviceId,
                ApplicationVersion = appVersion,
                Id = Guid.NewGuid()
            });
            connection.DeviceName = deviceName;
            connection.User = user;
            return connection;
        }
        /// 
        /// Used to report that playback has started for an item
        /// 
        /// The item.
        /// The session id.
        /// Task.
        /// 
        public async Task OnPlaybackStart(BaseItem item, Guid sessionId)
        {
            if (item == null)
            {
                throw new ArgumentNullException();
            }
            var session = Sessions.First(i => i.Id.Equals(sessionId));
            UpdateNowPlayingItem(session, item, false);
            var key = item.GetUserDataKey();
            var user = session.User;
            
            var data = _userDataRepository.GetUserData(user.Id, key);
            data.PlayCount++;
            data.LastPlayedDate = DateTime.UtcNow;
            if (!(item is Video))
            {
                data.Played = true;
            }
            await _userDataRepository.SaveUserData(user.Id, key, data, CancellationToken.None).ConfigureAwait(false);
            // Nothing to save here
            // Fire events to inform plugins
            EventHelper.QueueEventIfNotNull(PlaybackStart, this, new PlaybackProgressEventArgs
            {
                Item = item,
                User = user
            }, _logger);
        }
        /// 
        /// Used to report playback progress for an item
        /// 
        /// The item.
        /// The position ticks.
        /// if set to true [is paused].
        /// The session id.
        /// Task.
        /// 
        /// positionTicks
        public async Task OnPlaybackProgress(BaseItem item, long? positionTicks, bool isPaused, Guid sessionId)
        {
            if (item == null)
            {
                throw new ArgumentNullException();
            }
            if (positionTicks.HasValue && positionTicks.Value < 0)
            {
                throw new ArgumentOutOfRangeException("positionTicks");
            }
            var session = Sessions.First(i => i.Id.Equals(sessionId));
            UpdateNowPlayingItem(session, item, isPaused, positionTicks);
            var key = item.GetUserDataKey();
            var user = session.User;
            if (positionTicks.HasValue)
            {
                var data = _userDataRepository.GetUserData(user.Id, key);
                UpdatePlayState(item, data, positionTicks.Value);
                await _userDataRepository.SaveUserData(user.Id, key, data, CancellationToken.None).ConfigureAwait(false);
            }
            EventHelper.QueueEventIfNotNull(PlaybackProgress, this, new PlaybackProgressEventArgs
            {
                Item = item,
                User = user,
                PlaybackPositionTicks = positionTicks
            }, _logger);
        }
        /// 
        /// Used to report that playback has ended for an item
        /// 
        /// The item.
        /// The position ticks.
        /// The session id.
        /// Task.
        /// 
        public async Task OnPlaybackStopped(BaseItem item, long? positionTicks, Guid sessionId)
        {
            if (item == null)
            {
                throw new ArgumentNullException();
            }
            if (positionTicks.HasValue && positionTicks.Value < 0)
            {
                throw new ArgumentOutOfRangeException("positionTicks");
            }
            
            var session = Sessions.First(i => i.Id.Equals(sessionId));
            RemoveNowPlayingItem(session, item);
            var key = item.GetUserDataKey();
            var user = session.User;
            
            var data = _userDataRepository.GetUserData(user.Id, key);
            if (positionTicks.HasValue)
            {
                UpdatePlayState(item, data, positionTicks.Value);
            }
            else
            {
                // If the client isn't able to report this, then we'll just have to make an assumption
                data.PlayCount++;
                data.Played = true;
            }
            await _userDataRepository.SaveUserData(user.Id, key, data, CancellationToken.None).ConfigureAwait(false);
            EventHelper.QueueEventIfNotNull(PlaybackStopped, this, new PlaybackProgressEventArgs
            {
                Item = item,
                User = user,
                PlaybackPositionTicks = positionTicks
            }, _logger);
        }
        /// 
        /// Updates playstate position for an item but does not save
        /// 
        /// The item
        /// User data for the item
        /// The current playback position
        private void UpdatePlayState(BaseItem item, UserItemData data, long positionTicks)
        {
            var hasRuntime = item.RunTimeTicks.HasValue && item.RunTimeTicks > 0;
            // If a position has been reported, and if we know the duration
            if (positionTicks > 0 && hasRuntime)
            {
                var pctIn = Decimal.Divide(positionTicks, item.RunTimeTicks.Value) * 100;
                // Don't track in very beginning
                if (pctIn < _configurationManager.Configuration.MinResumePct)
                {
                    positionTicks = 0;
                }
                // If we're at the end, assume completed
                else if (pctIn > _configurationManager.Configuration.MaxResumePct || positionTicks >= item.RunTimeTicks.Value)
                {
                    positionTicks = 0;
                    data.Played = true;
                }
                else
                {
                    // Enforce MinResumeDuration
                    var durationSeconds = TimeSpan.FromTicks(item.RunTimeTicks.Value).TotalSeconds;
                    if (durationSeconds < _configurationManager.Configuration.MinResumeDurationSeconds)
                    {
                        positionTicks = 0;
                        data.Played = true;
                    }
                }
            }
            else if (!hasRuntime)
            {
                // If we don't know the runtime we'll just have to assume it was fully played
                data.Played = true;
                positionTicks = 0;
            }
            if (item is Audio)
            {
                positionTicks = 0;
            }
            data.PlaybackPositionTicks = positionTicks;
        }
    }
}