/*
 * Greenshot - a free and open source screenshot tool
 * Copyright (C) 2007-2015 Thomas Braun, Jens Klingen, Robin Krom
 *
 * For more information see: http://getgreenshot.org/
 * The Greenshot project is hosted on Sourceforge: http://sourceforge.net/projects/greenshot/
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 1 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

using Greenshot.IniFile;
using Greenshot.Plugin;
using GreenshotPlugin.UnmanagedHelpers;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Windows.Forms;

namespace GreenshotPlugin.Core
{
    /// <summary>
    /// Description of ClipboardHelper.
    /// </summary>
    public static class ClipboardHelper
    {
        private static readonly Object clipboardLockObject = new Object();
        private static readonly CoreConfiguration config = IniConfig.GetIniSection<CoreConfiguration>();
        private static readonly string FORMAT_FILECONTENTS = "FileContents";
        private static readonly string FORMAT_PNG = "PNG";
        private static readonly string FORMAT_PNG_OFFICEART = "PNG+Office Art";
        private static readonly string FORMAT_17 = "Format17";
        private static readonly string FORMAT_JPG = "JPG";
        private static readonly string FORMAT_JFIF = "JFIF";
        private static readonly string FORMAT_JFIF_OFFICEART = "JFIF+Office Art";
        private static readonly string FORMAT_GIF = "GIF";
        private static readonly string FORMAT_BITMAP = "System.Drawing.Bitmap";
        //private static readonly string FORMAT_HTML = "HTML Format";

        private static IntPtr nextClipboardViewer = IntPtr.Zero;
        // Template for the HTML Text on the clipboard
        // see: http://msdn.microsoft.com/en-us/library/ms649015%28v=vs.85%29.aspx
        // or:  http://msdn.microsoft.com/en-us/library/Aa767917.aspx
        private const string HTML_CLIPBOARD_STRING = @"Version:0.9
StartHTML:<<<<<<<1
EndHTML:<<<<<<<2
StartFragment:<<<<<<<3
EndFragment:<<<<<<<4
StartSelection:<<<<<<<3
EndSelection:<<<<<<<4
<!DOCTYPE>
<HTML>
<HEAD>
<TITLE>Greenshot capture</TITLE>
</HEAD>
<BODY>
<!--StartFragment -->
<img border='0' src='file:///${file}' width='${width}' height='${height}'>
<!--EndFragment -->
</BODY>
</HTML>";
        private const string HTML_CLIPBOARD_BASE64_STRING = @"Version:0.9
StartHTML:<<<<<<<1
EndHTML:<<<<<<<2
StartFragment:<<<<<<<3
EndFragment:<<<<<<<4
StartSelection:<<<<<<<3
EndSelection:<<<<<<<4
<!DOCTYPE>
<HTML>
<HEAD>
<TITLE>Greenshot capture</TITLE>
</HEAD>
<BODY>
<!--StartFragment -->
<img border='0' src='data:image/${format};base64,${data}' width='${width}' height='${height}'>
<!--EndFragment -->
</BODY>
</HTML>";

        /// <summary>
        /// Get the current "ClipboardOwner" but only if it isn't us!
        /// </summary>
        /// <returns>current clipboard owner</returns>
        private static string GetClipboardOwner()
        {
            string owner = null;
            try
            {
                IntPtr hWnd = User32.GetClipboardOwner();
                if (hWnd != IntPtr.Zero)
                {
                    int pid;
                    User32.GetWindowThreadProcessId(hWnd, out pid);
                    using (Process me = Process.GetCurrentProcess())
                    using (Process ownerProcess = Process.GetProcessById(pid))
                    {
                        // Exclude myself
                        if (ownerProcess != null && me.Id != ownerProcess.Id)
                        {
                            // Get Process Name
                            owner = ownerProcess.ProcessName;
                            // Try to get the starting Process Filename, this might fail.
                            try
                            {
                                owner = ownerProcess.Modules[0].FileName;
                            }
                            catch (Exception)
                            {
                            }
                        }
                    }
                }
            }
            catch (Exception e)
            {
                LOG.Warn("Non critical error: Couldn't get clipboard owner.", e);
            }
            return owner;
        }

        /// <summary>
        /// The SetDataObject will lock/try/catch clipboard operations making it save and not show exceptions.
        /// The bool "copy" is used to decided if the information stays on the clipboard after exit.
        /// </summary>
        /// <param name="ido"></param>
        /// <param name="copy"></param>
        private static void SetDataObject(IDataObject ido, bool copy)
        {
            lock (clipboardLockObject)
            {
                int retryCount = 5;
                while (retryCount >= 0)
                {
                    try
                    {
                        Clipboard.SetDataObject(ido, copy);
                        break;
                    }
                    catch (Exception ee)
                    {
                        if (retryCount == 0)
                        {
                            string messageText = null;
                            string clipboardOwner = GetClipboardOwner();
                            if (clipboardOwner != null)
                            {
                                messageText = string.Format("Greenshot wasn't able to write to the clipboard as the process {0} blocked the access.", clipboardOwner);
                            }
                            else
                            {
                                messageText = "An unexpected error occured while writing to the clipboard.";
                            }
                            LOG.Error(messageText, ee);
                        }
                        else
                        {
                            Thread.Sleep(100);
                        }
                    }
                    finally
                    {
                        --retryCount;
                    }
                }
            }
        }

        /// <summary>
        /// The GetDataObject will lock/try/catch clipboard operations making it save and not show exceptions.
        /// </summary>
        public static IDataObject GetDataObject()
        {
            lock (clipboardLockObject)
            {
                int retryCount = 2;
                while (retryCount >= 0)
                {
                    try
                    {
                        return Clipboard.GetDataObject();
                    }
                    catch (Exception ee)
                    {
                        if (retryCount == 0)
                        {
                            string messageText = null;
                            string clipboardOwner = GetClipboardOwner();
                            if (clipboardOwner != null)
                            {
                                messageText = string.Format("Greenshot wasn't able to write to the clipboard as the process {0} blocked the access.", clipboardOwner);
                            }
                            else
                            {
                                messageText = "An unexpected error occured while writing to the clipboard.";
                            }
                            LOG.Error(messageText, ee);
                        }
                        else
                        {
                            Thread.Sleep(100);
                        }
                    }
                    finally
                    {
                        --retryCount;
                    }
                }
            }
            return null;
        }

        /// <summary>
        /// Wrapper for Clipboard.ContainsText, Created for Bug #3432313
        /// </summary>
        /// <returns>boolean if there is text on the clipboard</returns>
        public static bool ContainsText()
        {
            IDataObject clipboardData = GetDataObject();
            return ContainsText(clipboardData);
        }

        /// <summary>
        /// Test if the IDataObject contains Text
        /// </summary>
        /// <param name="dataObject"></param>
        /// <returns></returns>
        public static bool ContainsText(IDataObject dataObject)
        {
            if (dataObject != null)
            {
                if (dataObject.GetDataPresent(DataFormats.Text) || dataObject.GetDataPresent(DataFormats.UnicodeText))
                {
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// Wrapper for Clipboard.ContainsImage, specialized for Greenshot, Created for Bug #3432313
        /// </summary>
        /// <returns>boolean if there is an image on the clipboard</returns>
        public static bool ContainsImage()
        {
            IDataObject clipboardData = GetDataObject();
            return ContainsImage(clipboardData);
        }

        /// <summary>
        /// Check if the IDataObject has an image
        /// </summary>
        /// <param name="dataObject"></param>
        /// <returns>true if an image is there</returns>
        public static bool ContainsImage(IDataObject dataObject)
        {
            if (dataObject != null)
            {
                if (dataObject.GetDataPresent(DataFormats.Bitmap)
                    || dataObject.GetDataPresent(DataFormats.Dib)
                    || dataObject.GetDataPresent(DataFormats.Tiff)
                    || dataObject.GetDataPresent(DataFormats.EnhancedMetafile)
                    || dataObject.GetDataPresent(FORMAT_PNG)
                    || dataObject.GetDataPresent(FORMAT_17)
                    || dataObject.GetDataPresent(FORMAT_JPG)
                    || dataObject.GetDataPresent(FORMAT_GIF))
                {
                    return true;
                }
                List<string> imageFiles = GetImageFilenames(dataObject);
                if (imageFiles != null && imageFiles.Count > 0)
                {
                    return true;
                }
                if (dataObject.GetDataPresent(FORMAT_FILECONTENTS))
                {
                    try
                    {
                        MemoryStream imageStream = dataObject.GetData(FORMAT_FILECONTENTS) as MemoryStream;
                        if (isValidStream(imageStream))
                        {
                            using (Image tmpImage = Image.FromStream(imageStream))
                            {
                                // If we get here, there is an image
                                return true;
                            }
                        }
                    }
                    catch (Exception)
                    {
                    }
                }
            }
            return false;
        }

        /// <summary>
        /// Simple helper to check the stream
        /// </summary>
        /// <param name="memoryStream"></param>
        /// <returns></returns>
        private static bool isValidStream(MemoryStream memoryStream)
        {
            return memoryStream != null && memoryStream.Length > 0;
        }

        /// <summary>
        /// Wrapper for Clipboard.GetImage, Created for Bug #3432313
        /// </summary>
        /// <returns>Image if there is an image on the clipboard</returns>
        public static Image GetImage()
        {
            IDataObject clipboardData = GetDataObject();
            // Return the first image
            foreach (Image clipboardImage in GetImages(clipboardData))
            {
                return clipboardImage;
            }
            return null;
        }

        /// <summary>
        /// Get all images (multiple if filenames are available) from the dataObject
        /// Returned images must be disposed by the calling code!
        /// </summary>
        /// <param name="dataObject"></param>
        /// <returns>IEnumerable<Image>
        public static IEnumerable<Image> GetImages(IDataObject dataObject)
        {
            // Get single image, this takes the "best" match
            Image singleImage = GetImage(dataObject);
            if (singleImage != null)
            {
                LOG.InfoFormat("Got image from clipboard with size {0} and format {1}", singleImage.Size, singleImage.PixelFormat);
                yield return singleImage;
            }
            else
            {
                // check if files are supplied
                List<string> imageFiles = GetImageFilenames(dataObject);
                if (imageFiles != null)
                {
                    foreach (string imageFile in imageFiles)
                    {
                        Image returnImage = null;
                        try
                        {
                            returnImage = ImageHelper.LoadImage(imageFile);
                        }
                        catch (Exception streamImageEx)
                        {
                            LOG.Error("Problem retrieving Image from clipboard.", streamImageEx);
                        }
                        if (returnImage != null)
                        {
                            LOG.InfoFormat("Got image from clipboard with size {0} and format {1}", returnImage.Size, returnImage.PixelFormat);
                            yield return returnImage;
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Get an Image from the IDataObject, don't check for FileDrop
        /// </summary>
        /// <param name="dataObject"></param>
        /// <returns>Image or null</returns>
        private static Image GetImage(IDataObject dataObject)
        {
            Image returnImage = null;
            if (dataObject != null)
            {
                IList<string> formats = GetFormats(dataObject);
                string[] retrieveFormats;

                // Found a weird bug, where PNG's from Outlook 2010 are clipped
                // So I build some special logik to get the best format:
                if (formats != null && formats.Contains(FORMAT_PNG_OFFICEART) && formats.Contains(DataFormats.Dib))
                {
                    // Outlook ??
                    LOG.Info("Most likely the current clipboard contents come from Outlook, as this has a problem with PNG and others we place the DIB format to the front...");
                    retrieveFormats = new[] { DataFormats.Dib, FORMAT_BITMAP, FORMAT_FILECONTENTS, FORMAT_PNG_OFFICEART, FORMAT_PNG, FORMAT_JFIF_OFFICEART, FORMAT_JPG, FORMAT_JFIF, DataFormats.Tiff, FORMAT_GIF };
                }
                else
                {
                    retrieveFormats = new[] { FORMAT_PNG_OFFICEART, FORMAT_PNG, FORMAT_17, FORMAT_JFIF_OFFICEART, FORMAT_JPG, FORMAT_JFIF, DataFormats.Tiff, DataFormats.Dib, FORMAT_BITMAP, FORMAT_FILECONTENTS, FORMAT_GIF };
                }
                foreach (string currentFormat in retrieveFormats)
                {
                    if (formats.Contains(currentFormat))
                    {
                        LOG.InfoFormat("Found {0}, trying to retrieve.", currentFormat);
                        returnImage = GetImageForFormat(currentFormat, dataObject);
                    }
                    else
                    {
                        LOG.DebugFormat("Couldn't find format {0}.", currentFormat);
                    }
                    if (returnImage != null)
                    {
                        ImageHelper.Orientate(returnImage);
                        return returnImage;
                    }
                }
            }
            return null;
        }

        /// <summary>
        /// Helper method to try to get an image in the specified format from the dataObject
        /// the DIB readed should solve the issue reported here: https://sourceforge.net/projects/greenshot/forums/forum/676083/topic/6354353/index/page/1
        /// It also supports Format17/DibV5, by using the following information: http://stackoverflow.com/a/14335591
        /// </summary>
        /// <param name="format">string with the format</param>
        /// <param name="dataObject">IDataObject</param>
        /// <returns>Image or null</returns>
        private static Image GetImageForFormat(string format, IDataObject dataObject)
        {
            object clipboardObject = GetFromDataObject(dataObject, format);
            MemoryStream imageStream = clipboardObject as MemoryStream;
            if (!isValidStream(imageStream))
            {
                // add "HTML Format" support here...
                return clipboardObject as Image;
            }
            else
            {
                if (config.EnableSpecialDIBClipboardReader)
                {
                    if (format == FORMAT_17 || format == DataFormats.Dib)
                    {
                        LOG.Info("Found DIB stream, trying to process it.");
                        try
                        {
                            byte[] dibBuffer = new byte[imageStream.Length];
                            imageStream.Read(dibBuffer, 0, dibBuffer.Length);
                            BITMAPINFOHEADER infoHeader = BinaryStructHelper.FromByteArray<BITMAPINFOHEADER>(dibBuffer);
                            if (!infoHeader.IsDibV5)
                            {
                                LOG.InfoFormat("Using special DIB <v5 format reader with biCompression {0}", infoHeader.biCompression);
                                int fileHeaderSize = Marshal.SizeOf(typeof(BITMAPFILEHEADER));
                                uint infoHeaderSize = infoHeader.biSize;
                                int fileSize = (int)(fileHeaderSize + infoHeader.biSize + infoHeader.biSizeImage);

                                BITMAPFILEHEADER fileHeader = new BITMAPFILEHEADER();
                                fileHeader.bfType = BITMAPFILEHEADER.BM;
                                fileHeader.bfSize = fileSize;
                                fileHeader.bfReserved1 = 0;
                                fileHeader.bfReserved2 = 0;
                                fileHeader.bfOffBits = (int)(fileHeaderSize + infoHeaderSize + infoHeader.biClrUsed * 4);

                                byte[] fileHeaderBytes = BinaryStructHelper.ToByteArray<BITMAPFILEHEADER>(fileHeader);

                                using (MemoryStream bitmapStream = new MemoryStream())
                                {
                                    bitmapStream.Write(fileHeaderBytes, 0, fileHeaderSize);
                                    bitmapStream.Write(dibBuffer, 0, dibBuffer.Length);
                                    bitmapStream.Seek(0, SeekOrigin.Begin);
                                    using (Image tmpImage = Image.FromStream(bitmapStream))
                                    {
                                        if (tmpImage != null)
                                        {
                                            return ImageHelper.Clone(tmpImage);
                                        }
                                    }
                                }
                            }
                            else
                            {
                                LOG.Info("Using special DIBV5 / Format17 format reader");
                                // CF_DIBV5
                                IntPtr gcHandle = IntPtr.Zero;
                                try
                                {
                                    GCHandle handle = GCHandle.Alloc(dibBuffer, GCHandleType.Pinned);
                                    gcHandle = GCHandle.ToIntPtr(handle);
                                    return new Bitmap(infoHeader.biWidth, infoHeader.biHeight, -(int)(infoHeader.biSizeImage / infoHeader.biHeight),
                                        infoHeader.biBitCount == 32 ? PixelFormat.Format32bppArgb : PixelFormat.Format24bppRgb,
                                        new IntPtr(handle.AddrOfPinnedObject().ToInt32() + infoHeader.OffsetToPixels + (infoHeader.biHeight - 1) * (int)(infoHeader.biSizeImage / infoHeader.biHeight)));
                                }
                                catch (Exception ex)
                                {
                                    LOG.Error("Problem retrieving Format17 from clipboard.", ex);
                                }
                                finally
                                {
                                    if (gcHandle == IntPtr.Zero)
                                    {
                                        GCHandle.FromIntPtr(gcHandle).Free();
                                    }
                                }
                            }
                        }
                        catch (Exception dibEx)
                        {
                            LOG.Error("Problem retrieving DIB from clipboard.", dibEx);
                        }
                    }
                }
                else
                {
                    LOG.Info("Skipping special DIB format reader as it's disabled in the configuration.");
                }
                try
                {
                    imageStream.Seek(0, SeekOrigin.Begin);
                    using (Image tmpImage = Image.FromStream(imageStream, true, true))
                    {
                        if (tmpImage != null)
                        {
                            LOG.InfoFormat("Got image with clipboard format {0} from the clipboard.", format);
                            return ImageHelper.Clone(tmpImage);
                        }
                    }
                }
                catch (Exception streamImageEx)
                {
                    LOG.Error(string.Format("Problem retrieving {0} from clipboard.", format), streamImageEx);
                }
            }
            return null;
        }

        /// <summary>
        /// Wrapper for Clipboard.GetText created for Bug #3432313
        /// </summary>
        /// <returns>string if there is text on the clipboard</returns>
        public static string GetText()
        {
            return GetText(GetDataObject());
        }

        /// <summary>
        /// Get Text from the DataObject
        /// </summary>
        /// <returns>string if there is text on the clipboard</returns>
        public static string GetText(IDataObject dataObject)
        {
            if (ContainsText(dataObject))
            {
                return (String)dataObject.GetData(DataFormats.Text);
            }
            return null;
        }

        /// <summary>
        /// Set text to the clipboard
        /// </summary>
        /// <param name="text"></param>
        public static void SetClipboardData(string text)
        {
            IDataObject ido = new DataObject();
            ido.SetData(DataFormats.Text, true, text);
            SetDataObject(ido, true);
        }

        private static string getHTMLString(ISurface surface, string filename)
        {
            string utf8EncodedHTMLString = Encoding.GetEncoding(0).GetString(Encoding.UTF8.GetBytes(HTML_CLIPBOARD_STRING));
            utf8EncodedHTMLString = utf8EncodedHTMLString.Replace("${width}", surface.Image.Width.ToString());
            utf8EncodedHTMLString = utf8EncodedHTMLString.Replace("${height}", surface.Image.Height.ToString());
            utf8EncodedHTMLString = utf8EncodedHTMLString.Replace("${file}", filename.Replace("\\", "/"));
            StringBuilder sb = new StringBuilder();
            sb.Append(utf8EncodedHTMLString);
            sb.Replace("<<<<<<<1", (utf8EncodedHTMLString.IndexOf("
        public static List<string> GetFormats()
        {
            return GetFormats(GetDataObject());
        }

        /// <summary>
        /// Retrieve a list of all formats currently in the IDataObject
        /// </summary>
        /// <returns>List<string> with the current formats
        public static List<string> GetFormats(IDataObject dataObj)
        {
            string[] formats = null;

            if (dataObj != null)
            {
                formats = dataObj.GetFormats();
            }
            if (formats != null)
            {
                LOG.DebugFormat("Got clipboard formats: {0}", String.Join(",", formats));
                return new List<string>(formats);
            }
            return new List<string>();
        }

        /// <summary>
        /// Check if there is currently something in the dataObject which has the supplied format
        /// </summary>
        /// <param name="dataObject">IDataObject</param>
        /// <param name="format">string with format</param>
        /// <returns>true if one the format is found</returns>
        public static bool ContainsFormat(string format)
        {
            return ContainsFormat(GetDataObject(), new[] { format });
        }

        /// <summary>
        /// Check if there is currently something on the clipboard which has the supplied format
        /// </summary>
        /// <param name="format">string with format</param>
        /// <returns>true if one the format is found</returns>
        public static bool ContainsFormat(IDataObject dataObject, string format)
        {
            return ContainsFormat(dataObject, new[] { format });
        }

        /// <summary>
        /// Check if there is currently something on the clipboard which has one of the supplied formats
        /// </summary>
        /// <param name="formats">string[] with formats</param>
        /// <returns>true if one of the formats was found</returns>
        public static bool ContainsFormat(string[] formats)
        {
            return ContainsFormat(GetDataObject(), formats);
        }

        /// <summary>
        /// Check if there is currently something on the clipboard which has one of the supplied formats
        /// </summary>
        /// <param name="dataObject">IDataObject</param>
        /// <param name="formats">string[] with formats</param>
        /// <returns>true if one of the formats was found</returns>
        public static bool ContainsFormat(IDataObject dataObject, string[] formats)
        {
            bool formatFound = false;
            List<string> currentFormats = GetFormats(dataObject);
            if (currentFormats == null || currentFormats.Count == 0 || formats == null || formats.Length == 0)
            {
                return false;
            }
            foreach (string format in formats)
            {
                if (currentFormats.Contains(format))
                {
                    formatFound = true;
                    break;
                }
            }
            return formatFound;
        }

        /// <summary>
        /// Get Object of type Type from the clipboard
        /// </summary>
        /// <param name="type">Type to get</param>
        /// <returns>object from clipboard</returns>
        public static Object GetClipboardData(Type type)
        {
            string format = type.FullName;
            return GetClipboardData(format);
        }

        /// <summary>
        /// Get Object for format from IDataObject
        /// </summary>
        /// <param name="dataObj">IDataObject</param>
        /// <param name="type">Type to get</param>
        /// <returns>object from IDataObject</returns>
        public static Object GetFromDataObject(IDataObject dataObj, Type type)
        {
            if (type != null)
            {
                return GetFromDataObject(dataObj, type.FullName);
            }
            return null;
        }

        /// <summary>
        /// Get ImageFilenames from the IDataObject
        /// </summary>
        /// <param name="dataObject">IDataObject</param>
        /// <returns></returns>
        public static List<string> GetImageFilenames(IDataObject dataObject)
        {
            List<string> filenames = new List<string>();
            string[] dropFileNames = (string[])dataObject.GetData(DataFormats.FileDrop);
            try
            {
                if (dropFileNames != null && dropFileNames.Length > 0)
                {
                    foreach (string filename in dropFileNames)
                    {
                        string ext = Path.GetExtension(filename).ToLower();
                        if ((ext == ".jpg") || (ext == ".jpeg") || (ext == ".tiff") || (ext == ".gif") || (ext == ".png") || (ext == ".bmp") || (ext == ".ico") || (ext == ".wmf"))
                        {
                            filenames.Add(filename);
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                LOG.Warn("Ignoring an issue with getting the dropFilenames from the clipboard: ", ex);
            }
            return filenames;
        }

        /// <summary>
        /// Get Object for format from IDataObject
        /// </summary>
        /// <param name="dataObj">IDataObject</param>
        /// <param name="format">format to get</param>
        /// <returns>object from IDataObject</returns>
        public static Object GetFromDataObject(IDataObject dataObj, string format)
        {
            if (dataObj != null)
            {
                try
                {
                    return dataObj.GetData(format);
                }
                catch (Exception e)
                {
                    LOG.Error("Error in GetClipboardData.", e);
                }
            }
            return null;
        }

        /// <summary>
        /// Get Object for format from the clipboard
        /// </summary>
        /// <param name="format">format to get</param>
        /// <returns>object from clipboard</returns>
        public static Object GetClipboardData(string format)
        {
            return GetFromDataObject(GetDataObject(), format);
        }
    }
}