/* 
 * 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
 */


/**
 * Utilities is a support class to trigger message boxes and the like in the front end.
 */

#include "stdafx.h"

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

#include "mforms/mforms.h"

DEFAULT_LOG_DOMAIN(DOMAIN_MFORMS_BE);

using namespace mforms;
using namespace base;

GThread *_mforms_main_thread = NULL;

static std::map<std::string, int> remembered_message_answers;
static std::string remembered_message_answer_file;

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

int Utilities::show_message(const std::string &title, const std::string &text,
                            const std::string &ok, const std::string &cancel,
                            const std::string &other)
{
  App::get()->close_splash();
  return ControlFactory::get_instance()->_utilities_impl.show_message(title, text, ok, cancel, other);
}

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

int Utilities::show_error(const std::string &title, const std::string &text,
                          const std::string &ok, const std::string &cancel,
                          const std::string &other)
{
  App::get()->close_splash();
  return ControlFactory::get_instance()->_utilities_impl.show_error(title, text, ok, cancel, other);
}

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

int Utilities::show_warning(const std::string &title, const std::string &text,
                            const std::string &ok, const std::string &cancel,
                            const std::string &other)
{
  App::get()->close_splash();
  return ControlFactory::get_instance()->_utilities_impl.show_warning(title, text, ok, cancel, other);
}

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

int Utilities::show_message_and_remember(const std::string &title, const std::string &text,
                                         const std::string &ok, const std::string &cancel,
                                         const std::string &other,
                                         const std::string &answer_id, const std::string &checkbox_text)
{
  App::get()->close_splash();
  if (remembered_message_answers.find(answer_id) != remembered_message_answers.end())
    return remembered_message_answers[answer_id];
  
  if (!ControlFactory::get_instance()->_utilities_impl.show_message_with_checkbox)
    return show_message(title, text, ok, cancel, other);

  bool remember = false;
  int rc = ControlFactory::get_instance()->_utilities_impl.show_message_with_checkbox(title, text, ok, cancel, other, checkbox_text, remember);
  if (remember)
  {
    remembered_message_answers[answer_id] = rc;
    save_message_answers();
  }
  return rc;
}


void Utilities::set_message_answers_storage_path(const std::string &path)
{
  remembered_message_answer_file = path;
  
  FILE *f = base_fopen(remembered_message_answer_file.c_str(), "r");
  if (f)
  {
    char line[1024];
    
    while (fgets(line, sizeof(line), f))
    {
      char *ptr = strrchr(line, '=');
      if (ptr)
      {
        *ptr= 0;
        remembered_message_answers[line] = atoi(ptr+1);
      }
    }
    fclose(f);
  }
}


void Utilities::save_message_answers()
{
  if (!remembered_message_answer_file.empty())
  {
    FILE *f = base_fopen(remembered_message_answer_file.c_str(), "w+");

    for (std::map<std::string, int>::const_iterator iter = remembered_message_answers.begin();
         iter != remembered_message_answers.end(); ++iter)
      fprintf(f, "%s=%i\n", iter->first.c_str(), iter->second);
    fclose(f);
  }
}


void Utilities::forget_message_answers()
{
  remembered_message_answers.clear();
  save_message_answers();
}

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

void Utilities::show_wait_message(const std::string &title, const std::string &text)
{
  // The wait message is a special window, so there's no need to hide the splash screen.
  ControlFactory::get_instance()->_utilities_impl.show_wait_message(title, text);
}

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

bool Utilities::hide_wait_message()
{
  return ControlFactory::get_instance()->_utilities_impl.hide_wait_message();
}

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

struct CancellableTaskData
{
  boost::function<void* ()> task;
  boost::shared_ptr<bool> finished_flag;
  boost::shared_ptr<void*> result_ptr;
  boost::shared_ptr<GMutex> mutex;
};

static void* cancellable_task_thread(CancellableTaskData *data)
{
  std::auto_ptr<CancellableTaskData> autodel(data);

  void *ptr = NULL;
  try
  {
    ptr = data->task();
  }
  catch (std::exception &exc)
  {
    log_error("Cancellable task threw uncaught exception: %s", exc.what());
  }
  g_mutex_lock(data->mutex.get());
  *data->result_ptr = ptr;
  *data->finished_flag = true;
  ControlFactory::get_instance()->_utilities_impl.stop_cancelable_wait_message();
  g_mutex_unlock(data->mutex.get());
  return 0;
}

static void real_mutex_unlock(boost::shared_ptr<GMutex> m)
{
  g_mutex_unlock(m.get());
}

static void mutex_free(GMutex *m)
{
  g_mutex_free(m);
}

bool Utilities::run_cancelable_task(const std::string &title, const std::string &text,
                                    const boost::function<void* ()> &task,
                                    const boost::function<bool ()> &cancel_task,
                                    void *&task_result)
{
  GThread *thread;
  GError *error = NULL;
  CancellableTaskData *data = new CancellableTaskData(); // this is freed by the thread
  boost::shared_ptr<bool> finished(new bool(false));
  boost::shared_ptr<void*> result(new void*((void*)-1));
  boost::shared_ptr<GMutex> lock(g_mutex_new(), mutex_free);
  
  data->mutex = lock;
  data->task = task;
  data->finished_flag = finished;
  
  // result_ptr is used by the task thread to pass back the result from the task callback
  // we cannot directly pass a pointer to task_result, because if the task is canceled,
  // this function will return and the caller may free the task_result object before the
  // canceled task actually finishes executing, which would invalidate any ptr to task_result 
  data->result_ptr = result;
  
  g_mutex_lock(lock.get());
  // start a thread that will run the task
  thread = g_thread_create((void*(*)(void*))cancellable_task_thread, data, 0, &error);
  if (!thread)
  {
    g_mutex_unlock(lock.get());
    delete data;
    std::string msg("Error creating thread: ");
    msg.append(error->message);
    g_error_free(error);
    throw std::runtime_error(msg);
  }

  boost::function<void ()> signal_ready = boost::bind(&real_mutex_unlock, data->mutex);

retry:
  if (ControlFactory::get_instance()->_utilities_impl.run_cancelable_wait_message(title, text, signal_ready, cancel_task))
  {
    signal_ready = boost::function<void ()>();

    // wait for the thread to finish clean up
    
    // sometimes, in the mac, there's a race and/or bug that causes the event loop
    // from the wait panel dialog to be exited when a nested modal dialog is closed.
    // clicking OK for the nested dialog, exits the wait panel, which would cause it
    // to return prematurely.. just execute the modal panel again if that's the case
    if (!*finished)
      goto retry;

    // task completed
    task_result = *result;
    return true;
  }
  else
  {
    log_debug2("run_cancelable_wait_message returned false\n");
    // task canceled by user
    return false;
  }
}

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

void Utilities::set_clipboard_text(const std::string &text)
{
  ControlFactory::get_instance()->_utilities_impl.set_clipboard_text(text);
}

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

std::string Utilities::get_clipboard_text()
{
  return ControlFactory::get_instance()->_utilities_impl.get_clipboard_text();
}

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

std::string Utilities::get_special_folder(FolderType type)
{
  return ControlFactory::get_instance()->_utilities_impl.get_special_folder(type);
}

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

void Utilities::open_url(const std::string &url)
{
  return ControlFactory::get_instance()->_utilities_impl.open_url(url);
}

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

bool Utilities::move_to_trash(const std::string &path)
{
  if (ControlFactory::get_instance()->_utilities_impl.move_to_trash)
    return ControlFactory::get_instance()->_utilities_impl.move_to_trash(path);
  else
  {
    if (g_file_test(path.c_str(), G_FILE_TEST_IS_DIR))
    {
      if (base_rmdir_recursively(path.c_str()) < 0)
        return false;
    }
    else
    {
      if (g_remove(path.c_str()) < 0)
        return false;
    }
    return true;
  }
}

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

void Utilities::add_timeout(float interval, const boost::function<bool ()> &callback)
{
  ControlFactory::get_instance()->_utilities_impl.add_timeout(interval, callback);
}

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

void Utilities::add_end_ok_cancel_buttons(mforms::Box *box, mforms::Button *ok, mforms::Button *cancel)
{
#ifdef __APPLE__
  box->add_end(ok, false, true);
  box->add_end(cancel, false, true);
#else
  box->add_end(cancel, false, true);
  box->add_end(ok, false, true);
#endif
}

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

bool Utilities::request_input(const std::string &title, const std::string &description,
                              const std::string &default_value, std::string &ret_value)
{
  // In order to avoid trouble with window z-ordering we explicitly ask to hide any wait window
  // that could get in the way. Same for the splash screen.
  hide_wait_message();
  App::get()->close_splash();

  mforms::Form input_form(NULL, (FormFlag) (FormDialogFrame | FormStayOnTop));
  mforms::Table content;
  mforms::ImageBox icon;
  mforms::Label description_label("");
  mforms::TextEntry edit;
  mforms::Box button_box(true);
  mforms::Button ok_button;
  mforms::Button cancel_button;

  input_form.set_title(title.empty() ? _("Enter a value") : title);

  content.set_padding(12);
  content.set_row_count(2);
  content.set_row_spacing(10);
  content.set_column_count(3);
  content.set_column_spacing(4);

  icon.set_image("message_edit.png");
  content.add(&icon, 0, 1, 0, 2, HFillFlag | VFillFlag);

  description_label.set_text(description);
  description_label.set_style(BoldStyle);

  edit.set_size(150, -1);
  edit.set_value(default_value);
  content.add(&description_label, 1, 2, 0, 1, HFillFlag | VFillFlag);
  content.add(&edit, 2, 3, 0, 1, HFillFlag | VFillFlag);

  button_box.set_spacing(10);
  ok_button.set_text(_("OK"));
  ok_button.set_size(75, -1);
  cancel_button.set_text(_("Cancel"));
  cancel_button.set_size(75, -1);
  Utilities::add_end_ok_cancel_buttons(&button_box, &ok_button, &cancel_button);
  content.add(&button_box, 1, 3, 1, 2, HFillFlag);

  input_form.set_content(&content);
  input_form.center();
  bool result= input_form.run_modal(&ok_button, &cancel_button);
  if (result)
    ret_value = edit.get_string_value();

  return result;  
}

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

void Utilities::store_password(const std::string &service, const std::string &account, const std::string &password)
{
  log_debug("Storing password for '%s'@'%s'\n", account.c_str(), service.c_str());
  ControlFactory::get_instance()->_utilities_impl.store_password(service, account, password);
}

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

bool Utilities::find_password(const std::string &service, const std::string &account, std::string &password)
{
  const bool ret = ControlFactory::get_instance()->_utilities_impl.find_password(service, account, password);
  log_debug("Looking up password for '%s'@'%s' has %s\n", account.c_str(), service.c_str(), ret ? "succeeded" : "failed");
  return ret;
}

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

void Utilities::forget_password(const std::string &service, const std::string &account)
{
  log_debug("Forgetting password for '%s'@'%s'\n", account.c_str(), service.c_str());
  ControlFactory::get_instance()->_utilities_impl.forget_password(service, account);
}

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

void *Utilities::perform_from_main_thread(const boost::function<void* ()> &slot, bool wait_done)
{
  return ControlFactory::get_instance()->_utilities_impl.perform_from_main_thread(slot, wait_done);
}

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

static void *_ask_for_password_main(const std::string &title, const std::string &service, std::string *username /*in/out*/,
                             bool prompt_storage, std::string *ret_password /*out*/, bool *ret_store /*out*/)
{
  log_debug("Creating and showing password dialog\n");

  Utilities::hide_wait_message(); 

  mforms::Form password_form(NULL, (FormFlag) (FormDialogFrame | FormStayOnTop));
  mforms::Table content;
  mforms::ImageBox icon;
  mforms::Label description("");
  mforms::Label service_description_label("");
  mforms::Label service_label("");
  mforms::Label user_description_label("");
  mforms::Label pw_description_label("");
  mforms::TextEntry password_edit(PasswordEntry);
  mforms::CheckBox save_password_box;
  mforms::Box button_box(true);
  mforms::Button ok_button;
  mforms::Button cancel_button;
  
  // Since we cannot simply change the function's signature (I only say: python) we have to use
  // a different way to pass an additional value in. The description used for the login details
  // request is passed in the title parameter as well, separated by the pipe symbol.
  std::vector<std::string> title_parts = base::split(title, "|", 2);
  if (title_parts.size() == 0 || title_parts[0].empty())
    password_form.set_title(_("MySQL Workbench Authentication"));
  else
    password_form.set_title(title_parts[0]);
  
  content.set_padding(12);
  content.set_row_count(6);
  content.set_row_spacing(prompt_storage ? 8 : 7);
  content.set_column_count(3);
  content.set_column_spacing(4);
  
  icon.set_image("message_wb_lock.png");
  content.add(&icon, 0, 1, 0, 6, HFillFlag | VFillFlag);
  
  if (title_parts.size() < 2 || title_parts[1].empty())
    description.set_text(_("Please enter password for the following service:"));
  else
    description.set_text(title_parts[1]);
  description.set_wrap_text(true);
  description.set_style(BigBoldStyle);
  description.set_size(300, -1);
  content.add(&description, 1, 3, 0, 1, HFillFlag | HExpandFlag | VFillFlag);
  
  service_description_label.set_text(_("Service:"));
  service_description_label.set_text_align(MiddleRight);
  service_description_label.set_style(BoldStyle);
  service_label.set_text(service);
  content.add(&service_description_label, 1, 2, 1, 2, HFillFlag | VFillFlag);
  content.add(&service_label, 2, 3, 1, 2, HFillFlag | VFillFlag);
  
  user_description_label.set_text(_("User:"));
  user_description_label.set_text_align(MiddleRight);
  user_description_label.set_style(BoldStyle);

  // Create an edit box for the user name if the given one is not set, otherwise just display the name.
  mforms::TextEntry* user_edit = NULL;
  if (username->empty())
  {
    user_edit = mforms::manage(new mforms::TextEntry());
    user_edit->set_value(_("<user name>"));
    content.add(&user_description_label, 1, 2, 2, 3, HFillFlag | VFillFlag);
    content.add(user_edit, 2, 3, 2, 3, HFillFlag | VFillFlag);
  }
  else
  {
    mforms::Label* user_label = mforms::manage(new mforms::Label(*username));
    content.add(&user_description_label, 1, 2, 2, 3, HFillFlag | VFillFlag);
    content.add(user_label, 2, 3, 2, 3, HFillFlag | VFillFlag);
  }
  
  pw_description_label.set_text(_("Password:"));
  pw_description_label.set_text_align(MiddleRight);
  pw_description_label.set_style(BoldStyle);
  content.add(&pw_description_label, 1, 2, 3, 4, HFillFlag | VFillFlag);
  content.add(&password_edit, 2, 3, 3, 4, HFillFlag | HExpandFlag);
  
  if (prompt_storage)
  {
#ifdef _WIN32
    save_password_box.set_text(_("Save password in vault"));
#else
    save_password_box.set_text(_("Save password in keychain"));
#endif
    content.add(&save_password_box, 2, 3, 4, 5, HExpandFlag | HFillFlag);
  }

  button_box.set_spacing(10);
  ok_button.set_text(_("OK"));
  ok_button.set_size(75, -1);
  cancel_button.set_text(_("Cancel"));
  cancel_button.set_size(75, -1);
  Utilities::add_end_ok_cancel_buttons(&button_box, &ok_button, &cancel_button);
  if (prompt_storage)
    content.add(&button_box, 1, 3, 5, 6, HFillFlag);
  else
    content.add(&button_box, 1, 3, 4, 5, HFillFlag);
  
  password_form.set_content(&content);
  password_form.center();
  bool result= password_form.run_modal(&ok_button, &cancel_button);
  if (result)
  {
    *ret_password= password_edit.get_string_value();
    *ret_store= save_password_box.get_active();

    if (user_edit != NULL)
      *username = user_edit->get_string_value();
  }
  
  return (void*)result;  
}

static bool _ask_for_password(const std::string &title, const std::string &service, std::string &username /*in/out*/,
                             bool prompt_storage, std::string &ret_password /*out*/, bool &ret_store /*out*/)
{
  if (Utilities::in_main_thread())
    return _ask_for_password_main(title, service, &username, prompt_storage, &ret_password, &ret_store) != NULL;
  else
    return Utilities::perform_from_main_thread(boost::bind(&_ask_for_password_main, 
                                               title, service, &username, prompt_storage, &ret_password, &ret_store)) != NULL;
}

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

bool Utilities::ask_for_password(const std::string &title, const std::string &service,
                                 const std::string &username, std::string &ret_password /*out*/)
{
  std::string ret_username = username;
  bool dummy= false;
  return _ask_for_password(title, service, ret_username, false, ret_password, dummy);
}

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

/**
 * Shows a dialog to request the password for the given service and user name.
 *
 * @param title Optional title describing the reason for the password request (eg. "Connect to MySQL Server")
 * @param service A description for what the password is required (e.g. "MySQL Server 5.1 on Thunder").
 * @param username [in/out] The name of the user for which to request the password.
 * @param password [out] Contains the password on return.
 *
 * @return True if the user pressed OK, otherwise false.
 */
bool Utilities::ask_for_password_check_store(const std::string &title, const std::string &service, 
                                             std::string &username, std::string &password, bool &store)
{
  return _ask_for_password(title, service, username, true, password, store);
}


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

/**
 * Shows a dialog to request the password for the given service and user name.
 * If requested by the user the password will be saved using either system facilities like OS X Keychain or
 * Gnome Keyring, or an encrypted file.
 *
 * @param title Optional title describing the reason for the password request (eg. "Connect to MySQL Server")
 * @param service A description for what the password is required (e.g. "MySQL Server 5.1 on Thunder").
 * @param username The name of the user for which to request the password. If empty on enter then the user name can
 *        also be edited.
 * @param reset_password Delete stored password and ask for a new one.
 * @param password [out] Contains the password on return.
 *
 * @return True if the user pressed OK, otherwise false.
 */
bool Utilities::credentials_for_service(const std::string &title, const std::string &service, std::string &username /*in/out*/,
                                         bool reset_password, std::string &password /*out*/)
{
  if (!reset_password && find_password(service, username, password))
    return true;

  if (reset_password)
    forget_password(service, username);  

  bool should_store_password_out = false;
  if (ask_for_password_check_store(title, service, username, password, should_store_password_out))
  {
    if (should_store_password_out)
    {
      try
      {
        store_password(service, username, password);
      }
      catch (std::exception &exc)
      {
        show_warning(title.empty() ? _("Error Storing Password") : title, 
                     std::string("There was an error storing the password:\n")+exc.what(), "OK");
        log_warning("Could not store password vault: %s\n", exc.what());
      }
    }
    return true;
  }
  return false;
}


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

#ifdef _WIN32

static int modal_loops = 0;

void Utilities::enter_modal_loop()
{
  modal_loops++;
}


void Utilities::leave_modal_loop()
{
  modal_loops--;
}


bool Utilities::in_modal_loop()
{
  return modal_loops > 0;
}

#endif

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

/**
 * Helper function to simplify icon loading. Returns NULL if the icon could not be found or
 * something wrong happened while loading.
 */
cairo_surface_t* Utilities::load_icon(const std::string& name)
{
  if (name.empty())
    return NULL;

  std::string icon_path= App::get()->get_resource_path(name);
  cairo_surface_t* result= cairo_image_surface_create_from_png(icon_path.c_str());
  if (result && cairo_surface_status(result) != CAIRO_STATUS_SUCCESS)
  {
    cairo_surface_destroy(result);
    result= NULL;
  }

  return result;
}

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

/**
 * Shortens the string so that it fits into the given width. If there is already enough room then
 * the input is simply returned. Otherwise letters are removed (via binary search) and ellipses
 * are added so that the entire result fits into that width.
 */
std::string Utilities::shorten_string(cairo_t* cr, const std::string& text, double width)
{
  int ellipsis_width= 0;
  int length;
  int l, h, n, w;
  cairo_text_extents_t extents;
  
  // If the text fits already, return the input.
  cairo_text_extents(cr, text.c_str(), &extents);
  if (extents.width <= width)
    return text;
  
  length= text.size();
  if (length == 0 || width <= 0)
    return "";
  else
  {
    cairo_text_extents(cr, "...", &extents);
    ellipsis_width= (int) ceil(extents.width);
  }
  
  const gchar* head= text.c_str();
  if (width <= ellipsis_width)
    return "";
  else
  {
    // Do a binary search for the optimal string length which fits into the given width.
    l= 0;
    h= length - 1;
    while (l < h)
    {
      n= (l + h) / 2;

      // Skip to the nth position, which needs the following loop as we don't have direct
      // access to a char in an utf-8 buffer (one of the limitations of that transformation format).
      const gchar* tail= head;
      for (int i= 0; i < n; i++)
        tail= g_utf8_next_char(tail);
      gchar* part= g_strndup(head, tail - head);
      cairo_text_extents(cr, part, &extents);
      g_free(part);
      w= (int) ceil(extents.width) + ellipsis_width;
      if (w <= width)
        l= n + 1;
      else
        h= n;
    }
    return text.substr(0, l - 1) + "...";
  }
  
  return "";
}

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

bool Utilities::in_main_thread()
{
  return g_thread_self() == _mforms_main_thread;
}

