/* 
 * Copyright (c) 2009, 2013, Oracle and/or its affiliates. All rights reserved.
 *
 * 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; version 2 of the
 * License.
 * 
 * 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, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301  USA
 */

/**
 * Windows front end implementation of the mforms Utilities APIs.
 */

#include "stdafx.h"

#include <StrSafe.h>

#include "mforms/mforms.h"
#include "wf_view.h"
#include "wf_utilities.h"

#include "base/log.h"
#include "base/string_utilities.h"
#include "base/log.h"
#include "base/file_utilities.h"

#include "ConvUtils.h"

DEFAULT_LOG_DOMAIN(DOMAIN_MFORMS_WRAPPER)

using namespace MySQL::Forms;
using namespace MySQL::Utilities;

//--------------------------------------------------------------------------------------------------

CustomMessageBox::CustomMessageBox()
{
  log_debug("Creating custom message box in old style\n");

  _picture= gcnew Windows::Forms::PictureBox();
  _button1= gcnew Windows::Forms::Button();
  _button2= gcnew Windows::Forms::Button();
  _button3= gcnew Windows::Forms::Button();
  _messageLabel= gcnew Windows::Forms::Label();
  _checkbox = gcnew Windows::Forms::CheckBox();

  SuspendLayout();

  Controls->Add(_picture);
  Controls->Add(_button2);
  Controls->Add(_button1);
  Controls->Add(_button3);
  Controls->Add(_messageLabel);
  Controls->Add(_checkbox);

  // picture
  _picture->Name= "picture";
  _picture->TabIndex= 0;
  _picture->TabStop= false;

  // button1
  _button1->Name= "button1";
  _button1->TabIndex= 1;
  _button1->UseVisualStyleBackColor= true;
  _button1->Click += gcnew System::EventHandler(this, &CustomMessageBox::ButtonClick);

  // button2
  _button2->Name= "button2";
  _button2->TabIndex= 2;
  _button2->UseVisualStyleBackColor= true;
  _button2->Click += gcnew System::EventHandler(this, &CustomMessageBox::ButtonClick);

  // button3
  _button3->Name= "button3";
  _button3->TabIndex= 3;
  _button3->UseVisualStyleBackColor= true;
  _button3->Click += gcnew System::EventHandler(this, &CustomMessageBox::ButtonClick);

  // messageText
  _messageLabel->Name= "messageLabel";
  _messageLabel->Padding= Windows::Forms::Padding(2);
  _messageLabel->TabIndex= 4;
  _messageLabel->Text= "label1";

  _checkbox->Name = "checkBox";
  _checkbox->TabIndex = 5;

  // Form
  this->Padding= Windows::Forms::Padding(8, 16, 8, 8);
  this->FormBorderStyle= ::FormBorderStyle::FixedDialog;
  this->AutoScaleDimensions= System::Drawing::SizeF(6, 13);
  this->AutoScaleMode= Windows::Forms::AutoScaleMode::Font;
  this->StartPosition = FormStartPosition::CenterScreen;
  this->Name= "CustomMessageBox";
  this->Text= "";
  this->ShowInTaskbar= false;
  this->HelpButton= false;
  this->MinimizeBox= false;
  this->MaximizeBox= false;
  //this->TopMost= true;
  this->Owner = UtilitiesImpl::get_mainform();

  ResumeLayout(false);
}

//--------------------------------------------------------------------------------------------------

// Some more task dialog icons, which are not defined in commctrl.h.
#define TD_SHIELD_BLUE_ICON      MAKEINTRESOURCE(-5)
#define TD_SECURITY_WARNING_ICON MAKEINTRESOURCE(-6)
#define TD_SECURITY_ERROR_ICON   MAKEINTRESOURCE(-7)
#define TD_SHIELD_SUCCESS_ICON   MAKEINTRESOURCE(-8)
#define TD_SHIELD_GRAY_ICON      MAKEINTRESOURCE(-9)

typedef HRESULT (WINAPI *PTaskDialogIndirect)(const TASKDIALOGCONFIG *pTaskConfig,
  __out_opt int *pnButton, __out_opt int *pnRadioButton, __out_opt BOOL *pfVerificationFlagChecked);

mforms::DialogResult CustomMessageBox::ShowVistaStyle(const std::string& title, const std::string& text,
  PCWSTR mainIcon, const std::string& buttonOK, const std::string& buttonCancel,
  const std::string& buttonOther, const std::string& checkbox, bool& checked)
{
  log_debug("Creating custom message box in vista style\n");

  // Load TaskDialogIndirect dynamically so we don't get a problem on XP.
  HMODULE comctl32 = LoadLibraryW(L"comctl32.dll");
  PTaskDialogIndirect _TaskDialogIndirect = (PTaskDialogIndirect) GetProcAddress(comctl32, "TaskDialogIndirect");

  TASKDIALOGCONFIG config = {0};
  config.cbSize = sizeof(config);

  int button = 0;
  int radioButton = 0;
  BOOL verificationChecked = FALSE;

  int buttonCount = 0;
  TASKDIALOG_BUTTON buttons[2];

  std::wstring do_text = base::string_to_wstring(buttonOK);
  if (!do_text.empty())
  {
    buttons[buttonCount].nButtonID = IDOK;
    buttons[buttonCount].pszButtonText = do_text.c_str();
    buttonCount++;
  }

  // Enable dialog cancellation via red-cross button if we have a cancel button.
  // If the text of the button is the standard one ("Cancel") then use the built-in
  // button, otherwise create an own command link.
  std::wstring cancel_text = base::string_to_wstring(buttonCancel);
  if (!cancel_text.empty())
  {
    config.dwFlags |= TDF_ALLOW_DIALOG_CANCELLATION;
    if (base::tolower(buttonCancel) == "cancel")
      config.dwCommonButtons = TDCBF_CANCEL_BUTTON;
    else
    {
      buttons[buttonCount].nButtonID = IDCANCEL;
      buttons[buttonCount].pszButtonText = cancel_text.c_str();
      buttonCount++;
    }
  }

  std::wstring other_text = base::string_to_wstring(buttonOther);
  if (other_text.size() > 0)
  {
    buttons[buttonCount].nButtonID = 1000;
    buttons[buttonCount].pszButtonText = other_text.c_str();
    buttonCount++;
  }

  // If we have more than one normal alternatives to show then enable command links.
  if (buttonCount > 1)
    config.dwFlags |= TDF_USE_COMMAND_LINKS;

  config.hwndParent = GetForegroundWindow();
  
  config.pszMainIcon = mainIcon;
  config.pszWindowTitle = L"MySQL Workbench";

  std::wstring titleText= base::string_to_wstring(title);
  config.pszMainInstruction = titleText.c_str();
  std::wstring descriptionText= base::string_to_wstring(text);
  config.pszContent = descriptionText.c_str();
  config.pButtons = buttons;
  config.cButtons = buttonCount;
  
  std::wstring checkbox_text = base::string_to_wstring(checkbox);
  if (checkbox_text.size() > 0)
    config.pszVerificationText = checkbox_text.c_str();

  log_debug("Running custom message box\n");

  HRESULT result = _TaskDialogIndirect(&config, &button, &radioButton, &verificationChecked);
  FreeLibrary(comctl32);

  if (!SUCCEEDED(result))
    return mforms::ResultCancel;

  checked = verificationChecked == TRUE;
  switch (button)
  {
  case IDOK:
    return mforms::ResultOk;
    break;
  case IDCANCEL:
    return mforms::ResultCancel;
    break;
  default:
    return mforms::ResultOther;
  }
}

//--------------------------------------------------------------------------------------------------

/// <summary>
/// Displays the CustomMessageBox and returns a dialog result, depending on the button clicked.
/// </summary>
mforms::DialogResult CustomMessageBox::ShowTraditionalStyle(String^ title,  String^ text, Image^ image,  String^ button1,
   String^ button2,  String^ button3,  String^ checkbox, bool& checked)
{
  log_debug("Showing traditional custom message box\n");

  CustomMessageBox^ box= gcnew CustomMessageBox();
  
  box->_picture->Image= image; 
  box->_picture->Size= image->Size;
  box->Text= title;
  box->_messageLabel->Text= text;
  box->_button1->Text= button1;
  box->_button1->DialogResult = Windows::Forms::DialogResult::OK;
  box->_button2->Text= button2;
  box->_button2->DialogResult = Windows::Forms::DialogResult::Cancel;
  box->_button3->Text= button3;
  box->_button3->DialogResult = Windows::Forms::DialogResult::Ignore;
  box->_checkbox->Text = checkbox;
  box->ComputeLayout();

  box->AcceptButton = box->_button1;
  box->CancelButton = box->_button2;

  Windows::Forms::DialogResult rc = box->ShowDialog();

  checked = box->_checkbox->Checked;
  switch (rc)
  {
  case Windows::Forms::DialogResult::OK:
    return mforms::ResultOk;

  default:
  case Windows::Forms::DialogResult::Cancel:
    return mforms::ResultCancel;

  case Windows::Forms::DialogResult::Ignore:
    return mforms::ResultOther;
  }
}

//--------------------------------------------------------------------------------------------------

// A little helper for repeating a button setup.
void AdjustButton(Windows::Forms::Button^ button)
{
  // Note: we need to set the Enabled property too as indicator if this button should be considered
  // when layouting the form. The Visible property cannot be used as long as the button's parent
  // is still hidden (which is the case when we do the layout process).
  if (button->Text == "")
  {
    button->Visible= false;
    button->Enabled= false;
  }
  else
  {
    Size size= button->GetPreferredSize(Drawing::Size::Empty);
    if (size.Width < 75)
      size.Width= 75;
    button->Size= size;
    button->Visible= true;
    button->Enabled= true;
  }
}

//--------------------------------------------------------------------------------------------------

#define MESSAGE_BOX_BUTTON_SPACING 8 // The spacing between two message buttons.
#define MESSAGE_BOX_MIN_WIDTH 300
#define MESSAGE_BOX_MIN_HEIGHT 128

/**
 * As the name already says this function computes the layout of the message box depending on the 
 * content (image size, button text etc.).
 */
void CustomMessageBox::ComputeLayout()
{
  log_debug2("Layouting custom message box\n");

  SuspendLayout();

  // Buttons have a minimum width of 75 px. Make them all same size and hide those which don't have
  // any text.
  AdjustButton(_button1);
  AdjustButton(_button2);
  AdjustButton(_button3);
  _checkbox->Enabled = _checkbox->Text != "";
  _checkbox->Visible = _checkbox->Enabled; 

  int visibleButtonCount= 0;
  System::Drawing::Size size= System::Drawing::Size::Empty;
  if (_button1->Enabled)
  {
    size= _button1->Size;
    visibleButtonCount++;
  }
  if (_button2->Enabled)
  {
    size.Height= _button2->Height;
    if (_button2->Width > size.Width)
      size.Width= _button2->Width;
    visibleButtonCount++;
  }
  if (_button3->Enabled)
  {
    size.Height= _button3->Height;
    if (_button3->Width > size.Width)
      size.Width= _button3->Width;
    visibleButtonCount++;
  }

  // For the common height we use the one from the last visible button we found.
  // Since the font is assumed to be the same for all buttons they also should compute
  // the same height when asked for their preferred size.
  // Compute the total size of the button area on the way (including padding of the container).
  System::Drawing::Size buttonSize= System::Drawing::Size::Empty;
  if (size.Width > 0)
  {
    // Compute only if there is at least one visible button.
    buttonSize.Height= size.Height;
    if (_button1->Enabled)
    {
      _button1->Size= size;
      buttonSize.Width= size.Width;
    }
    if (_button2->Enabled)
    {
      _button2->Size= size;
      if (buttonSize.Width > 0)
        buttonSize.Width += MESSAGE_BOX_BUTTON_SPACING;
      buttonSize.Width += size.Width;
    }
    if (_button3->Enabled)
    {
      _button3->Size= size;
      if (buttonSize.Width > 0)
        buttonSize.Width += MESSAGE_BOX_BUTTON_SPACING;
      buttonSize.Width += size.Width;
    }

    // For the right spacing between button box and border.
    buttonSize.Width += MESSAGE_BOX_BUTTON_SPACING;
  }

  // Additional size if the checkbox is visible.
  int effective_button_width = buttonSize.Width;
  if (_checkbox->Enabled)
  {
    _checkbox->Size = _checkbox->PreferredSize;
    effective_button_width += Padding.Left + _checkbox->Size.Width + MESSAGE_BOX_BUTTON_SPACING;
  }

  // Compute message text layout. Start with a certain minimum width.
  int minWidth= (effective_button_width > MESSAGE_BOX_MIN_WIDTH) ? effective_button_width : MESSAGE_BOX_MIN_WIDTH;
  System::Drawing::Size proposedSize= System::Drawing::Size(minWidth, 1);
  
  // We use the golden ratio to create an appealing layout.
  // Increase width in 10% steps until we found the proper ratio.
  int lastWidth= 0;
  while (true)
  {
    size= _messageLabel->GetPreferredSize(proposedSize);

    // Terminate loop if we get a height of 0, the same width as in the last run, beyond a maximum width
    // or reached the golden ratio. Getting the same width means we never can get to the golden
    // ratio as the text is too small.
    if ((size.Height == 0) || (size.Width == lastWidth) || 
      (size.Width >= 1000) || ((float) size.Width / size.Height >= 1.618))
      break;

    lastWidth= size.Width;
    proposedSize.Width += proposedSize.Width / 10;
  };

  // GetPreferredSize might return a value smaller than our minimum width. Account for that.
  if (size.Width < minWidth)
    size.Width= minWidth;
  _messageLabel->Size= size;

  // Now that we have the text size compute the overall size of the message box.
  // The image is vertically centered at the left side (with some padding)
  // and the buttons are at the bottom (right aligned).
  int textHeight= _messageLabel->Padding.Vertical + _messageLabel->Height;
  if (textHeight < _picture->Height + _picture->Padding.Vertical)
    textHeight= _picture->Height + _picture->Padding.Vertical;
  size.Width= Padding.Horizontal + _picture->Padding.Horizontal + _picture->Width +
    _messageLabel->Padding.Horizontal + _messageLabel->Width;
  size.Height= Padding.Vertical + textHeight + buttonSize.Height;

  // Make sure we have good looking minimum height.
  if (size.Height < MESSAGE_BOX_MIN_HEIGHT)
    size.Height= MESSAGE_BOX_MIN_HEIGHT;
  ClientSize= size;

  // Move picture to its final location (center vertically over the message text's height).
  Point location;
  location.X= Padding.Left;
  location.Y= Padding.Top + (textHeight - _picture->Height - _picture->Padding.Vertical) / 2;
  _picture->Location= location;

  // Text location too.
  location.X= _picture->Right + _messageLabel->Padding.Left;
  location.Y= Padding.Top;
  _messageLabel->Location= location;

  // Move the buttons to their final locations.
  if (buttonSize.Width > 0)
  {
    location= Point(ClientSize.Width - buttonSize.Width, ClientSize.Height - buttonSize.Height - Padding.Bottom);
    if (_button1->Enabled)
    {
      _button1->Location= location;
      location.X += _button1->Width + MESSAGE_BOX_BUTTON_SPACING;
    }
    if (_button3->Enabled)
    {
      _button3->Location= location;
      location.X += _button3->Width + MESSAGE_BOX_BUTTON_SPACING;
    }

    // Button 2 is our Cancel button (the one which is triggered when ESC is pressed), so place it last.
    if (_button2->Enabled)
      _button2->Location= location;
  }

  // Display the checkbox on the same line as the buttons but left aligned (if visible).
  if (_checkbox->Enabled)
  {
    location.X = Padding.Left;
    location.Y += (buttonSize.Height - _checkbox->Size.Height) / 2;
    _checkbox->Location = location;
  }

  ResumeLayout(true);
}

//--------------------------------------------------------------------------------------------------

void CustomMessageBox::ButtonClick(Object^ sender, EventArgs^ arguments)
{
  log_debug2("Button was clicked in custom message box\n");

  DialogResult = ((Windows::Forms::Button^)sender)->DialogResult;
}

//--------------------------------------------------------------------------------------------------

mforms::DialogResult CustomMessageBox::Show(const std::string& title, const std::string& text,
  PCWSTR mainIcon, const std::string& buttonOK, const std::string& buttonCancel,
  const std::string& buttonOther, const std::string& checkbox, bool& checked)
{
  log_debug("About to show a custom message box\n");

  // Our message looks different depending on whether we are running on XP or Vista and above.
  if (ControlUtilities::IsVistaOrAbove())
  {
    mforms::Utilities::enter_modal_loop();
    mforms::DialogResult result = ShowVistaStyle(title, text, mainIcon, buttonOK, buttonCancel,
      buttonOther, checkbox, checked);
    mforms::Utilities::leave_modal_loop();

    return result;
  }
  else
  {
     String^ boxTitle = CppStringToNative(title);
     String^ boxText = CppStringToNative(text);
     String^ ok_text = CppStringToNative(buttonOK);
     String^ cancel_text = CppStringToNative(buttonCancel);
     String^ other_text = CppStringToNative(buttonOther);
     String^ checkbox_text = CppStringToNative(checkbox);

    Image^ image;
    if (mainIcon == TD_WARNING_ICON)
      image= Image::FromFile("images/ui/message_warning.png");
    else
      if (mainIcon == TD_ERROR_ICON)
        image= Image::FromFile("images/ui/message_error.png");
      else
        image= Image::FromFile("images/ui/message_confirm.png");
    
    mforms::Utilities::enter_modal_loop();
    mforms::DialogResult result = CustomMessageBox::ShowTraditionalStyle(boxTitle, boxText, image,
      ok_text, cancel_text, other_text, checkbox_text, checked);
    mforms::Utilities::leave_modal_loop();
    return result;
  }
}

//--------------------------------------------------------------------------------------------------

Windows::Forms::DialogResult CustomMessageBox::Show(MessageType type,  String^ title,  String^ text,  
   String^ buttonOK,  String^ buttonCancel,  String^ buttonOther,  String^ checkbox, [Out] bool% checked)
{
  log_debug("About to show a custom message box\n");

  mforms::DialogResult result;

  // Our message looks different depending on whether we are running on XP or Vista and above.
  if (ControlUtilities::IsVistaOrAbove())
  {
    mforms::Utilities::enter_modal_loop();
    PCWSTR mainIcon;
    switch (type)
    {
    case MessageType::MessageWarning:
      mainIcon = TD_WARNING_ICON;
      break;
    case MessageType::MessageError:
      mainIcon = TD_ERROR_ICON;
      break;
    default:
      mainIcon = TD_INFORMATION_ICON;
      break;
    }
    bool isChecked = false;
    result = ShowVistaStyle(
      NativeToCppString(title), NativeToCppString(text), mainIcon, NativeToCppString(buttonOK),
      NativeToCppString(buttonCancel), NativeToCppString(buttonOther), NativeToCppString(checkbox),
      isChecked
    );
    checked = isChecked;
    mforms::Utilities::leave_modal_loop();
  }
  else
  {
    Image^ image;
    switch (type)
    {
    case MessageType::MessageWarning:
      image= Image::FromFile("images/ui/message_warning.png");
      break;
    case MessageType::MessageError:
      image= Image::FromFile("images/ui/message_error.png");
      break;
    default:
      image= Image::FromFile("images/ui/message_confirm.png");
      break;
    }

    mforms::Utilities::enter_modal_loop();
    bool isChecked = false;
    result = CustomMessageBox::ShowTraditionalStyle(title, text, image, buttonOK, buttonCancel,
      buttonOther, checkbox, isChecked);
    checked = isChecked;
    mforms::Utilities::leave_modal_loop();
  }

  Windows::Forms::DialogResult native_result;
  switch (result)
  {
  case mforms::ResultCancel:
    native_result = Windows::Forms::DialogResult::Cancel;
    break;
  case mforms::ResultOther:
    native_result = Windows::Forms::DialogResult::Ignore;
    break;
  default:
    native_result = Windows::Forms::DialogResult::OK;
    break;
  }
  return native_result;
}

//--------------------------------------------------------------------------------------------------

Windows::Forms::DialogResult CustomMessageBox::Show(MessageType type,  String^ title,  String^ text,
   String^ buttonOK)
{
  bool checked = false;
  return Show(type, title, text, buttonOK, "", "", "", checked);
}

//----------------- DispatchControl ----------------------------------------------------------------

delegate InvokationResult^ RunSlotDelegate();

void* DispatchControl::RunOnMainThread(const boost::function<void* ()>& slot, bool wait)
{
  log_debug("Running slot on main thread (%swaiting for it)\n", wait ? "" : "not ");

  if (InvokeRequired)
  {
    log_debug2("Cross thread invocation required\n");

    _slot = new boost::function<void* ()>(slot);
    if (wait)
    {
      InvokationResult^ result =
        (InvokationResult^) Invoke(gcnew RunSlotDelegate(this, &DispatchControl::RunSlot));
      return result->Result;
    }
    else
    {
      BeginInvoke( gcnew RunSlotDelegate(this, &DispatchControl::RunSlot));

      // If we don't wait for the result we cannot return it.
      return NULL;
    }
  }

  return slot();
}


DispatchControl::~DispatchControl()
{
  delete _slot; 
}

//--------------------------------------------------------------------------------------------------

/**
 * Helper function to run a given slot in the main thread.
 */
InvokationResult^ DispatchControl::RunSlot()
{
  log_debug2("Running slot on main thread\n");

  return gcnew InvokationResult((*_slot)());
}

//----------------- UtilitiesImpl ------------------------------------------------------------------

UtilitiesImpl::UtilitiesImpl()
{
}

//--------------------------------------------------------------------------------------------------

int UtilitiesImpl::show_message(const std::string &title, const std::string &text, const std::string &ok, 
  const std::string &cancel, const std::string &other)
{
  log_debug("Showing a message to the user\n");

  hide_wait_message();

  bool checked;
  return CustomMessageBox::Show(title, text, TD_INFORMATION_ICON, ok, cancel, other, "", checked);
}

//--------------------------------------------------------------------------------------------------

int UtilitiesImpl::show_error(const std::string &title, const std::string &text, const std::string &ok, 
  const std::string &cancel, const std::string &other)
{
  log_debug("Showing an error to the user\n");

  hide_wait_message();

  bool checked;
  return CustomMessageBox::Show(title, text, TD_ERROR_ICON, ok, cancel, other, "", checked);
}

//--------------------------------------------------------------------------------------------------

int UtilitiesImpl::show_warning(const std::string &title, const std::string &text, const std::string &ok, 
  const std::string &cancel, const std::string &other)
{
  log_debug("Showing a warning to the user\n");

  hide_wait_message();

  bool checked;
  return CustomMessageBox::Show(title, text, TD_WARNING_ICON, ok, cancel, other, "", checked);
}

//--------------------------------------------------------------------------------------------------

int UtilitiesImpl::show_message_with_checkbox(const std::string &title, const std::string &text,
  const std::string &ok, const std::string &cancel, const std::string &other, const std::string &checkbox_text,
  bool &isChecked)
{
  log_debug("Showing a message with checkbox to the user\n");

  hide_wait_message();

  std::string checkboxText = (checkbox_text.size() > 0) ? checkbox_text :
    _("Don't show this message again");
  return CustomMessageBox::Show(title, text, TD_INFORMATION_ICON, ok, cancel, other, checkboxText,
    isChecked);
}

//--------------------------------------------------------------------------------------------------

/**
 * Shows the warning heads-up-display with the given title and text.
 */
void UtilitiesImpl::show_wait_message(const std::string &title, const std::string &text)
{
  log_debug("Showing wait message\n");

  HUDForm::Show(CppStringToNative(title), CppStringToNative(text), true);
}

//--------------------------------------------------------------------------------------------------

/**
 * Hides a previously shown wait message.
 */
bool UtilitiesImpl::hide_wait_message()
{
  log_debug("Hiding the wait message\n");

  bool result= HUDForm::IsVisible;
  if (result)
  {
    log_debug2("Wait message was visible, finishing it\n");

    HUDForm::Finish();
  }
  else
    log_debug2("Wait message was not visibile, nothing to do\n");


  return result;
}

//--------------------------------------------------------------------------------------------------

ref class CallSlotDelegate
{
public:
  CallSlotDelegate(const boost::function<bool ()> *s) : slot(s) {}
  const boost::function<bool ()> *slot;
  bool call_slot()
  {
    return (*slot)();
  }
};

//-------------------------------------------------------------------------------------------------

bool UtilitiesImpl::run_cancelable_wait_message(const std::string &title, const std::string &text, 
                                                const boost::function<void ()> &start_task,
                                                const boost::function<bool ()> &cancel_slot)
{
  log_debug("Running a cancelable wait message\n");

  CallSlotDelegate ^caller = gcnew CallSlotDelegate(&cancel_slot);

  HUDForm::CancelDelegate ^deleg = gcnew HUDForm::CancelDelegate(caller, &CallSlotDelegate::call_slot);

  // There is a possible race condition here
  // if the task executed by start_task() finishes fast enough, it could
  // result in a call to stop_cancelable_wait_message() before the below
  // ShowModal. That will cause the modal window will stay up forever.

  if (start_task)
  {
    log_debug2("Starting the task\n");

    start_task();
  }

  log_debug2("Running the HUD window\n");
  Windows::Forms::DialogResult result = HUDForm::ShowModal(CppStringToNative(title), CppStringToNative(text), true, deleg);
  log_debug2("HUD window returned with code: %i\n", result);

  // Abort is used if the window was forcibly closed (e.g. when showing another dialog, like password query).
  return (result == Windows::Forms::DialogResult::OK) || (result == Windows::Forms::DialogResult::Abort);
}

//--------------------------------------------------------------------------------------------------

/**
 * Signals the operation being described in a previous run_cancelable_wait_message() call has
 * finished and the message panel should be taken down.
 */
void UtilitiesImpl::stop_cancelable_wait_message()
{
  log_debug("Explicit cancelation of the wait message\n");

  // Wait until the HUDForm is visible (i.e. ShowModal is called) before closing it
  // to avoid a race condition. This could deadlock only if start_task() is not a 
  // thread, which would be a problem in itself.
  while (!HUDForm::IsVisible)
    ;
  HUDForm::Finish();
}

//--------------------------------------------------------------------------------------------------

/**
 * Places the given string on the clipboard.
 * 
 * @param content The text to be placed on the clipboard. It is assume its encoding is UTF-8.
 */
void UtilitiesImpl::set_clipboard_text(const std::string &content)
{
  log_debug("Setting clipboard text\n");

  if (!content.empty())
  {
    String^ text = CppStringToNative(content);
    try
    {
      Clipboard::Clear();

      // If the clipboard cannot be opened because another application uses it at this very moment
      // we try a second time with built-in retries. If that still fails we inform the user.
      Clipboard::SetText(text);

      log_debug2("Successful\n");
    }
    catch (ExternalException ^)
    {
      try
      {
        log_debug2("Got exception, second attempt\n");
        Clipboard::SetDataObject(text, false, 5, 200);
        log_debug2("Successful\n");
      }
      catch (ExternalException ^e)
      {
        String^ msg = e->Message + Environment::NewLine + Environment::NewLine;
        msg += "Caused by:" + Environment::NewLine;

        HWND window = GetOpenClipboardWindow();
        TCHAR window_text[501];
        GetWindowText(window, window_text, 500);
        String ^converted_text = gcnew String(window_text);
        msg += converted_text;
        log_debug2("Error while setting the text: %s\n", NativeToCppString(msg).c_str());

        CustomMessageBox::Show(MessageType::MessageError, _("Error while copying to clipboard"), msg, "Close");
      }
    }
  }
}

//--------------------------------------------------------------------------------------------------

/**
 * Returns the current text on the clipboard, if there is any.
 * 
 * @result If there is text on the clipboard (ANSI or Unicode) it is returned as UTF-8 string.
 * @note The returned text gets all CRLF Windows line breaks converted to pure LF.
 */
std::string UtilitiesImpl::get_clipboard_text()
{
  log_debug("Reading clipboard text\n");

  String^ unicode= (String^) Clipboard::GetData(DataFormats::UnicodeText);
  if (unicode == nullptr)
    return "";

  return NativeToCppString(unicode);
}

//--------------------------------------------------------------------------------------------------

/**
 * Returns platform specific user folders, e.g. for the desktop, the user's documents etc.
 */
std::string UtilitiesImpl::get_special_folder(mforms::FolderType type)
{
  log_debug("Get special folder\n");

  Environment::SpecialFolder special_folder;
  switch (type)
  {
  case mforms::Desktop:
    special_folder = Environment::SpecialFolder::DesktopDirectory;
    break;
  case mforms::ApplicationData:
    special_folder = Environment::SpecialFolder::ApplicationData;
    break;
  case mforms::WinProgramFiles:
    special_folder = Environment::SpecialFolder::ProgramFiles;
    break;
  case mforms::WinProgramFilesX86:
    special_folder = Environment::SpecialFolder::ProgramFilesX86;
    break;
  case mforms::ApplicationSettings:
    special_folder = Environment::SpecialFolder::ApplicationData;
    break;
  default: // Documents
    special_folder = Environment::SpecialFolder::MyDocuments;
    break;
  };

  // Getting the 64 bit application folder works differently if we are a 32 bit application.
  // IntPtr has a size of 8 for 64 bit apps.
  if (IntPtr::Size == 4 && type == mforms::WinProgramFiles)
  {
    log_debug2("Getting the 64bit program path\n");

    WCHAR folder[MAX_PATH];
    ExpandEnvironmentStrings(L"%ProgramW6432%", folder, ARRAYSIZE(folder));
    return base::wstring_to_string(folder);
  }

  // ApplicationSettings is not a predefined folder, but our own application data folder.
  if (type == mforms::ApplicationSettings)
  {
    std::string folder = NativeToCppString(Environment::GetFolderPath(special_folder));
    return folder + "/MySQL/Workbench";
  }

  return NativeToCppString(Environment::GetFolderPath(special_folder));
}

//--------------------------------------------------------------------------------------------------

void UtilitiesImpl::open_url(const std::string &url)
{
  try
  {
    log_debug("Opening the URL: %s\n", url.c_str());

    System::Diagnostics::Process::Start(CppStringToNative(url));
  }
  catch (Exception ^e)
  {
    String ^message = e->Message->ToString();
    log_debug("Error opening the url: %s\n", NativeToCppString(message).c_str());

    MessageBox::Show(message, "Error Opening Browser", 
      MessageBoxButtons::OK, MessageBoxIcon::Error, MessageBoxDefaultButton::Button1);
  }
}

//--------------------------------------------------------------------------------------------------

bool UtilitiesImpl::move_to_trash(const std::string &file_name)
{
  log_debug("Moving file to trash: %s\n", file_name.c_str());
  
  SHFILEOPSTRUCT shf = {0};

  // Filenames must be double 0 terminated.
  wchar_t path[MAX_PATH + 1] = {0};

  shf.wFunc = FO_DELETE;
  shf.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION;

  // Paths must be double 0 terminated.
  std::wstring converted_filename = base::string_to_wstring(file_name);
  StringCchCopyW(path, sizeof(path), converted_filename.c_str()); 
  shf.pFrom = path;
  int result = SHFileOperation(&shf); 

  return (result == 0) && !shf.fAnyOperationsAborted;
}

//--------------------------------------------------------------------------------------------------

void MySQL::Forms::UtilitiesImpl::reveal_file(const std::string &path)
{
  std::wstring native_path = base::string_to_wstring(path);
  PIDLIST_ABSOLUTE pidl = ILCreateFromPath(native_path.c_str());
  if (pidl != NULL)
  {
    if (SHOpenFolderAndSelectItems(pidl, 0, 0, 0) != S_OK)
      log_debug("Could not open Explorer with the path %s\n", path.c_str());

    ILFree(pidl);
  }
}

//--------------------------------------------------------------------------------------------------

// The password cache is a temporary storage and only used for a short time frame when looking up a password.
static GStaticMutex password_mutex = G_STATIC_MUTEX_INIT;
static std::map<std::string, std::string> password_cache;
typedef std::map<std::string, std::string>::iterator PasswordIterator;

#define DOMAIN_SEPARATOR (char) 2
#define PASSWORD_SEPARATOR (char) 3

/**
 * Loads encrypted passwords from disk. These are typically held only for a short moment.
 */
void UtilitiesImpl::load_passwords()
{
  log_debug("Loading password cache\n");
  
  // Load password cache from disk. Don't throw an error if the cache file doesn't exist yet, though.
  std::string file = get_special_folder(mforms::ApplicationData) + "/MySQL/Workbench/workbench_user_data.dat";
  std::string user_info = _(std::string("If loading the passwords fails repeatedly you should clear the vault. This will remove all ") +
    "passwords, hence you have to enter them again.");
  if (g_file_test(file.c_str(), G_FILE_TEST_EXISTS))
  {
    gchar* content;
    gsize length;
    GError* error= NULL;
    bool result= g_file_get_contents(file.c_str(), &content, &length, &error) == TRUE;
    if (!result)
    {
      log_error("Error loading password data: %s\n", error->message);
      int result = show_error("Password management error", "Error while loading passwords: " + 
        std::string(error->message) + "\n\n" + user_info,
        _("Ignore"), "", _("Clear Vault"));
      if (result == mforms::ResultOther)
        base::remove(file);
      return;
    }

    DATA_BLOB data_in;
    DATA_BLOB data_out;

    data_in.pbData= (BYTE*) content;
    data_in.cbData= length;
    
    log_debug2("Decrypting password data\n");
    result = CryptUnprotectData(&data_in, NULL, NULL, NULL, NULL, CRYPTPROTECT_UI_FORBIDDEN, &data_out) == TRUE;
    
    g_free(content);

    if (!result)
    {
      log_error("Could not decrypt password data\n");
      int result = show_error("Password management error", "Could not decrypt password cache.\n\n" + user_info,
        _("Ignore"), "", _("Clear Vault"));
      if (result == mforms::ResultOther)
        base::remove(file);
      return;
    }

    try
    {
      log_debug2("Filling password cache\n");

      // Split the string into individual items and fill the password cache with them.
      std::stringstream ss((char*) data_out.pbData);
      std::string item;
      while (std::getline(ss, item, '\n'))
      {
        std::string::size_type pos= item.find_first_of(PASSWORD_SEPARATOR, 0);

        if (std::string::npos != pos)
          password_cache[item.substr(0, pos)]= item.substr(pos + 1, item.length());
      }
    }
    finally
    {
      LocalFree(data_out.pbData);
    }
  }
}

//--------------------------------------------------------------------------------------------------

/**
 * Saves the password cache to disk (if store is true) and clears it so passwords aren't kept in
 * memory any longer than necessary.
 */
void UtilitiesImpl::unload_passwords(bool store)
{
  log_debug("Unloading password cache\n");
  
  // Store all passwords in a string for encryption.
  try
  {
    if (store)
    {
      log_debug("Storing password data\n");

      std::string plain_data;
      for (PasswordIterator iterator = password_cache.begin(); iterator != password_cache.end(); iterator++)
        plain_data += iterator->first + PASSWORD_SEPARATOR + iterator->second + "\n";

      DATA_BLOB data_in;
      DATA_BLOB data_out;

      data_in.pbData = (BYTE*) plain_data.c_str();
      data_in.cbData = (DWORD) plain_data.length() + 1;
      
      if (!CryptProtectData(&data_in, NULL, NULL, NULL, NULL, CRYPTPROTECT_UI_FORBIDDEN, &data_out))
      {
        log_error("Error encrypting password data\n");
        show_error("Password management error", "Could not encrypt password cache.", _("Close"), "", "");
        return;
      }

      // Now write the encrypted data to file.
      std::string file= get_special_folder(mforms::ApplicationData) + "/MySQL/Workbench/workbench_user_data.dat";
      GError* error= NULL;
      bool result= g_file_set_contents(file.c_str(), (gchar*) data_out.pbData, data_out.cbData, &error) == TRUE;

      LocalFree(data_out.pbData);

      if (!result)
      {
        log_error("Error storing password data: %s\n", error->message);
        show_error("Password management error", "Error while storing passwords: " + std::string(error->message),
          _("Close"), "", "");
      }
    }
  }
  finally
  {
    password_cache.clear();
  }
}

//--------------------------------------------------------------------------------------------------

/**
 * This function stores the given password in our password file, parameterized by the given service
 * and user name. The file is encrypted by the system for safety and can only be decrypted by the
 * same user who encrypted it.
 */
void UtilitiesImpl::store_password(const std::string &service, const std::string &account, const std::string &password)
{
  g_static_mutex_lock(&password_mutex);

  try
  {
    load_passwords();

    // Keep the password in our password cache and write the entire cache to file after that.
    password_cache[service + DOMAIN_SEPARATOR + account]= password;

    unload_passwords(true);
  }
  finally
  {
    g_static_mutex_unlock(&password_mutex);
  }
}

//--------------------------------------------------------------------------------------------------

/**
 * Return the plain text password for the given service and account.
 */
bool UtilitiesImpl::find_password(const std::string &service, const std::string &account, std::string &password)
{
  log_debug("Looking up password for service: %s, account: %s\n", service.c_str(), account.c_str());
  
  g_static_mutex_lock(&password_mutex);

  try
  {
    load_passwords();

    bool result= true;
    PasswordIterator iterator= password_cache.find(service + DOMAIN_SEPARATOR + account);
    if (iterator == password_cache.end())
      result= false;
    else
      password= iterator->second;

    unload_passwords(false);
    return result;
  }
  finally
  {
    g_static_mutex_unlock(&password_mutex);
  }
}

//--------------------------------------------------------------------------------------------------

/**
 * Remove the password for the given service and account if there is one.
 */
void UtilitiesImpl::forget_password(const std::string &service, const std::string &account)
{
  g_static_mutex_lock(&password_mutex);

  try
  {
    load_passwords();

    PasswordIterator iterator= password_cache.find(service + DOMAIN_SEPARATOR + account);
    if (iterator != password_cache.end())
    {
      password_cache.erase(iterator);
      unload_passwords(true);
    }
    else
      unload_passwords(false);
  }
  finally
  {
    g_static_mutex_unlock(&password_mutex);
  }
}

//--------------------------------------------------------------------------------------------------

void* UtilitiesImpl::perform_from_main_thread(const boost::function<void* ()>& slot, bool wait)
{
  return _dispatcher->RunOnMainThread(slot, wait);
}

//--------------------------------------------------------------------------------------------------

/**
 * Returns the main form of the application.
 */
Windows::Forms::Form^ UtilitiesImpl::get_mainform()
{
  log_debug2("Returning main form\n");

  return Application::OpenForms["MainForm"];
}

//--------------------------------------------------------------------------------------------------

public ref class TimerHandler
{
public:
  TimerHandler(float interval, const boost::function<bool ()> &slot)
  {
    log_debug("Creating new TimerHandler\n");

    _timer = gcnew System::Windows::Forms::Timer();

    _slot = new boost::function<bool ()>(slot);

    _timer->Interval = (int) (interval * 1000);
    _timer->Tick += gcnew EventHandler(this, &TimerHandler::timer_tick);
    _timer->Start();
  }

  ~TimerHandler()
  {
    log_debug("Destructing TimerHandler instance\n");

    _timer->Stop();
    delete _timer;
    delete _slot;
  }

private:
  boost::function<bool ()> *_slot;
  System::Windows::Forms::Timer ^_timer;

  void timer_tick(Object^ sender, System::EventArgs ^e)
  {
    log_debug3("Timer tick\n");

    // Emulate behavior in MacOS X (timers aren't fired when in modal loops).
    // Also works around a deadlock of python when a timer is fired while inside a modal loop.
    if (!mforms::Utilities::in_modal_loop())
    {
      _timer->Stop();

      // if callback returns true, then restart the timer
      if ((*_slot)())
        _timer->Enabled = true;
      else
        delete this;
    }
  }
};

//-------------------------------------------------------------------------------------------------

void UtilitiesImpl::add_timeout(float interval, const boost::function<bool ()> &slot)
{
  log_debug("Adding new timeout\n");

  TimerHandler ^handler = gcnew TimerHandler(interval, slot);
}

//-------------------------------------------------------------------------------------------------
