using System;
using System.Collections.Concurrent;
using System.IO;
using System.Threading;
using ZeroLevel.Models;
using ZeroLevel.Services.Pools;

namespace ZeroLevel.Network.FileTransfer
{
    public sealed class FileSender
    {
        private BlockingCollection<FileTransferTask> _tasks = new BlockingCollection<FileTransferTask>();
        private ObjectPool<FileTransferTask> _taskPool = new ObjectPool<FileTransferTask>(() => new FileTransferTask(), 100);
        private readonly Thread _uploadFileThread;
        private bool _resendWhenServerError = false;
        private bool _resendWhenClientError = false;

        public void ResendWhenServerError(bool resend = true) => _resendWhenServerError = resend;
        public void ResendWhenClientError(bool resend = true) => _resendWhenClientError = resend;

        public FileSender()
        {
            _uploadFileThread = new Thread(UploadFileProcessing);
            _uploadFileThread.IsBackground = true;
            _uploadFileThread.Start();
        }

        public void Send(ExClient client, string fileName, Action<string> completeHandler = null, Action<string, string> errorHandler = null)
        {
            if (client == null) return;
            PushTransferTask(client, fileName, completeHandler, errorHandler);
        }

        private void PushTransferTask(ExClient client, string filePath, Action<string> completeHandler = null, Action<string, string> errorHandler = null)
        {
            if (client == null)
            {
                throw new ArgumentNullException(nameof(client));
            }
            if (string.IsNullOrWhiteSpace(filePath))
            {
                throw new ArgumentNullException(nameof(filePath));
            }
            if (false == File.Exists(filePath))
            {
                throw new FileNotFoundException(filePath);
            }
            var task = _taskPool.Allocate();
            task.CompletedHandler = completeHandler;
            task.ErrorHandler = errorHandler;
            task.FilePath = filePath;
            task.Client = client;
            _tasks.Add(task);
        }

        private void UploadFileProcessing()
        {
            while (true)
            {
                var task = _tasks.Take();
                try
                {
                    ExecuteSendFile(new FileReader(task.FilePath), task);
                    task.CompletedHandler?.Invoke(task.FilePath);
                }
                catch (Exception ex)
                {
                    task.ErrorHandler?.Invoke(task.FilePath, ex.ToString());
                }
                finally
                {
                    _taskPool.Free(task);
                }
            }
        }

        public bool Connected(ExClient client, TimeSpan timeout)
        {
            bool connected = false;
            using (var waiter = new ManualResetEvent(false))
            {
                client.Request<bool>("__file_transfer_ping__", (response) => { connected = response; waiter.Set(); });
                waiter.WaitOne(timeout);
            }
            return connected;
        }

        private static bool Send<T>(ExClient client, string inbox, T frame,
            bool resendWhenConnectionError, bool resendWhenServerError)
        {
            bool sended = false;
            var handle = new Action<InvokeResult>(ir =>
            {
                if (ir.Success == false && resendWhenServerError)
                {
                    Send<T>(client, inbox, frame, resendWhenConnectionError, false);
                }
            });
            sended = client.Request<T, InvokeResult>(inbox, frame, handle).Success;
            if (sended == false && resendWhenConnectionError)
            {
                sended = client.Request<T, InvokeResult>(inbox, frame, handle).Success;
            }
            return sended;
        }

        internal void ExecuteSendFile(FileReader reader, FileTransferTask task)
        {
            Log.Info($"Start upload file {reader.Path}");
            var startinfo = reader.GetStartInfo();

            if (!Send(task.Client, "__file_transfer_start_transfer__", startinfo, _resendWhenClientError, _resendWhenServerError))
            {
                Log.Debug($"Upload file {reader.Path} interrupted");
                return;
            }
            foreach (var chunk in reader.Read())
            {
                if (!Send(task.Client, "__file_transfer_frame__", chunk, _resendWhenClientError, _resendWhenServerError))
                {
                    Log.Debug($"Upload file {reader.Path} interrupted");
                    return;
                }
            }
            Send(task.Client, "__file_transfer_complete_transfer__", reader.GetCompleteInfo(), _resendWhenClientError, _resendWhenServerError);
        }
    }
}