using CefSharp;
using CefSharp.WinForms;
using System;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
using TweetDuck.Configuration;
using TweetDuck.Core.Bridge;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Handling;
using TweetDuck.Core.Notification;
using TweetDuck.Core.Notification.Screenshot;
using TweetDuck.Core.Other;
using TweetDuck.Core.Other.Settings;
using TweetDuck.Core.Utils;
using TweetDuck.Plugins;
using TweetDuck.Plugins.Enums;
using TweetDuck.Plugins.Events;
using TweetDuck.Resources;
using TweetDuck.Updates;
using TweetDuck.Updates.Events;
using TweetLib.Audio;

namespace TweetDuck.Core{
    sealed partial class FormBrowser : Form{
        private static UserConfig Config => Program.UserConfig;

        public bool IsWaiting{
            set{
                if (value){
                    browser.Enabled = false;
                    Cursor = Cursors.WaitCursor;
                }
                else{
                    browser.Enabled = true;
                    Cursor = Cursors.Default;

                    if (Focused){ // re-focus browser only if the window or a child is activated
                        browser.Focus();
                    }
                }
            }
        }

        public string UpdateInstallerPath { get; private set; }

        private readonly ChromiumWebBrowser browser;
        private readonly PluginManager plugins;
        private readonly UpdateHandler updates;
        private readonly FormNotificationTweet notification;
        private readonly ContextMenu contextMenu;
        private readonly MemoryUsageTracker memoryUsageTracker;

        private bool isLoaded;
        private bool isBrowserReady;
        private FormWindowState prevState;

        private TweetScreenshotManager notificationScreenshotManager;
        private SoundNotification soundNotification;

        public FormBrowser(PluginManager pluginManager, UpdaterSettings updaterSettings){
            InitializeComponent();

            Text = Program.BrandName;

            this.plugins = pluginManager;
            this.plugins.Reloaded += plugins_Reloaded;
            this.plugins.PluginChangedState += plugins_PluginChangedState;

            this.contextMenu = ContextMenuBrowser.CreateMenu(this);
            this.memoryUsageTracker = new MemoryUsageTracker("TDGF_tryRunCleanup");

            this.notification = new FormNotificationTweet(this, plugins){
                #if DEBUG
                CanMoveWindow = () => (ModifierKeys & Keys.Alt) == Keys.Alt
                #else
                CanMoveWindow = () => false
                #endif
            };

            this.notification.Show();

            this.browser = new ChromiumWebBrowser("https://tweetdeck.twitter.com/"){
                MenuHandler = new ContextMenuBrowser(this),
                JsDialogHandler = new JavaScriptDialogHandler(),
                LifeSpanHandler = new LifeSpanHandler(),
                RequestHandler = new RequestHandlerBrowser()
            };

            #if DEBUG
            this.browser.ConsoleMessage += BrowserUtils.HandleConsoleMessage;
            #endif

            this.browser.LoadingStateChanged += browser_LoadingStateChanged;
            this.browser.FrameLoadStart += browser_FrameLoadStart;
            this.browser.FrameLoadEnd += browser_FrameLoadEnd;
            this.browser.LoadError += browser_LoadError;
            this.browser.RegisterAsyncJsObject("$TD", new TweetDeckBridge(this, notification));
            this.browser.RegisterAsyncJsObject("$TDP", plugins.Bridge);
            
            browser.BrowserSettings.BackgroundColor = (uint)TwitterUtils.BackgroundColor.ToArgb();
            browser.Dock = DockStyle.None;
            browser.Location = ControlExtensions.InvisibleLocation;
            Controls.Add(browser);

            Controls.Add(new MenuStrip{ Visible = false }); // fixes Alt freezing the program in Win 10 Anniversary Update

            Disposed += (sender, args) => {
                memoryUsageTracker.Dispose();

                browser.Dispose();
                contextMenu.Dispose();

                notificationScreenshotManager?.Dispose();
                soundNotification?.Dispose();
            };

            this.trayIcon.ClickRestore += trayIcon_ClickRestore;
            this.trayIcon.ClickClose += trayIcon_ClickClose;
            Config.TrayBehaviorChanged += Config_TrayBehaviorChanged;

            UpdateTrayIcon();

            Config.MuteToggled += Config_MuteToggled;
            Config.ZoomLevelChanged += Config_ZoomLevelChanged;

            this.updates = new UpdateHandler(browser, updaterSettings);
            this.updates.UpdateAccepted += updates_UpdateAccepted;
            this.updates.UpdateDismissed += updates_UpdateDismissed;

            RestoreWindow();
        }

        private bool TryBringToFront<T>() where T : Form{
            T form = Application.OpenForms.OfType<T>().FirstOrDefault();

            if (form != null){
                form.BringToFront();
                return true;
            }
            else return false;
        }

        private void ShowChildForm(Form form){
            form.VisibleChanged += (sender, args) => form.MoveToCenter(this);
            form.Show(this);
        }

        public void ForceClose(){
            trayIcon.Visible = false; // checked in FormClosing event
            Close();
        }

        // window setup

        private void RestoreWindow(){
            Config.BrowserWindow.Restore(this, true);
            prevState = WindowState;
            isLoaded = true;
        }

        private void OnBrowserReady(){
            if (!isBrowserReady){
                browser.Location = Point.Empty;
                browser.Dock = DockStyle.Fill;
                isBrowserReady = true;
            }
        }

        private void UpdateTrayIcon(){
            trayIcon.Visible = Config.TrayBehavior.ShouldDisplayIcon();
        }

        // active event handlers

        private void browser_LoadingStateChanged(object sender, LoadingStateChangedEventArgs e){
            if (!e.IsLoading){
                foreach(string word in TwitterUtils.DictionaryWords){
                    browser.AddWordToDictionary(word);
                }

                BeginInvoke(new Action(OnBrowserReady));
                browser.LoadingStateChanged -= browser_LoadingStateChanged;
            }
        }

        private void browser_FrameLoadStart(object sender, FrameLoadStartEventArgs e){
            if (e.Frame.IsMain){
                memoryUsageTracker.Stop();

                if (Config.ZoomLevel != 100){
                    BrowserUtils.SetZoomLevel(browser.GetBrowser(), Config.ZoomLevel);
                }

                if (TwitterUtils.IsTwitterWebsite(e.Frame)){
                    ScriptLoader.ExecuteFile(e.Frame, "twitter.js");
                }
            }
        }

        private void browser_FrameLoadEnd(object sender, FrameLoadEndEventArgs e){
            if (e.Frame.IsMain && TwitterUtils.IsTweetDeckWebsite(e.Frame)){
                e.Frame.ExecuteJavaScriptAsync(TwitterUtils.BackgroundColorFix);

                UpdateProperties(PropertyBridge.Properties.AllBrowser);
                ScriptLoader.ExecuteFile(e.Frame, "code.js");
                ReinjectCustomCSS(Config.CustomBrowserCSS);

                if (plugins.HasAnyPlugin(PluginEnvironment.Browser)){
                    ScriptLoader.ExecuteFile(e.Frame, PluginManager.PluginBrowserScriptFile);
                    ScriptLoader.ExecuteFile(e.Frame, PluginManager.PluginGlobalScriptFile);
                    plugins.ExecutePlugins(e.Frame, PluginEnvironment.Browser, true);
                }

                TweetDeckBridge.ResetStaticProperties();

                if (Config.EnableBrowserGCReload){
                    memoryUsageTracker.Start(this, e.Browser, Config.BrowserMemoryThreshold);
                }
            }
        }

        private void browser_LoadError(object sender, LoadErrorEventArgs e){
            if (e.ErrorCode == CefErrorCode.Aborted){
                return;
            }

            if (!e.FailedUrl.StartsWith("http://td/", StringComparison.Ordinal)){
                string errorPage = ScriptLoader.LoadResource("pages/error.html", true);

                if (errorPage != null){
                    browser.LoadHtml(errorPage.Replace("{err}", BrowserUtils.GetErrorName(e.ErrorCode)), "http://td/error");
                }
            }
        }

        private void timerResize_Tick(object sender, EventArgs e){
            FormBrowser_ResizeEnd(this, e); // also stops timer
        }

        private void FormBrowser_Activated(object sender, EventArgs e){
            if (!isLoaded)return;

            trayIcon.HasNotifications = false;

            if (!browser.Enabled){      // when taking a screenshot, the window is unfocused and
                browser.Enabled = true; // the browser is disabled; if the user clicks back into
            }                           // the window, enable the browser again
        }

        private void FormBrowser_LocationChanged(object sender, EventArgs e){
            if (!isLoaded)return;
            
            timerResize.Stop();
            timerResize.Start();
        }

        private void FormBrowser_Resize(object sender, EventArgs e){
            if (!isLoaded)return;

            if (WindowState != prevState){
                prevState = WindowState;

                if (WindowState == FormWindowState.Minimized){
                    if (Config.TrayBehavior.ShouldHideOnMinimize()){
                        Hide(); // hides taskbar too?! welp that works I guess
                    }
                }
                else{
                    FormBrowser_ResizeEnd(sender, e);
                }
            }
            else{
                timerResize.Stop();
                timerResize.Start();
            }
        }

        private void FormBrowser_ResizeEnd(object sender, EventArgs e){ // also triggers when the window moves
            if (!isLoaded)return;

            timerResize.Stop();
            
            if (Location != ControlExtensions.InvisibleLocation){
                Config.BrowserWindow.Save(this);
                Config.Save();
            }
        }

        private void FormBrowser_FormClosing(object sender, FormClosingEventArgs e){
            if (!isLoaded)return;

            if (Config.TrayBehavior.ShouldHideOnClose() && trayIcon.Visible && e.CloseReason == CloseReason.UserClosing){
                Hide(); // hides taskbar too?! welp that works I guess
                e.Cancel = true;
            }
        }

        private void FormBrowser_FormClosed(object sender, FormClosedEventArgs e){
            if (isLoaded && UpdateInstallerPath == null){
                updates.CleanupDownload();
            }
        }

        private void Config_MuteToggled(object sender, EventArgs e){
            UpdateProperties(PropertyBridge.Properties.MuteNotifications);
        }

        private void Config_ZoomLevelChanged(object sender, EventArgs e){
            BrowserUtils.SetZoomLevel(browser.GetBrowser(), Config.ZoomLevel);
        }

        private void Config_TrayBehaviorChanged(object sender, EventArgs e){
            UpdateTrayIcon();
        }

        private void trayIcon_ClickRestore(object sender, EventArgs e){
            Show();
            RestoreWindow();
            Activate();
            UpdateTrayIcon();
        }

        private void trayIcon_ClickClose(object sender, EventArgs e){
            ForceClose();
        }
        
        private void plugins_Reloaded(object sender, PluginErrorEventArgs e){
            browser.GetBrowser().Reload();
        }

        private void plugins_PluginChangedState(object sender, PluginChangedStateEventArgs e){
            browser.ExecuteScriptAsync("window.TDPF_setPluginState", e.Plugin, e.IsEnabled);
        }

        private void updates_UpdateAccepted(object sender, UpdateAcceptedEventArgs e){
            this.InvokeAsyncSafe(() => {
                foreach(Form form in Application.OpenForms.Cast<Form>().Reverse()){
                    if (form is FormSettings || form is FormPlugins || form is FormAbout){
                        form.Close();
                    }
                }
            
                updates.BeginUpdateDownload(this, e.UpdateInfo, update => {
                    if (update.DownloadStatus == UpdateDownloadStatus.Done){
                        UpdateInstallerPath = update.InstallerPath;
                    }

                    ForceClose();
                });
            });
        }

        private void updates_UpdateDismissed(object sender, UpdateDismissedEventArgs e){
            this.InvokeAsyncSafe(() => {
                Config.DismissedUpdate = e.VersionTag;
                Config.Save();
            });
        }

        private void soundNotification_PlaybackError(object sender, PlaybackErrorEventArgs e){
            e.Ignore = true;

            using(FormMessage form = new FormMessage("Notification Sound Error", "Could not play custom notification sound.\n"+e.Message, MessageBoxIcon.Error)){
                form.AddButton(FormMessage.Ignore, ControlType.Cancel | ControlType.Focused);

                Button btnOpenSettings = form.AddButton("View Options");
                btnOpenSettings.Width += 16;
                btnOpenSettings.Location = new Point(btnOpenSettings.Location.X-16, btnOpenSettings.Location.Y);

                if (form.ShowDialog() == DialogResult.OK && form.ClickedButton == btnOpenSettings){
                    OpenSettings(typeof(TabSettingsSounds));
                }
            }
        }

        protected override void WndProc(ref Message m){
            if (isLoaded){
                if (m.Msg == Program.WindowRestoreMessage){
                    if (WindowsUtils.CurrentProcessID == m.WParam.ToInt32()){
                        trayIcon_ClickRestore(trayIcon, new EventArgs());
                    }

                    return;
                }
                else if (m.Msg == Program.SubProcessMessage){
                    int processId = m.WParam.ToInt32();

                    if (WindowsUtils.IsChildProcess(processId)){ // child process is checked in two places for safety
                        BrowserProcesses.Link(m.LParam.ToInt32(), processId);
                    }

                    return;
                }
            }
            
            if (isBrowserReady && m.Msg == NativeMethods.WM_PARENTNOTIFY && (m.WParam.ToInt32() & 0xFFFF) == NativeMethods.WM_XBUTTONDOWN){
                browser.ExecuteScriptAsync("TDGF_onMouseClickExtra", (m.WParam.ToInt32() >> 16) & 0xFFFF);
                return;
            }

            base.WndProc(ref m);
        }

        // notification helpers

        public FormNotificationMain CreateNotificationForm(bool enableContextMenu){
            return new FormNotificationMain(this, plugins, enableContextMenu);
        }

        public void PauseNotification(){
            notification.PauseNotification();
        }

        public void ResumeNotification(){
            notification.ResumeNotification();
        }

        // javascript calls

        public void ReinjectCustomCSS(string css){
            browser.ExecuteScriptAsync("TDGF_reinjectCustomCSS", css?.Replace(Environment.NewLine, " ") ?? string.Empty);
        }

        public void UpdateProperties(PropertyBridge.Properties properties){
            browser.ExecuteScriptAsync(PropertyBridge.GenerateScript(properties));
        }

        public void ReloadToTweetDeck(){
            browser.ExecuteScriptAsync($"gc&&gc();window.location.href='{TwitterUtils.TweetDeckURL}'");
        }

        // callback handlers

        public void OpenContextMenu(){
            contextMenu.Show(this, PointToClient(Cursor.Position));
        }

        public void OpenSettings(){
            OpenSettings(null);
        }

        public void OpenSettings(Type startTab){
            if (!TryBringToFront<FormSettings>()){
                bool prevEnableUpdateCheck = Config.EnableUpdateCheck;

                FormSettings form = new FormSettings(this, plugins, updates, startTab);

                form.FormClosed += (sender, args) => {
                    if (!prevEnableUpdateCheck && Config.EnableUpdateCheck){
                        updates.DismissUpdate(string.Empty);
                        updates.Check(false);
                    }

                    if (!Config.EnableTrayHighlight){
                        trayIcon.HasNotifications = false;
                    }

                    if (Config.EnableBrowserGCReload){
                        memoryUsageTracker.Start(this, browser.GetBrowser(), Config.BrowserMemoryThreshold);
                    }
                    else{
                        memoryUsageTracker.Stop();
                    }
                    
                    UpdateProperties(PropertyBridge.Properties.ExpandLinksOnHover | PropertyBridge.Properties.SwitchAccountSelectors | PropertyBridge.Properties.HasCustomNotificationSound);

                    notification.RequiresResize = true;
                    form.Dispose();
                };

                ShowChildForm(form);
            }
        }

        public void OpenAbout(){
            if (!TryBringToFront<FormAbout>()){
                ShowChildForm(new FormAbout());
            }
        }

        public void OpenPlugins(){
            if (!TryBringToFront<FormPlugins>()){
                ShowChildForm(new FormPlugins(plugins));
            }
        }

        public void OnTweetNotification(){ // may be called multiple times, once for each type of notification
            if (Config.EnableTrayHighlight && !ContainsFocus){
                trayIcon.HasNotifications = true;
            }
        }

        public void PlayNotificationSound(){
            if (Config.NotificationSoundPath.Length == 0){
                return;
            }

            if (soundNotification == null){
                soundNotification = new SoundNotification();
                soundNotification.PlaybackError += soundNotification_PlaybackError;
            }

            soundNotification.Play(Config.NotificationSoundPath);
        }

        public void OnTweetScreenshotReady(string html, int width, int height){
            if (notificationScreenshotManager == null){
                notificationScreenshotManager = new TweetScreenshotManager(this, plugins);
            }

            notificationScreenshotManager.Trigger(html, width, height);
        }

        public void DisplayTooltip(string text){
            if (string.IsNullOrEmpty(text)){
                toolTip.Hide(this);
            }
            else{
                Point position = PointToClient(Cursor.Position);
                position.Offset(20, 10);
                toolTip.Show(text, this, position);
            }
        }

        public void TriggerTweetScreenshot(){
            browser.ExecuteScriptAsync("TDGF_triggerScreenshot()");
        }
    }
}