using System;
using System.Linq;
using System.ComponentModel;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.Serialization;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.IO;
using System.Windows.Forms;
using FooEditEngine;
using FooEditEngine.Windows;
using FooEditor.Properties;
using Microsoft.WindowsAPICodePack.Taskbar;
using EncodeDetect;

namespace FooEditor
{
    public delegate void EncodeChangeEventHandler(object sender,EventArgs e);
    public delegate void DocumentTypeChangeEventHandler(object sender, EventArgs e);
    public delegate void LinefeedTypeChangeEventHandler(object sender, EventArgs e);

    [SerializableAttribute]
    public partial class EditForm : WeifenLuo.WinFormsUI.Docking.DockContent,ISerializable
    {
        public EditForm()
        {
            InitializeComponent();

            if (Environment.OSVersion.Version.Major == 6 && Environment.OSVersion.Version.Minor >= 1)
                this.windowsTaskbar = TaskbarManager.Instance;

            this.fooTextBox1.MouseClick += new MouseEventHandler(fooTextBox1_MouseClick);
            this.fooTextBox1.MouseDoubleClick += new MouseEventHandler(fooTextBox1_MouseDoubleClick);
            this.fooTextBox1.Document.Update += new DocumentUpdateEventHandler(Document_Update);

            this.EncodeChangeEvent += new EncodeChangeEventHandler((s, e) => { });
            this.DocumentChangeTypeEvent += new DocumentTypeChangeEventHandler((s, e) => { });
            this.LinefeedTypeChangeEvent += new LinefeedTypeChangeEventHandler((s, e) => { });
        }

        private Encoding _enc;
        private LineFeedType _linefeed = LineFeedType.CRLF;
        private string _filepath,_doctype;
        private TaskbarManager windowsTaskbar;
        private int updateCount;
        private AutocompleteBox _CompleteBox;
        private Task task;

        public event EncodeChangeEventHandler EncodeChangeEvent;
        public event DocumentTypeChangeEventHandler DocumentChangeTypeEvent;
        public event LinefeedTypeChangeEventHandler LinefeedTypeChangeEvent;

        /// <summary>
        /// 保持しているシンタックスハイライトの定義
        /// </summary>
        public SyntaxDefnition SynataxDefnition
        {
            get;
            private set;
        }

        /// <summary>
        /// ドキュメントの文字コードを表す
        /// </summary>
        public Encoding enc
        {
            get { return this._enc; }
            set
            {
                this._enc = value;
                this.EncodeChangeEvent(this, new EventArgs());
            }
        }

        /// <summary>
        /// 非同期操作中なら真を返す
        /// </summary>
        public bool isBusy
        {
            get
            {
                return this.task != null &&
                    this.task.IsCompleted == false &&
                    this.task.IsCanceled == false &&
                    this.task.IsFaulted == false;
            }
        }

        /// <summary>
        /// ドキュメントの改行コードを表す
        /// </summary>
        public LineFeedType linefeed
        {
            get { return this._linefeed; }
            set
            {
                this._linefeed = value;
                this.LinefeedTypeChangeEvent(this, new EventArgs());
            }
        }

        /// <summary>
        /// ドキュメントの文章タイプを表す
        /// </summary>
        /// <remarks>
        /// nullを設定した場合ハイライト関連のプロパティもnullに設定され、オートインデントも行われなくなります
        /// </remarks>
        public string DocumentType
        {
            get { return this._doctype; }
            set
            {
                _doctype = value;
                if (value != null)
                {
                    if(RegistorKeyword(value))
                        return;
                    this.fooTextBox1.LayoutLines.HilightAll();
                }
                else
                {
                    UnRegistorKeyword();
                    this.fooTextBox1.LayoutLines.ClearHilight();
                }
                this.fooTextBox1.Refresh();
                this.DocumentChangeTypeEvent(this, new EventArgs());
            }
        }

        /// <summary>
        /// ドキュメントのファイルパスを表す
        /// </summary>
        public string filepath
        {
            get { return this._filepath; }
            set
            {
                this._filepath = value;
                this.Text = Path.GetFileName(value);
            }
        }

        /// <summary>
        /// ダーティフラグを表す
        /// </summary>
        public bool isDirty
        {
            get;
            private set;
        }

        /// <summary>
        /// 補完候補を管理する
        /// </summary>
        public AutocompleteBox CompleteBox
        {
            get { return this._CompleteBox; }
            set
            {
                if (this._CompleteBox != null)
                {
                    this._CompleteBox.ShowingCompleteBox = null;
                    this._CompleteBox.SelectItem = null;
                }
                this._CompleteBox = value;
            }
        }

        internal int GetTabCountAtCurrentLine()
        {
            int tabNum = 0;
            for (int i = this.fooTextBox1.SelectionStart; i >= 0; i++)
            {
                if (this.fooTextBox1.Document[i] == Document.NewLine)
                    break;
                else if (this.fooTextBox1.Document[i] == '\n')
                    tabNum++;
            }
            return tabNum;
        }

        /// <summary>
        /// ファイルから読み取る
        /// </summary>
        /// <param name="filepath">ファイル名</param>
        /// <param name="codepage">コードページ。NULLの場合は自動判定を行う</param>
        /// <remarks>改行コードは自動判別されます</remarks>
        public void LoadFile(string filepath, Encoding codepage)
        {
            string KeywordFileName = DeciedKeywordFileName(filepath);

            try
            {
                this.LoadPrepare(filepath, codepage);
                this.fooTextBox1.Document.Load(filepath, this.enc);
            }
            catch (UnauthorizedAccessException ex)
            {
                throw new UserOperationException(ex.Message);
            }
            catch (IOException ex)
            {
                throw new UserOperationException(ex.Message);
            }

            this.LoadComplete(KeywordFileName, filepath);
        }

        /// <summary>
        /// ファイルから読み取る
        /// </summary>
        /// <param name="filepath">ファイル名</param>
        /// <param name="codepage">コードページ。NULLの場合は自動判定を行う</param>
        /// <remarks>改行コードは自動判別されます</remarks>
        public void LoadFileAsync(string filepath, Encoding codepage)
        {
            if (this.isBusy)
                throw new InvalidOperationException();

            string KeywordFileName = DeciedKeywordFileName(filepath);

            this.fooTextBox1.Enabled = false;
            try
            {
                this.LoadPrepare(filepath, codepage);
                this.task = new Task(() => {
                    this.fooTextBox1.Document.Load(filepath, this.enc);
                });
                this.task.ContinueWith((t) => {
                    this.BeginInvoke(new Action(()=>{
                        this.LoadComplete(KeywordFileName, filepath);
                    }));
                });
                this.task.Start();
            }
            catch (UnauthorizedAccessException ex)
            {
                throw new UserOperationException(ex.Message);
            }
            catch (IOException ex)
            {
                throw new UserOperationException(ex.Message);
            }
        }

        void LoadPrepare(string filepath,Encoding codepage)
        {
            if (codepage == null)
                this.enc = DectingEncode.GetCode2(filepath);
            else
                this.enc = codepage;

            this.linefeed = DectingEncode.GetLineFeed(filepath, this.enc);
            if (this.linefeed == LineFeedType.UNKOWN)
                this.linefeed = LineFeedType.CRLF;
        }

        void LoadComplete(string KeywordFileName,string filepath)
        {
            this.filepath = filepath;
            this.DocumentType = KeywordFileName;

            Config cfg = Config.GetInstance();
            cfg.RecentFile.InsertAtFirst(filepath);

            this.fooTextBox1.Enabled = true;
            this.fooTextBox1.JumpCaret(0);
            this.fooTextBox1.Focus();
            this.fooTextBox1.Refresh();

            this.updateCount = 0;   //こうしないとユーザーの予想よりも早くオートセーブされてしまう
        }

        /// <summary>
        /// ファイルに保存する
        /// </summary>
        /// <param name="filename">ファイル名</param>
        /// <param name="backup">既に存在しているファイルを残す場合は真。そうでない場合は偽</param>
        public void SaveFile(string filename, bool backup = false)
        {
            this.SaveFileAsync(filename, backup);
            this.task.Wait();
        }

        /// <summary>
        /// ファイルに保存する
        /// </summary>
        /// <param name="filename">ファイル名</param>
        /// <param name="backup">既に存在しているファイルを残す場合は真。そうでない場合は偽</param>
        public void SaveFileAsync(string filename,bool backup = false)
        {
            if (this.isBusy)
                throw new InvalidOperationException();

            if (backup && File.Exists(filename))
            {
                string oldFilename = GetBackupFileName(filename);
                if(oldFilename != null)
                    File.Move(filename, oldFilename);
            }

            this.task = new Task(() =>
            {
                this.fooTextBox1.Document.Save(filename, this.enc, DectingEncode.StringFromLineFeedType(this.linefeed));
            });
            this.task.ContinueWith((t) =>
            {
                this.BeginInvoke(new Action(() =>
                {
                    this.filepath = filename;

                    Config cfg = Config.GetInstance();
                    cfg.RecentFile.InsertAtFirst(filename);
                }));
            });
            this.task.Start();
        }

        /// <summary>
        /// 管理者権限を使用してファイルを保存する
        /// </summary>
        /// <param name="filename">ファイル名</param>
        /// <param name="backup">既に存在しているファイルを残す場合は真。そうでない場合は偽</param>
        public void SaveFileByAdministrator(string filename, bool backup = false)
        {
            if (MessageBox.Show(Resources.UACSaveConfirm, Resources.UACSaveConfirmDialogTitle, MessageBoxButtons.YesNo) == DialogResult.No)
                return;

            string tempfile = Path.GetTempPath() + Path.GetRandomFileName();
            this.fooTextBox1.Document.Save(tempfile, this.enc, DectingEncode.StringFromLineFeedType(this.linefeed));
            this.DoAdminOperation(tempfile, filename, backup);
        }

        /// <summary>
        /// 管理者権限を使用してファイルを保存する
        /// </summary>
        /// <param name="filename">ファイル名</param>
        /// <param name="backup">既に存在しているファイルを残す場合は真。そうでない場合は偽</param>
        public void SaveFileAsyncByAdministrator(string filename, bool backup = false)
        {
            if (this.isBusy)
                throw new InvalidOperationException();

            string tempfile = Path.GetTempPath() + Path.GetRandomFileName();
            this.task = new Task(() =>
            {
                this.fooTextBox1.Document.Save(tempfile, this.enc, DectingEncode.StringFromLineFeedType(this.linefeed));
            });
            this.task.ContinueWith((t) =>
            {
                this.BeginInvoke(new Action(() =>
                {
                    this.DoAdminOperation(tempfile, filename, backup);
                }));
            });
            this.task.Start();
        }

        void DoAdminOperation(string tempfile,string filename,bool backup)
        {
            AdminiOperation operation = new AdminiOperation();

            if (backup && File.Exists(filename))
            {
                string oldFilename = GetBackupFileName(filename);
                if (oldFilename != null)
                    operation.WriteCode(string.Format("move\t{0}\t{1}", filename, oldFilename));
            }

            operation.WriteCode(string.Format("copy\t{0}\t{1}", tempfile, filename));

            bool result = false;
            try
            {
                result = operation.Execute();
            }
            catch (Win32Exception ex)  //UACのキャンセル時に発生するので
            {
                if (ex.NativeErrorCode != 1223)
                    throw;
            }
            if (result)
            {
                this.filepath = filename;
                Config cfg = Config.GetInstance();
                cfg.RecentFile.InsertAtFirst(filename);
            }
        }

        protected override void OnClosed(EventArgs e)
        {
            base.OnClosed(e);
            if(this.task != null && this.task.IsCompleted == false)
                this.task.Wait();
        }

        void SaveBackupFolder(string filename)
        {
            if (this.isBusy)
                return;
            if (filename == null)
                filename = this.Text.Trim(Resources.EditFormDirtySign[0]);
            string BackupFolderPath = Path.Combine(Config.ApplicationFolder, "Backup");
            if (Directory.Exists(BackupFolderPath) == false)
                Directory.CreateDirectory(BackupFolderPath);
            string BackupFilename = filename.Replace(Path.DirectorySeparatorChar, '_');
            BackupFilename = BackupFilename.Replace(Path.VolumeSeparatorChar, '_');
            this.task = new Task(() =>
            {
                this.fooTextBox1.Document.Save(Path.Combine(BackupFolderPath,BackupFilename), this.enc, DectingEncode.StringFromLineFeedType(this.linefeed));
            });
            this.task.Start();
        }

        string GetBackupFileName(string filename)
        {
            string directoryPart = Path.GetDirectoryName(filename);
            string filePart = Path.GetFileName(filename);
            IEnumerable<string> files = Directory.EnumerateFiles(directoryPart, filePart + "*");

            int newCount = files.Count();

            Config cfg = Config.GetInstance();
            if (newCount > cfg.MaxBackupCount)
                return null;

            return filename + "." + newCount;
        }

        private string DeciedKeywordFileName(string EditingFile)
        {
            Config cfg = Config.GetInstance();

            if (EditingFile == null)
                return Resources.DocumentModeNone;

            foreach (KeyValuePair<string, string> kv in cfg.SyntaxDefinitions)
            {
                Regex regex = new Regex(kv.Value, RegexOptions.IgnoreCase);
                Match m = regex.Match(Path.GetFileName(EditingFile));
                if (m.Success == true)
                    return kv.Key;
            }
            return Resources.DocumentModeNone;
        }

        private bool RegistorKeyword(string name)
        {
            if (name == Resources.DocumentModeNone)
                return true;

            string filepath = GetKeywordFilePath(name);

            if (filepath == null)
                return true;

            this.SynataxDefnition = new SyntaxDefnition();
            this.SynataxDefnition.generateKeywordList(filepath);

            if (this.SynataxDefnition.Hilighter == FooEditor.SyntaxDefnition.XmlHilighter)
            {
                this.fooTextBox1.Hilighter = new XmlHilighter();
            }
            else
            {
                GenericHilighter Hilighter = new GenericHilighter();
                Hilighter.KeywordManager = this.SynataxDefnition;
                this.fooTextBox1.Hilighter = Hilighter;
            }

            return false;
        }

        string GetKeywordFilePath(string name)
        {
            Config cfg = Config.GetInstance();

            string KeywordFolderName = "Keywords";
            
            string filepath = Path.Combine(Config.ApplicationFolder, KeywordFolderName, name);

            if (File.Exists(filepath))
                return filepath;
            
            filepath = Path.Combine(Application.StartupPath, KeywordFolderName, name);

            if (File.Exists(filepath))
                return filepath;

            return null;
        }

        void UnRegistorKeyword()
        {
            this.fooTextBox1.Hilighter = null;
        }

        void Document_Update(object sender, DocumentUpdateEventArgs e)
        {
            if (e.type != UpdateType.Clear)
                this.isDirty = true;
            else
                this.isDirty = false;

            char c = Resources.EditFormDirtySign[0];

            this.BeginInvoke(new Action(()=>{
                if (this.isDirty)
                {
                    if (this.Text.Last() != c)
                        this.Text = this.Text + c;
                }
                else
                {
                    this.Text = this.Text.TrimEnd(new char[] { c });
                }
            }));

            Config cfg = Config.GetInstance();
            if (e.type == UpdateType.Replace && cfg.AutoSaveCount != 0)
            {
                if (this.updateCount > cfg.AutoSaveCount)
                    this.SaveBackupFolder(this.filepath);
                else
                    this.updateCount++;
            }
        }

        void fooTextBox1_MouseClick(object sender, MouseEventArgs e)
        {
            this.Parent.Focus();
        }

        void fooTextBox1_MouseDoubleClick(object sender, MouseEventArgs e)
        {
            if (this.fooTextBox1.UrlMark == false)
                return;

            FooMouseEventArgs me = e as FooMouseEventArgs;

            foreach (Marker m in this.fooTextBox1.Document.GetMarkers(me.index))
            {
                if (m.hilight == HilightType.Url)
                {
                    ProcessStartInfo info = new ProcessStartInfo();
                    info.Arguments = "";
                    info.FileName = this.fooTextBox1.Document.ToString(m.start,m.length);
                    info.Verb = "open";
                    info.UseShellExecute = true;
                    Process process = Process.Start(info);
                    me.Handled = true;
                    break;
                }
            }
        }

        public EditForm(SerializationInfo info, StreamingContext context) : this()
        {
            this.linefeed = (LineFeedType)info.GetInt32("LineFeed");
            this.enc = Encoding.GetEncoding(info.GetString("Encoding"));
            this.DocumentType = info.GetString("DocumentType");
            this.fooTextBox1.InsertMode = info.GetBoolean("InsertMode");
            this.filepath = info.GetString("FilePath");
            this.Text = info.GetString("Title");

            int LineCount = info.GetInt32("LineCount");

            this.fooTextBox1.Document.UndoManager.BeginLock();
            this.fooTextBox1.Document.FireUpdateEvent = false;
            for (int i = 0; i < LineCount; i++)
            {
                this.fooTextBox1.Document.Append(info.GetString(i.ToString()));
            }
            this.fooTextBox1.Document.FireUpdateEvent = true;
            this.fooTextBox1.Document.UndoManager.EndLock();

            TextPoint tp = new TextPoint();
            tp.row = info.GetInt32("CaretPostionRow");
            tp.col = info.GetInt32("CaretPostionCol");
            this.fooTextBox1.JumpCaret(tp.row, tp.col);
        }

        public void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            info.AddValue("LineFeed", (int)this.linefeed);
            info.AddValue("Encoding", this.enc.WebName);
            info.AddValue("DocumentType", this._doctype);
            info.AddValue("InsertMode", this.fooTextBox1.InsertMode);
            info.AddValue("CaretPostionRow", this.fooTextBox1.CaretPostion.row);
            info.AddValue("CaretPostionCol", this.fooTextBox1.CaretPostion.col);
            info.AddValue("FilePath", this.filepath);
            info.AddValue("Title", this.Text);

            int LineCount = this.fooTextBox1.LayoutLines.Count;
            info.AddValue("LineCount", this.fooTextBox1.LayoutLines.Count);

            for (int i = 0; i < LineCount; i++)
                info.AddValue(i.ToString(), this.fooTextBox1.LayoutLines[i]);
        }
    }
}
