using BackupEssentials.Backup.IO;
using BackupEssentials.Sys;
using BackupEssentials.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;

namespace BackupEssentials.Backup{
    public class BackupRunner{
        private static readonly bool DEBUG = false;

        private BackgroundWorker Worker;
        public readonly BackupRunInfo RunInfo;

        public Action<object,ProgressChangedEventArgs> EventProgressUpdate;
        public Action<object,RunWorkerCompletedEventArgs> EventCompleted;

        public BackupRunner(BackupRunInfo info){
            RunInfo = info;
        }

        public void Start(){
            if (Worker != null)return;

            Worker = new BackgroundWorker();
            Worker.WorkerReportsProgress = true;
            Worker.WorkerSupportsCancellation = true;

            if (EventProgressUpdate != null)Worker.ProgressChanged += new ProgressChangedEventHandler(EventProgressUpdate);
            if (EventCompleted != null)Worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(EventCompleted);
            Worker.DoWork += new DoWorkEventHandler(WorkerDoWork);

            Worker.RunWorkerAsync(RunInfo);
        }

        public void Cancel(){
            if (Worker != null)Worker.CancelAsync();
        }

        private void WorkerDoWork(object sender, DoWorkEventArgs e){
            BackgroundWorker worker = (BackgroundWorker)sender;
            BackupRunInfo data = (BackupRunInfo)e.Argument;

            string[] src = data.Source;
            string fullSrc = string.Join(", ",src);
            string destFolder = data.Destination;
            string srcParent = null;

            bool ignoreRoot = false;

            if (src[0].EndsWith(@":\",StringComparison.Ordinal)){
                if (src.Length == 1){
                    srcParent = src[0];

                    List<string> newSrc = new List<string>();
                    foreach(string dir in GetVisibleDirectories(src[0],"*",SearchOption.TopDirectoryOnly))newSrc.Add(Path.Combine(src[0],dir));
                    foreach(string file in Directory.GetFiles(src[0],"*.*",SearchOption.TopDirectoryOnly))newSrc.Add(Path.Combine(src[0],file));
                    src = newSrc.ToArray();

                    ignoreRoot = true;
                }
            }
            else srcParent = Directory.GetParent(src[0]).FullName;

            // Verify source files
            if (srcParent == null){
                throw new Exception(Settings.Default.Language["BackupWindow.Error.MultipleLocations"]);
            }

            for(int a = 1; a < src.Length; a++){
                if (!Directory.GetParent(src[a]).FullName.Equals(srcParent))throw new Exception(Settings.Default.Language["BackupWindow.Error.MultipleLocations"]);
            }

            if (srcParent[srcParent.Length-1] == '\\')srcParent = srcParent.Substring(0,srcParent.Length-1);

            // Figure out the source file and directory lists
            HashSet<string> rootSrcEntries = new HashSet<string>();
            Dictionary<string,IOEntry> srcEntries = new Dictionary<string,IOEntry>(), dstEntries = new Dictionary<string,IOEntry>();
            string[] updatedSrc = new string[src.Length];

            for(int a = 0; a < src.Length; a++){
                string srcEntry = src[a];

                if (File.GetAttributes(srcEntry).HasFlag(FileAttributes.Directory)){
                    int srcLen = srcParent.Length+1;
                    string dname = srcEntry.Remove(0,srcLen);

                    rootSrcEntries.Add(dname);
                    srcEntries.Add(dname,new IOEntry(){ Type = IOType.Directory, AbsolutePath = srcEntry });

                    foreach(string dir in GetVisibleDirectories(srcEntry,"*",SearchOption.AllDirectories))srcEntries.Add(dir.Remove(0,srcLen),new IOEntry(){ Type = IOType.Directory, AbsolutePath = dir });
                    foreach(string file in Directory.GetFiles(srcEntry,"*.*",SearchOption.AllDirectories))srcEntries.Add(file.Remove(0,srcLen),new IOEntry(){ Type = IOType.File, AbsolutePath = file });
                }
                else{
                    string fname = Path.GetFileName(srcEntry);

                    rootSrcEntries.Add(fname);
                    srcEntries.Add(fname,new IOEntry(){ Type = IOType.File, AbsolutePath = srcEntry });
                
                    updatedSrc[a] = Directory.GetParent(srcEntry).FullName;
                }
            }

            src = updatedSrc;

            // Figure out the destination info
            if (!Directory.Exists(destFolder))Directory.CreateDirectory(destFolder);
            
            int destFolderLen = destFolder.Length+1;

            foreach(string entry in Directory.GetFileSystemEntries(destFolder,"*",SearchOption.TopDirectoryOnly)){
                string entryName = entry.Remove(0,destFolderLen);

                if (ignoreRoot || rootSrcEntries.Remove(entryName)){
                    if (File.GetAttributes(entry).HasFlag(FileAttributes.Directory)){
                        dstEntries.Add(entryName,new IOEntry(){ Type = IOType.Directory, AbsolutePath = entry });
                        foreach(string dir in GetVisibleDirectories(entry,"*",SearchOption.AllDirectories))dstEntries.Add(dir.Remove(0,destFolderLen),new IOEntry(){ Type = IOType.Directory, AbsolutePath = dir });
                        foreach(string file in Directory.GetFiles(entry,"*.*",SearchOption.AllDirectories))dstEntries.Add(file.Remove(0,destFolderLen),new IOEntry(){ Type = IOType.File, AbsolutePath = file });
                    }
                    else dstEntries.Add(entryName,new IOEntry(){ Type = IOType.File, AbsolutePath = entry });
                }
            }

            // Generate the IO actions
            List<IOActionEntry> actions = new List<IOActionEntry>();
            KeyEqualityComparer<string,IOEntry> keyComparer = new KeyEqualityComparer<string,IOEntry>();
            
            IEnumerable<KeyValuePair<string,IOEntry>> ioDeleted = dstEntries.Except(srcEntries,keyComparer);
            IEnumerable<KeyValuePair<string,IOEntry>> ioAdded = srcEntries.Except(dstEntries,keyComparer);
            IEnumerable<string> ioIntersecting = srcEntries.Keys.Intersect(dstEntries.Keys);
            
            foreach(KeyValuePair<string,IOEntry> deleted in ioDeleted){
                if (!ignoreRoot && deleted.Key.IndexOf(Path.DirectorySeparatorChar) == -1)continue; // ignore everything in root folder
                actions.Add(new IOActionEntry(){ Type = deleted.Value.Type, Action = IOAction.Delete, RelativePath = deleted.Key });
            }

            foreach(KeyValuePair<string,IOEntry> added in ioAdded){
                actions.Add(new IOActionEntry(){ Type = added.Value.Type, Action = IOAction.Create, RelativePath = added.Key });
            }

            foreach(string intersecting in ioIntersecting){
                IOEntry srcEntry = srcEntries[intersecting];
                if (srcEntry.Type == IOType.Directory)continue;

                FileInfo srcFileInfo = new FileInfo(srcEntry.AbsolutePath);
                FileInfo dstFileInfo = new FileInfo(dstEntries[intersecting].AbsolutePath);

                if (srcFileInfo.Length == dstFileInfo.Length && srcFileInfo.LastWriteTime == dstFileInfo.LastWriteTime)continue;
                actions.Add(new IOActionEntry(){ Type = IOType.File, Action = IOAction.Replace, RelativePath = intersecting });
            }

            actions.Sort((entry1, entry2) => {
                if (entry1.Type == IOType.Directory && entry2.Type == IOType.File)return -1;
                else if (entry2.Type == IOType.Directory && entry1.Type == IOType.File)return 1;
                else return 0;
            });

            // Report a state update
            worker.ReportProgress(0,actions.Count);

            // Start working
            List<int> indexesToRemove = new List<int>();
            int totalActions = actions.Count, attempts = 10;
            bool firstAttempt = true;
            string path;

            BackupReport.Builder reportBuilder = new BackupReport.Builder();

            if (DEBUG){
                reportBuilder.Add("Report.Title.Debug");
                reportBuilder.Add("Report.Info.Debug");
                reportBuilder.AddLine();
            }

            reportBuilder.Add("Report.Title.PreparingBackup");
            reportBuilder.Add(BackupReport.Constants.Source,fullSrc);
            reportBuilder.Add(BackupReport.Constants.Destination,destFolder);
            reportBuilder.Add(BackupReport.Constants.Date,Settings.Default.DateFormat.ParseDate(DateTime.Now));
            reportBuilder.AddLine();
            reportBuilder.Add("Report.Title.FilesAndFolders");
            reportBuilder.Add(BackupReport.Constants.EntriesAdded,actions.Count((entry) => entry.Action == IOAction.Create).ToString(CultureInfo.InvariantCulture));
            reportBuilder.Add(BackupReport.Constants.EntriesUpdated,actions.Count((entry) => entry.Action == IOAction.Replace).ToString(CultureInfo.InvariantCulture));
            reportBuilder.Add(BackupReport.Constants.EntriesDeleted,actions.Count((entry) => entry.Action == IOAction.Delete).ToString(CultureInfo.InvariantCulture));
            reportBuilder.AddLine();
            reportBuilder.Add("Report.Title.StartingBackup");

            while(actions.Count > 0 && --attempts > 0){
                if (firstAttempt)firstAttempt = false;
                else Thread.Sleep(200);

                for(int index = 0; index < actions.Count; index++){
                    IOActionEntry entry = actions[index];

                    try{
                        bool ignoreEntry = false;

                        if (entry.Action == IOAction.Delete){
                            path = Path.Combine(destFolder,entry.RelativePath);

                            if (entry.Type == IOType.File){
                                if (File.Exists(path)){
                                    if (DEBUG)reportBuilder.Add("[D] Deleting file: "+path);
                                    else File.Delete(path);
                                }
                                else ignoreEntry = true;
                            }
                            else if (entry.Type == IOType.Directory){
                                if (Directory.Exists(path)){
                                    if (DEBUG)reportBuilder.Add("[D] Deleting directory: "+path);
                                    else Directory.Delete(path,true);
                                }
                                else ignoreEntry = true;
                            }
                        }
                        else if (entry.Action == IOAction.Create){
                            path = Path.Combine(destFolder,entry.RelativePath);

                            if (entry.Type == IOType.File){
                                if (DEBUG)reportBuilder.Add("[D] Copying file: "+Path.Combine(srcParent,entry.RelativePath)+" --> "+path);
                                else File.Copy(Path.Combine(srcParent,entry.RelativePath),path,false);
                            }
                            else if (entry.Type == IOType.Directory){
                                if (!Directory.Exists(path)){
                                    if (DEBUG)reportBuilder.Add("[D] Creating directory: "+path);
                                    else Directory.CreateDirectory(path);
                                }
                            }
                        }
                        else if (entry.Action == IOAction.Replace){
                            if (DEBUG)reportBuilder.Add("[D] Replacing file: "+Path.Combine(srcParent,entry.RelativePath)+" --> "+Path.Combine(destFolder,entry.RelativePath));
                            else File.Copy(Path.Combine(srcParent,entry.RelativePath),Path.Combine(destFolder,entry.RelativePath),true);
                        }
                        
                        indexesToRemove.Add(index-indexesToRemove.Count); // goes from 0 to actions.Count, removing each index will move the structure
                        if (!ignoreEntry)reportBuilder.Add(entry.Action,entry.Type,entry.RelativePath);
                        
                        worker.ReportProgress((int)Math.Ceiling(((totalActions-actions.Count+indexesToRemove.Count)*100D)/totalActions));
                        if (worker.CancellationPending)break;
                    }catch(Exception exception){ // if an action failed, it will not be removed
                        Debug.WriteLine("Failed: "+entry.ToString());
                        Debug.WriteLine(exception.Message);
                        // TODO handle special exceptions (security etc)
                    }

                    if (worker.CancellationPending){
                        reportBuilder.Add("Report.Title.BackupCanceled");
                        e.Result = reportBuilder.Finish();
                        throw new Exception(Settings.Default.Language["Report.Error.Canceled"]);
                    }
                }

                foreach(int index in indexesToRemove)actions.RemoveAt(index);
                indexesToRemove.Clear();
            }

            if (attempts == 0){
                reportBuilder.Add("Report.Title.BackupFailed");
                e.Result = reportBuilder.Finish();
                throw new Exception(Settings.Default.Language["Report.Error.Failed"]);
            }

            reportBuilder.Add("Report.Title.BackupFinished");
            e.Result = reportBuilder.Finish();
        }

        private static IEnumerable<string> GetVisibleDirectories(string path, string searchPattern, SearchOption searchOption){
            foreach(string dir in Directory.EnumerateDirectories(path,searchPattern,searchOption)){
                if (!File.GetAttributes(dir).HasFlag(FileAttributes.Hidden))yield return dir;
            }
        }

        private class IOEntry{
            public IOType Type;
            public string AbsolutePath;

            public override string ToString(){
                return "{ Type: "+Type.ToString()+", AbsolutePath: "+(AbsolutePath == null ? "<null>" : AbsolutePath.ToString())+" }";
            }
        }

        private class IOActionEntry{
            private string _relativePath;

            public IOType Type;
            public IOAction Action;
            public string RelativePath { get { return _relativePath; } set { _relativePath = value; if (_relativePath.StartsWith("\\",StringComparison.OrdinalIgnoreCase))_relativePath = _relativePath.Substring(1); } }
            
            public override string ToString(){
                return "{ Type: "+Type.ToString()+", Action: "+Action.ToString()+", RelativePath: "+(RelativePath == null ? "<null>" : RelativePath.ToString())+" }";
            }
        }
    }
}