// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;

namespace MouseWithoutBorders
{
    public partial class FormHelper : System.Windows.Forms.Form
    {
        private readonly List<FocusArea> focusZone = new();
        private readonly Lock bmScreenLock = new();
        private long lastClipboardEventTime;

        private IClipboardHelper remoteClipboardHelper;
        private const string TEXT_TYPE_SEP = "{4CFF57F7-BEDD-43d5-AE8F-27A61E886F2F}";

        private const int MAX_TEXT_SIZE = 20 * 1024 * 1024;
        private const int MAX_IMAGE_SIZE = 50 * 1024 * 1024;

        private struct FocusArea
        {
            internal RectangleF Rec;
            internal Color Color;

            internal FocusArea(RectangleF rec, Color color)
            {
                Rec = rec;
                Color = color;
            }
        }

        private Point focus1 = Point.Empty;
        private Point focus2 = Point.Empty;

        public FormHelper()
        {
            remoteClipboardHelper = IpcHelper.CreateIpcClient();

            if (remoteClipboardHelper == null)
            {
                QuitDueToCommunicationError();
                return;
            }

            SetDPIAwareness();
            InitializeComponent();
            lastClipboardEventTime = GetTick();
            ClipboardMMHelper.HookClipboard(this);
        }

        private void SetDPIAwareness()
        {
            int setProcessDpiAwarenessResult = -1;

            try
            {
                setProcessDpiAwarenessResult = NativeMethods.SetProcessDpiAwareness(2);
                SendLog(string.Format(CultureInfo.InvariantCulture, "SetProcessDpiAwareness: {0}.", setProcessDpiAwarenessResult));
            }
            catch (DllNotFoundException)
            {
                SendLog("SetProcessDpiAwareness is unsupported in Windows 7 and lower.");
            }
            catch (Exception e)
            {
                SendLog(e.ToString());
            }

            try
            {
                if (setProcessDpiAwarenessResult != 0)
                {
                    SendLog(string.Format(CultureInfo.InvariantCulture, "SetProcessDPIAware: {0}.", NativeMethods.SetProcessDPIAware()));
                }
            }
            catch (Exception e)
            {
                SendLog(e.ToString());
            }
        }

        private bool quitDueToCommunicationError;

        private void QuitDueToCommunicationError()
        {
            if (!quitDueToCommunicationError)
            {
                quitDueToCommunicationError = true;

                if (Process.GetCurrentProcess().SessionId != NativeMethods.WTSGetActiveConsoleSessionId())
                {
                    EventLogger.LogEvent(Application.ProductName + " cannot be used in a remote desktop or virtual machine session.");
                }
                else
                {
                    _ = MessageBox.Show(
                        "Unable to connect to Mouse Without Borders process, clipboard sharing is no longer working.\r\nSee EventLog for more information.",
                        Text,
                        MessageBoxButtons.OK,
                        MessageBoxIcon.Error);
                }

                Process.GetCurrentProcess().Kill();
            }
        }

        private void FormHelper_DragEnter(object sender, DragEventArgs e)
        {
            object o = e.Data.GetData(DataFormats.FileDrop);

            if (o != null)
            {
                e.Effect = DragDropEffects.Copy;
                Array ar = (string[])o;

                if (ar.Length > 0)
                {
                    string fileName = ar.GetValue(0).ToString();
                    Hide();

                    try
                    {
                        remoteClipboardHelper.SendDragFile(fileName);
                    }
                    catch (Exception ex)
                    {
                        EventLogger.LogEvent("FormHelper_DragEnter: " + ex.Message, EventLogEntryType.Error);
                        QuitDueToCommunicationError();
                    }
                }
            }
        }

        private void TimerHelper_Tick(object sender, EventArgs e)
        {
            lock (bmScreenLock)
            {
                if (picScr == null)
                {
                    timerHelper.Stop();
                    Hide();
                }
            }
        }

        private void FormHelper_Shown(object sender, EventArgs e)
        {
            timerHelper.Start(); // To be sure.
            Hide();
        }

        internal void SendLog(string log)
        {
            try
            {
                EventLogger.LogEvent(log, EventLogEntryType.Warning);
            }
            catch (Exception e)
            {
                EventLogger.LogEvent(log + " ==> SendLog Exception: " + e.Message, EventLogEntryType.Warning);
            }
        }

        private long GetTick() // ms
        {
            return DateTime.Now.Ticks / 10000;
        }

        private string GetClipboardText()
        {
            string st = string.Empty, tmp = ClipboardMMHelper.GetText(TextDataFormat.UnicodeText);
            int txtL = 0, rtfL = 0, htmL = 0;

            if (tmp != null && (txtL = tmp.Length) > 0)
            {
                st += "TXT" + tmp + TEXT_TYPE_SEP;
            }

            tmp = ClipboardMMHelper.GetText(TextDataFormat.Rtf);

            if (tmp != null && (rtfL = tmp.Length) > 0)
            {
                st += "RTF" + tmp + TEXT_TYPE_SEP;
            }

            tmp = ClipboardMMHelper.GetText(TextDataFormat.Html);

            if (tmp != null && (htmL = tmp.Length) > 0)
            {
                st += "HTM" + tmp + TEXT_TYPE_SEP;
            }

            if (st.Length > 0)
            {
                if (st.Length > MAX_TEXT_SIZE)
                {
                    st = null;

                    SendLog(string.Format(CultureInfo.CurrentCulture, "GetClipboardText, Text too big: TXT = {0}, RTF = {1}, HTM = {2}.", txtL, rtfL, htmL));
                }
                else
                {
                    SendLog(string.Format(CultureInfo.CurrentCulture, "GetClipboardText: TXT = {0}, RTF = {1}, HTM = {2}.", txtL, rtfL, htmL));
                }
            }

            return st;
        }

#pragma warning disable CA2213 // Disposing is done by ComponentResourceManager
        private Image bmScreen;
        private Point startOrg;
        private Point start;
        private Point stop;
        private PictureBox left;
        private PictureBox top;
        private PictureBox right;
        private PictureBox bottom;
        private PictureBox picScr;
#pragma warning restore CA2213
        private int customScreenCaptureInProgress;
        private int screenLeft;
        private int screenTop;

        protected override void WndProc(ref Message m)
        {
            const int WM_DRAWCLIPBOARD = 0x0308;
            const int WM_CHANGECBCHAIN = 0x030D;
            const int WM_CLIPBOARDUPDATE = 0x031D;

            switch (m.Msg)
            {
                case WM_DRAWCLIPBOARD:
                case WM_CLIPBOARDUPDATE:
                    ClipboardMMHelper.PassMessageToTheNextViewer(m);

                    if (GetTick() - lastClipboardEventTime < 1000)
                    {
                        lastClipboardEventTime = GetTick();
                        return;
                    }

                    if (customScreenCaptureInProgress > 0)
                    {
                        // 10 secs timeout for a failed capture.
                        if (GetTick() - lastClipboardEventTime < 10000)
                        {
                            return;
                        }
                        else
                        {
                            customScreenCaptureInProgress = 0;
                        }
                    }

                    lastClipboardEventTime = GetTick();

                    ByteArrayOrString? data = null;
                    bool isFile = false;

                    if (ClipboardMMHelper.ContainsText())
                    {
                        data = GetClipboardText();
                    }
                    else if (ClipboardMMHelper.ContainsImage())
                    {
                        MemoryStream ms = new();
                        Image im = ClipboardMMHelper.GetImage();

                        if (im != null)
                        {
                            im.Save(ms, ImageFormat.Png);

                            if (ms.Length > 0)
                            {
                                if (ms.Length > MAX_IMAGE_SIZE)
                                {
                                    SendLog("Image from clipboard, image too big: " + ms.Length.ToString(CultureInfo.InvariantCulture));
                                }
                                else
                                {
                                    data = ms.GetBuffer();
                                    SendLog("Image from clipboard: " + ms.Length.ToString(CultureInfo.InvariantCulture));
                                }
                            }
                            else
                            {
                                SendLog("ClipboardMMHelper image is 0 in length.");
                            }
                        }
                        else
                        {
                            SendLog("ClipboardMMHelper image (GetImage) is null.");
                        }

                        ms.Dispose();
                    }
                    else if (ClipboardMMHelper.ContainsFileDropList())
                    {
                        StringCollection files = ClipboardMMHelper.GetFileDropList();

                        if (files != null)
                        {
                            if (files.Count > 0)
                            {
                                data = files[0];
                                isFile = true;

                                SendLog("File from clipboard: " + files[0]);
                            }
                            else
                            {
                                SendLog("GetFileDropList returned no file.");
                            }
                        }
                        else
                        {
                            SendLog("GetFileDropList returned null.");
                        }
                    }
                    else
                    {
                        SendLog("ClipboardMMHelper does not have text/image/file data.");
                        return;
                    }

                    if (data != null)
                    {
                        try
                        {
                            remoteClipboardHelper.SendClipboardData((ByteArrayOrString)data, isFile);
                        }
                        catch (Exception ex)
                        {
                            EventLogger.LogEvent("WM_DRAWCLIPBOARD: " + ex.Message, EventLogEntryType.Error);
                            QuitDueToCommunicationError();
                        }

                        GC.Collect();
                    }
                    else
                    {
                        SendLog("Null clipboard data returned. See previous messages (if any) for more information.");
                    }

                    break;

                case WM_CHANGECBCHAIN:
                    if (!ClipboardMMHelper.UpdateNextClipboardViewer(m))
                    {
                        ClipboardMMHelper.PassMessageToTheNextViewer(m);
                    }

                    break;

                case 0x401:
                    screenLeft = 0;
                    screenTop = 0;

                    foreach (Screen s in Screen.AllScreens)
                    {
                        if (s.Bounds.Left < screenLeft)
                        {
                            screenLeft = s.Bounds.Left;
                        }

                        if (s.Bounds.Top < screenTop)
                        {
                            screenTop = s.Bounds.Top;
                        }
                    }

                    customScreenCaptureInProgress = 1;
                    lastClipboardEventTime = GetTick();
                    SendLog("**************************************************\r\nScreen capture triggered.");
                    m.Result = new IntPtr(1);
                    break;

                case 0x406:
                    if (m.Msg == 0x406)
                    {
                        lock (bmScreenLock)
                        {
                            bmScreen = null;

                            for (int i = 0; i < 30; i++)
                            {
                                Thread.Sleep(100);

                                if (ClipboardMMHelper.ContainsImage())
                                {
                                    bmScreen = ClipboardMMHelper.GetImage();
                                    customScreenCaptureInProgress = 1;
                                    break;
                                }
                            }

                            if (bmScreen == null)
                            {
                                customScreenCaptureInProgress = 0;
                                SendLog("No image found in the clipboard.");
                            }
                            else
                            {
                                Opacity = 1;
                                picScr = new PictureBox();
                                picScr.Dock = DockStyle.Fill;
                                picScr.SizeMode = PictureBoxSizeMode.StretchImage;
                                picScr.Image = bmScreen;
                                picScr.Refresh();
                                Controls.Add(picScr);
                                AssignEventHandlers(picScr);
                            }
                        }
                    }

                    MouseMoveHandler();
                    break;

                case 0x407:
                    Program.DotForm.SetPosition(m.WParam.ToInt32(), m.LParam.ToInt32());
                    Program.DotForm.TopMost = true;
                    Program.DotForm.Show();
                    Application.DoEvents();

                    // Simulate input to help bring to the foreground, as it doesn't seem to work in every case otherwise.
                    NativeMethods.INPUT input = new NativeMethods.INPUT { type = (int)NativeMethods.InputType.INPUT_MOUSE, mi = { } };
                    NativeMethods.INPUT[] inputs = new NativeMethods.INPUT[] { input };
                    _ = NativeMethods.SendInput(1, inputs, Marshal.SizeOf(typeof(NativeMethods.INPUT)));
                    m.Result = SetForeGround() ? new IntPtr(1) : IntPtr.Zero;
                    break;

                case 0x408:
                    Program.DotForm.Hide();
                    break;

                case 0x400:
                    m.Result = Handle;
                    break;

                case SharedConst.QUIT_CMD:
                    Process.GetCurrentProcess().Kill();
                    break;

                default:
                    base.WndProc(ref m);
                    break;
            }
        }

        private bool SetForeGround()
        {
            string logTag = nameof(SetForeGround);
            IntPtr foreGroundWindow = NativeMethods.GetForegroundWindow();

            if (foreGroundWindow == Program.DotForm.Handle)
            {
                SendLog($"{logTag}.Foreground window is already the dot form: {foreGroundWindow}.");
                return true;
            }

            Program.DotForm.Activate();

            bool setForegroundWindow = NativeMethods.SetForegroundWindow(Program.DotForm.Handle);
            SendLog($"{logTag}.{nameof(NativeMethods.SetForegroundWindow)}({Program.DotForm.Handle}) returned: {setForegroundWindow}.");

            return LogForeGroundWindow(logTag);
        }

        private bool LogForeGroundWindow(string logTag)
        {
            IntPtr foreGroundWindow = NativeMethods.GetForegroundWindow();

            if (foreGroundWindow == Program.DotForm.Handle)
            {
                SendLog($"{logTag}.Foreground window is now the dot form: {Program.DotForm.Handle}.");
                return true;
            }
            else
            {
                SendLog($"{logTag}.Foreground window is: [{foreGroundWindow}].");
                return false;
            }
        }

        private void AssignEventHandlers(PictureBox p)
        {
            p.MouseDown += new System.Windows.Forms.MouseEventHandler(FormHelper_MouseDown);
            p.MouseMove += new System.Windows.Forms.MouseEventHandler(FormHelper_MouseMove);
            p.MouseUp += new System.Windows.Forms.MouseEventHandler(FormHelper_MouseUp);
        }

        private void MouseMoveHandler()
        {
            lock (bmScreenLock)
            {
                if (bmScreen != null && Opacity == 1)
                {
                    focusZone.Clear();
                    TopMost = true;
                    Show();

                    startOrg = new Point(MousePosition.X - screenLeft, MousePosition.Y - screenTop);

                    if (left == null)
                    {
                        left = new PictureBox();
                        left.BackColor = Color.Red;
                        Controls.Add(left);
                        left.BringToFront();
                        AssignEventHandlers(left);
                    }

                    left.Left = startOrg.X;
                    left.Top = startOrg.Y;
                    left.Width = 2;
                    left.Height = 100;
                    left.Show();
                    left.Refresh();

                    if (top == null)
                    {
                        top = new PictureBox();
                        top.BackColor = Color.Red;
                        Controls.Add(top);
                        top.BringToFront();
                        AssignEventHandlers(top);
                    }

                    top.Left = startOrg.X;
                    top.Top = startOrg.Y;
                    top.Height = 2;
                    top.Width = 100;
                    top.Show();
                    top.Refresh();
                }
            }
        }

        private void MouseDownMoveHandler()
        {
            lock (bmScreenLock)
            {
                if (bmScreen != null && left != null && top != null)
                {
                    int x = MousePosition.X - screenLeft;
                    int y = MousePosition.Y - screenTop;

                    start = new Point(startOrg.X < x ? startOrg.X : x, startOrg.Y < y ? startOrg.Y : y);
                    stop = new Point(startOrg.X > x ? startOrg.X : x, startOrg.Y > y ? startOrg.Y : y);

                    left.Left = start.X;
                    left.Top = start.Y;
                    left.Width = 2;
                    left.Height = stop.Y - start.Y;
                    left.Show();

                    top.Left = start.X;
                    top.Top = start.Y;
                    top.Height = 2;
                    top.Width = stop.X - start.X;
                    top.Show();

                    if (right == null)
                    {
                        right = new PictureBox();
                        right.BackColor = Color.Red;
                        Controls.Add(right);
                        right.BringToFront();
                        AssignEventHandlers(right);
                    }

                    right.Left = stop.X;
                    right.Top = start.Y;
                    right.Width = 2;
                    right.Height = stop.Y - start.Y;
                    right.Show();

                    if (bottom == null)
                    {
                        bottom = new PictureBox();
                        bottom.BackColor = Color.Red;
                        Controls.Add(bottom);
                        bottom.BringToFront();
                        AssignEventHandlers(bottom);
                    }

                    bottom.Left = start.X;
                    bottom.Top = stop.Y;
                    bottom.Height = 2;
                    bottom.Width = stop.X - start.X;
                    bottom.Show();

                    TopMost = true;
                    Show();
                }
            }
        }

        private void MouseUpHandler(bool canceled = false, bool rightMouseButton = false)
        {
            lock (bmScreenLock)
            {
                MouseDownMoveHandler();
                customScreenCaptureInProgress = 0;

                if (!canceled && bmScreen != null && left != null && top != null && right != null && bottom != null
                    && stop.X - start.X > 0 && stop.Y - start.Y > 0)
                {
                    start = PointInStandardDPI(start);
                    stop = PointInStandardDPI(stop);
                    Bitmap bm = new(stop.X - start.X, stop.Y - start.Y);
                    Graphics g = Graphics.FromImage(bm);

                    try
                    {
                        g.DrawImage(bmScreen, 0, 0, new Rectangle(start, bm.Size), GraphicsUnit.Pixel);
                        g.DrawRectangle(Pens.DarkOrange, 0, 0, stop.X - start.X - 1, stop.Y - start.Y - 1);

                        foreach (FocusArea f in focusZone)
                        {
                            RectangleF rec = RectangleFInStandardDPI(f.Rec);
                            rec.X -= start.X;
                            rec.Y -= start.Y;

                            g.DrawEllipse(new Pen(f.Color, 2), rec);
                        }

                        if (rightMouseButton)
                        {
                            try
                            {
                                string tempFile = Path.GetTempPath() + Path.GetFileNameWithoutExtension(Path.GetTempFileName()) + ".png";
                                SendLog(tempFile);
                                bm.Save(tempFile, ImageFormat.Png);
                                lastClipboardEventTime = GetTick();
                                ClipboardMMHelper.SetText(tempFile);
                            }
                            catch (IOException ioException)
                            {
                                SendLog("IO!Exception!: " + ioException.Message);
                            }
                            catch (ArgumentNullException argException)
                            {
                                SendLog("ArgNull!Exception!: " + argException.Message);
                            }
                            catch (ExternalException internalException)
                            {
                                SendLog("Internal!Exception!: " + internalException.Message);
                            }
                        }
                        else
                        {
                            lastClipboardEventTime = GetTick() - 10000;
                            ClipboardMMHelper.SetImage(bm);
                        }
                    }
                    finally
                    {
                        g.Dispose();
                        bm.Dispose();
                        bmScreen.Dispose();
                        bmScreen = null;
                    }
                }

                ShowInTaskbar = false;
                Hide();
                Controls.Clear();
                Opacity = 0.11;
                left?.Dispose();
                top?.Dispose();
                right?.Dispose();
                bottom?.Dispose();
                picScr?.Dispose();
                left = top = right = bottom = picScr = null;

                SendLog("Screen capture ended.\r\n**************************************************");
            }
        }

        private void FormHelper_FormClosed(object sender, FormClosedEventArgs e)
        {
            ClipboardMMHelper.UnhookClipboard();
        }

        private void FormHelper_LocationChanged(object sender, EventArgs e)
        {
            lock (bmScreenLock)
            {
                if (picScr == null && !timerHelper.Enabled)
                {
                    Opacity = 0.11;
                    timerHelper.Start();
                }
            }
        }

        private void FormHelper_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.KeyCode == Keys.Escape)
            {
                MouseUpHandler(true);
            }

            if (focus1.IsEmpty)
            {
                focus1 = stop;
            }
        }

        private void FormHelper_KeyUp(object sender, KeyEventArgs e)
        {
            focus2 = stop;

            float x = Math.Min(focus1.X, focus2.X);
            float y = Math.Min(focus1.Y, focus2.Y);
            float w = Math.Abs(focus1.X - focus2.X);
            float h = Math.Abs(focus1.Y - focus2.Y);
            x -= 0.25F * w;
            y -= 0.25F * h;
            w *= 1.5F;
            h *= 1.5F;

            focusZone.Add(new FocusArea(new RectangleF(x, y, w, h), e.KeyCode == Keys.B ? Color.Blue : e.KeyCode == Keys.G ? Color.Green : Color.Red));

            focus1 = focus2 = Point.Empty;
        }

        private void FormHelper_FormClosing(object sender, FormClosingEventArgs e)
        {
            if (e.CloseReason == CloseReason.UserClosing)
            {
                e.Cancel = true;
            }
        }

        private void FormHelper_MouseDown(object sender, MouseEventArgs e)
        {
            SendLog("Screen capture Mouse down.");
            MouseMoveHandler();
            customScreenCaptureInProgress = 2;
        }

        private void FormHelper_MouseMove(object sender, MouseEventArgs e)
        {
            if (customScreenCaptureInProgress == 1)
            {
                MouseMoveHandler();
            }
            else if (customScreenCaptureInProgress == 2)
            {
                MouseDownMoveHandler();
            }
        }

        private void FormHelper_MouseUp(object sender, MouseEventArgs e)
        {
            MouseUpHandler(false, e.Button == MouseButtons.Right);
        }

        private Point PointInStandardDPI(Point p)
        {
            // Since the process is DPI awareness, just log and return.
            // TODO: Test in Win8/7/XP.
            SendLog(string.Format(CultureInfo.CurrentCulture, "this.Width={0}, this.Height={1}, bmScreen.Width={2}, bmScreen.Height={3}, x={4}, y={5}", Width, Height, bmScreen.Width, bmScreen.Height, p.X, p.Y));
            return p;
        }

        private RectangleF RectangleFInStandardDPI(RectangleF r)
        {
            // Since the process is DPI awareness, just return.
            return r;
        }
    }
}
