using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;

namespace ZeroLevel.Services.FileSystem
{
    public sealed class PeriodicFileSystemWatcher :
        IDisposable
    {
        private long _updateSourceTaskKey;
        private readonly string _sourceFolder;
        private readonly string _temporaryFolder;
        private readonly TimeSpan _period;
        private readonly Action<FileMeta> _callback;
        private readonly HashSet<string> _extensions;

        public event Action<int> OnStartMovingFilesToTemporary = delegate { };
        public event Action<string, string> OnMovingFileToTemporary = delegate { };
        public event Action OnCompleteMovingFilesToTemporary = delegate { };

        private readonly bool _autoRemoveTempFileAfterCallback = false;
        private readonly bool _useSubdirectories = false;

        public PeriodicFileSystemWatcher(TimeSpan period, string watch_folder, string temp_folder, Action<FileMeta> callback
            , IEnumerable<string> extensions = null
            , bool removeTempFileAfterCallback = false
            , bool useSubdirectories = false)
        {
            if (string.IsNullOrWhiteSpace(watch_folder))
            {
                throw new ArgumentNullException(nameof(watch_folder));
            }
            if (callback == null)
            {
                throw new ArgumentNullException(nameof(callback));
            }
            _extensions = new HashSet<string>(extensions?.Select(e => e.ToLowerInvariant()) ?? Enumerable.Empty<string>());
            _useSubdirectories = useSubdirectories;
            _autoRemoveTempFileAfterCallback = removeTempFileAfterCallback;
            _callback = callback;
            _sourceFolder = watch_folder;
            _temporaryFolder = temp_folder;
            _period = period;
            if (Path.IsPathRooted(_temporaryFolder) == false)
            {
                _temporaryFolder = Path.Combine(Configuration.BaseDirectory, _temporaryFolder);
            }
            if (false == Directory.Exists(_temporaryFolder))
            {
                Directory.CreateDirectory(_temporaryFolder);
            }
        }

        public void Start()
        {
            _updateSourceTaskKey = Sheduller.RemindEvery(_period, CheckSourceFolder);
        }

        public void Stop()
        {
            Sheduller.Remove(_updateSourceTaskKey);
        }

        private void CheckSourceFolder()
        {
            string[] files = null;
            try
            {
                files = GetFilesFromSource();
            }
            catch (Exception ex)
            {
                Log.SystemError(ex, $"[PeriodicFileSystemWatcher.CheckSourceFolder] Failed to process input directory '{_sourceFolder}'");
            }
            if (files == null || files.Length == 0)
            {
                return;
            }
            foreach (Action<int> startFilesMoveingCallback in OnStartMovingFilesToTemporary.GetInvocationList())
                startFilesMoveingCallback.BeginInvoke(files.Length, null, null);

            foreach (var file in files)
            {
                if (!File.Exists(file))
                {
                    Log.Warning($"[PeriodicFileSystemWatcher.CheckSourceFolder] Find '{file}' does not exists");
                    continue;
                }
                Log.Debug($"[PeriodicFileSystemWatcher.CheckSourceFolder] Find new file {file}");
                if (FSUtils.IsFileLocked(new FileInfo(file)))
                {
                    continue;
                }
                string tempFile;
                try
                {
                    tempFile = MoveToTemporary(file);
                    if (string.IsNullOrWhiteSpace(tempFile))
                    {
                        Log.SystemWarning($"[PeriodicFileSystemWatcher.CheckSourceFolder] Failed to move file '{file}' to temporary directory '{_temporaryFolder}'. Without system error!");
                        continue;
                    }

                    foreach (Action<string,string> moveFileToTempCallback in OnMovingFileToTemporary.GetInvocationList())
                        moveFileToTempCallback.BeginInvoke(file, tempFile, null, null);
                }
                catch (Exception ex)
                {
                    Log.SystemError(ex, $"[PeriodicFileSystemWatcher.CheckSourceFolder] Failed to attempt to move file '{file}' to temporary directory '{_temporaryFolder}'");
                    continue;
                }
                Log.Debug($"[PeriodicFileSystemWatcher.CheckSourceFolder] Handle file {file}");
                try
                {
                    _callback.Invoke(new FileMeta(Path.GetFileName(file), tempFile));
                }
                catch (Exception ex)
                {
                    Log.SystemError(ex, $"[PeriodicFileSystemWatcher.CheckSourceFolder] Fault callback for file '{tempFile}'");
                }
                try
                {
                    if (_autoRemoveTempFileAfterCallback)
                    {
                        File.Delete(tempFile);
                    }
                }
                catch (Exception ex)
                {
                    Log.SystemError(ex, $"[PeriodicFileSystemWatcher.CheckSourceFolder] Fault delete file {tempFile}");
                }
            }
            foreach (Action completeFileMovingCallback in OnCompleteMovingFilesToTemporary.GetInvocationList())
                completeFileMovingCallback.BeginInvoke(null, null);
        }

        /// <summary>
        /// Moving a file to a temporary directory
        /// </summary>
        private string MoveToTemporary(string from)
        {
            if (from == null)
            {
                throw new ArgumentException("from");
            }
            string tempFile = Path.Combine(_temporaryFolder, Path.GetFileName(from));
            if (File.Exists(tempFile))
            {
                tempFile = TrySolveCollision(tempFile);
            }
            File.Copy(from, tempFile, false);
            if (File.Exists(tempFile))
            {
                File.Delete(from);
                return tempFile;
            }
            return null;
        }

        /// <summary>
        /// Resolving collisions in filenames in the temporary directory
        /// </summary>
        private static string TrySolveCollision(string file)
        {
            if (file == null)
            {
                throw new ArgumentNullException("file");
            }
            string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(file);
            string extension = Path.GetExtension(file);
            string directoryName = Path.GetDirectoryName(file);
            if (directoryName != null)
            {
                int num = 0;
                do
                {
                    num++;
                }
                while (File.Exists(Path.Combine(directoryName,
                fileNameWithoutExtension + "_" +
                num.ToString(CultureInfo.CurrentCulture) +
                extension)));
                return Path.Combine(directoryName,
                    fileNameWithoutExtension + "_" +
                    num.ToString(CultureInfo.CurrentCulture) +
                    extension);
            }
            throw new ArgumentException("folder");
        }

        /// <summary>
        /// Getting a list of files from the input directory
        /// </summary>
        private string[] GetFilesFromSource()
        {
            string[] files;
            if (_extensions.Count > 0)
            {
                files = Directory.GetFiles(_sourceFolder, "*.*", _useSubdirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly)
                    ?.Where(f => _extensions.Contains(Path.GetExtension(f).ToLowerInvariant()))
                    ?.ToArray();
            }
            else
            {
                files = Directory.GetFiles(_sourceFolder, "*.*", _useSubdirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly);
            }
            if (files != null)
            {
                Array.Sort<string>(files, FileNameSortCompare);
            }
            return files;
        }

        /// <summary>
        /// File Name Comparison
        /// </summary>
        private static int FileNameSortCompare(string x, string y)
        {
            int num;
            int num2;
            if (int.TryParse(Path.GetFileNameWithoutExtension(x), out num) &&
                int.TryParse(Path.GetFileNameWithoutExtension(y), out num2))
            {
                return num - num2;
            }
            return string.Compare(x, y, StringComparison.InvariantCultureIgnoreCase);
        }

        public void Dispose()
        {
            Sheduller.Remove(_updateSourceTaskKey);
        }
    }
}