using System;
using System.Threading.Tasks;
using System.Threading;
using Grpc.Core;
using Aurora.Models.Media;
using Aurora.Proto.Sync;
using Aurora.Proto.Events;
using Aurora.Proto.Party;
using LibVLCSharp.Shared;

namespace Aurora.Services.PlayerService
{

    public class PlayerService : BaseService<PlayerService>
    {
        private const long _ticksPerMillisecond = 10000;
        private BaseMedia _currentMedia;
        private MediaPlayer _mediaPlayer;
        private LibVLC _libvlc;
        private PlaybackState _state;

        public PlayerService()
        {
            _libvlc = new LibVLC();
            _state = PlaybackState.Stopped;
        }

        /// <summary>
        /// Event handler for changing playback states.
        /// </summary>
        public event PlaybackStateChangedEventHandler PlaybackStateChanged;

        public event MediaChangedEventHandler MediaChanged;

        /// <summary>
        /// The state of playback
        /// </summary>
        /// <value></value>
        public PlaybackState PlaybackState
        {
            get { return _state; }
        }

        public bool IsLoaded
        {
            get
            {
                return this._currentMedia != null;
            }
        }

        public bool IsMediaLoaded(BaseMedia media)
        {
            return _currentMedia == media;
        }

        public BaseMedia CurrentMedia
        {
            get { return _currentMedia; }
        }

        public float CurrentMediaPosition
        {
            get
            {
                return _mediaPlayer.Position;
            }
        }

        public long CurrentMediaLength
        {
            get
            {
                return _mediaPlayer.Length;
            }
        }

        /// <summary>
        /// Load media into the media player.
        /// </summary>
        /// <param name="media">Media to load</param>
        public async Task LoadMedia(BaseMedia media)
        {
            if (_state == PlaybackState.Playing || _state == PlaybackState.Buffering)
            {
                Unload();
            }
            _currentMedia = media;
            await _currentMedia.Load();
            var md = new Media(_libvlc, _currentMedia.DataStream);
            _mediaPlayer = new MediaPlayer(md);
            _mediaPlayer.Stopped += OnStopped;
            md.Dispose();

            if (MediaChanged != null)
            {
                MediaChanged.Invoke(this, new MediaChangedEventArgs(_currentMedia.Id, _currentMedia.Metadata));
            }

        }

        /// <summary>
        /// Play currently loaded media.
        /// </summary>
        public void Play()
        {
            PlaybackState oldState = _state;
            _state = PlaybackState.Playing;

            _mediaPlayer.Play();
            //Use sync RPC for remote audio
            if (_currentMedia is RemoteAudio)
            {
                RemoteAudio media = _currentMedia as RemoteAudio;
                if (!media.FromHost)
                {
                    RemoteSyncService.RemoteSyncServiceClient remoteSyncClient = media.RemoteSyncClient;

                    //Sync playback in a separate task
                    //Task completes when host stops syncing (when a song is complete)
                    Task syncTask = new Task(async () =>
                    {
                        CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
                        using (AsyncServerStreamingCall<Sync> syncStream = remoteSyncClient
                            .GetMediaSync(new Proto.General.Empty()))
                        {
                            try
                            {
                                while (await syncStream.ResponseStream.MoveNext(cancellationTokenSource.Token))
                                {
                                    Sync sync = new Sync(syncStream.ResponseStream.Current);
                                    if (sync != null)
                                    {
                                        Utils.Time time = Utils.TimeUtils.GetNetworkTime();
                                        //Adjust position based on sync
                                        DateTime localTime = time.DateTime;

                                        //Get offset - elapsed time converted to milliseconds
                                        float length = CurrentMediaLength;
                                        float offset = (localTime.Ticks - sync.ServerTimeTicks) * _ticksPerMillisecond;

                                        float newPosition = (sync.TrackPosition + offset);

                                        //Adjust position if greater than 10 percent difference
                                        float oldPosition = _mediaPlayer.Position;
                                        if (newPosition - oldPosition > 0.001 ||
                                            newPosition - oldPosition < -0.001)
                                        {
                                            _mediaPlayer.Position = newPosition;
                                            Console.WriteLine(string.Format("**Audio synced**"));
                                            Console.WriteLine(string.Format("Remote Server Time {0}", new DateTime(sync.ServerTimeTicks).ToLongTimeString()));
                                            Console.WriteLine(string.Format("Remote Track Time: {0}", sync.TrackPosition));
                                            Console.WriteLine(string.Format("Local Server Time: {0}", time.DateTime.ToLongTimeString()));
                                            Console.WriteLine(string.Format("Local Track Time: {0}", _mediaPlayer.Position));
                                            Console.WriteLine(string.Format("Offset: {0}", offset));
                                            Console.WriteLine(string.Format("Old Position: {0}", oldPosition));
                                            Console.WriteLine(string.Format("New Position: {0}", newPosition));

                                        }
                                    }
                                }
                            }
                            catch (Exception ex)
                            {
                                Console.WriteLine("Exception caught while attempting to sync: " + ex.Message);
                            }
                        }
                    });

                    syncTask.Start();
                }
            }

            if (PlaybackStateChanged != null)
            {
                PlaybackStateChanged.Invoke(this, new PlaybackStateChangedEventArgs(oldState, _state));
            }
        }

        /// <summary>
        /// Pause currently loaded media.
        /// </summary>
        public void Pause()
        {
            PlaybackState oldState = _state;
            _state = PlaybackState.Buffering;
            _mediaPlayer.Pause();

            if (PlaybackStateChanged != null)
            {
                PlaybackStateChanged.Invoke(this, new PlaybackStateChangedEventArgs(oldState, _state));
            }
        }

        /// <summary>
        /// Stop currently loaded media.
        /// </summary>
        public void Stop()
        {
            PlaybackState oldState = _state;
            _state = PlaybackState.Stopped;
            _mediaPlayer.Stop();

            if (PlaybackStateChanged != null)
            {
                PlaybackStateChanged.Invoke(this, new PlaybackStateChangedEventArgs(oldState, _state));
            }
        }

        public void Enqueue(BaseMedia song)
        {
            throw new NotImplementedException();
        }

        public void Dequeue(BaseMedia song)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Unload currently loaded media.
        /// </summary>
        private void Unload()
        {
            _currentMedia.Unload();
            _currentMedia = null;
            _mediaPlayer.Media = null;
            _mediaPlayer = null;
        }

        /// <summary>
        /// Event fired when currently loaded media player stops.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        private void OnStopped(object sender, EventArgs args)
        {
            PlaybackStateChanged.Invoke(this, new PlaybackStateChangedEventArgs(_state, PlaybackState.Stopped));
            _state = PlaybackState.Stopped;
            this.Unload();
        }

    }
}
