/* 
 * Copyright (c) 2007, 2010, 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
 */


#include "stdafx.h"

#include "sql_editor_be.h"
#include "grtsqlparser/sql_facade.h"
#include "sqlide.h"
#include "base/string_utilities.h"
#include "base/boost_smart_ptr_helpers.h"
#include <boost/foreach.hpp>
#include "base/log.h"

#include "objimpl/db.query/db_query_QueryBuffer.h"

DEFAULT_LOG_DOMAIN("Sql_editor");

using namespace bec;
using namespace grt;
using namespace base;


Sql_editor::Ref Sql_editor::create(db_mgmt_RdbmsRef rdbms)
{
  Ref sql_editor;

  const char *def_module_name= "Sql";//QQQ rename to SqlSupport
  std::string module_name= rdbms->name().repr() + def_module_name;
  Sql::Ref sql_module= dynamic_cast<Sql::Ref>(rdbms->get_grt()->get_module(module_name));
  if (!sql_module) // fallback to db agnostic sql module
    sql_module= dynamic_cast<Sql::Ref>(rdbms->get_grt()->get_module(def_module_name));
  if (sql_module)
    sql_editor= sql_module->getSqlEditor(rdbms);
  
  if (sql_editor)
  {
    // setup the GRT object
    db_query_QueryBuffer::ImplData *data= new db_query_QueryBuffer::ImplData(sql_editor->grtobj(), sql_editor);
    sql_editor->grtobj()->set_data(data);
  }
  return sql_editor;
}


Sql_editor::Sql_editor(db_mgmt_RdbmsRef rdbms)
:
_rdbms(rdbms),
_eol(EolHelpers::eol(EolHelpers::eol_lf)),
_is_refresh_enabled(true),
_is_sql_check_enabled(true),
_selection_start(0),
_selection_end(0),
_cursor_pos(0),
_has_sql_errors(false),
_sql_check_progress_msg_throttle(500),
_grtobj(rdbms.get_grt())
{
  _sql_checker_mutex= g_mutex_new();
  _sql_errors_mutex= g_mutex_new();
  _sql_statement_borders_mutex= g_mutex_new();
  _sql_statement_borders_cache_mutex= g_mutex_new();

  _grtm= GRTManager::get_instance_for(rdbms->get_grt());
  GRT *grt= _grtm->get_grt();
  _options= DictRef::cast_from(grt->unserialize(make_path(_grtm->get_basedir(), "modules/data/" + _rdbms->id() + ".sql.editor.xml")));

  {
    SqlFacade::Ref sql_facade= SqlFacade::instance_for_rdbms(rdbms);
    _sql_checker= sql_facade->sqlSemanticCheck();
    _sql_checker->is_ast_generation_enabled(false);
  }
  _sql_checker_task= GrtThreadedTask::create(_grtm);
  _sql_checker_tag= 0;
}


Sql_editor::~Sql_editor()
{
  g_mutex_free(_sql_checker_mutex);
  g_mutex_free(_sql_errors_mutex);
  g_mutex_free(_sql_statement_borders_mutex);
  g_mutex_free(_sql_statement_borders_cache_mutex);
}


int Sql_editor::int_option(std::string name)
{
  return IntegerRef::cast_from(_options[name]);
}


std::string Sql_editor::string_option(std::string name)
{
  return StringRef::cast_from(_options[name]);
}


const std::string & Sql_editor::sql()
{
  return _sql;
}


void Sql_editor::sql(const std::string &sql)
{
  if (_sql != sql)
  {
    _sql= sql;
    eol(EolHelpers::eol(EolHelpers::detect(sql)));
    text_change_signal();
  }
}


void Sql_editor::eol(const std::string &value)
{
  if (value == _eol)
    return;
  _eol= value;
  _sql_checker->eol(_eol);
  eol_change_signal();
}


void Sql_editor::sql_mode(const std::string &value)
{
  _sql_checker->sql_mode(value);
}


bool Sql_editor::has_sql_errors() const
{
  return _has_sql_errors;
}


void Sql_editor::sql_parser_err_cb(Sql_editor::Sql_parser_err_cb cb)
{
  _sql_parser_err_cb= cb;
}


void Sql_editor::check_sql(bool sync)
{
  _sql_checker_tag++;
  _sql_checker->stop();

  _has_sql_errors= false;

  {
    GMutexLock sql_statement_borders_mutex(_sql_statement_borders_mutex);
    _sql_statement_borders.clear();
  }
  {
    GMutexLock sql_errors_mutex(_sql_errors_mutex);
    _sql_errors.clear();
  }

  _sql_checker_task->exec(sync, boost::bind(&Sql_editor::do_check_sql, this, _1, weak_ptr_from(this)));
}


grt::StringRef Sql_editor::do_check_sql(grt::GRT *grt, Ptr self_ptr)
{
  RETVAL_IF_FAIL_TO_RETAIN_WEAK_PTR (Sql_editor, self_ptr, self, grt::StringRef(""))

  GMutexLock sql_checker_mutex(_sql_checker_mutex);

  _sql_checker->report_sql_statement_border= boost::bind(&Sql_editor::on_report_sql_statement_border, this, _1, _2, _3, _4, _sql_checker_tag);
  _sql_checker->parse_error_cb(boost::bind(&Sql_editor::on_sql_error, this, _1, _2, _3, _4, _sql_checker_tag));
  _sql_checker_task->progress_cb(boost::bind(&Sql_editor::on_sql_check_progress, this, _1, _2, _sql_checker_tag));
  _sql_checker_task->finish_cb(boost::bind(&Sql_editor::on_sql_check_finished, this));

  _last_sql_check_progress_msg_timestamp= timestamp();

  _sql_checker->check_sql(_sql);

  // to ensure there is no pending items (sql errors, statement borders) to process, send final progress message
  _sql_checker_task->send_progress(0.f, std::string(), std::string());

  return grt::StringRef("");
}


int Sql_editor::on_sql_error(int tok_lineno, int tok_line_pos, int tok_len, const std::string &msg, int tag)
{
  if (tag != _sql_checker_tag)
    return 0;
  _has_sql_errors= true;
  {
    GMutexLock sql_errors_mutex(_sql_errors_mutex);
    _sql_errors.push_back(SqlError(tok_lineno, tok_line_pos, tok_len, msg, tag));
  }
  request_sql_check_results_refresh();

  return 0;
}


int Sql_editor::on_report_sql_statement_border(int begin_lineno, int begin_line_pos, int end_lineno, int end_line_pos, int tag)
{
  if (tag != _sql_checker_tag)
    return 0;
  {
    GMutexLock sql_statement_borders_cache_mutex(_sql_statement_borders_cache_mutex);
    _sql_statement_borders_cache.push_back(SqlStatementBorder(tag, begin_lineno, begin_line_pos, end_lineno, end_line_pos));
  }
  request_sql_check_results_refresh();
  return 0;
}


void Sql_editor::request_sql_check_results_refresh()
{
  if (_last_sql_check_progress_msg_timestamp + _sql_check_progress_msg_throttle < timestamp())
  {
    _sql_checker_task->send_progress(0.f, std::string(), std::string());
    _last_sql_check_progress_msg_timestamp= timestamp();
  }
}


int Sql_editor::on_sql_check_progress(float progress, const std::string &msg, int tag)
{
  if (tag != _sql_checker_tag)
    return 0;

  // sql statement beginnings
  {
    SqlStatementBorders sql_statement_borders;
    {
      GMutexLock sql_statement_borders_cache_mutex(_sql_statement_borders_cache_mutex);
      sql_statement_borders.swap(_sql_statement_borders_cache);
    }
    BOOST_FOREACH (const SqlStatementBorder &sql_statement_border, sql_statement_borders)
      if (sql_statement_border.tag == _sql_checker_tag && report_sql_statement_border)
        report_sql_statement_border(sql_statement_border.begin_lineno, sql_statement_border.begin_line_pos,
          sql_statement_border.end_lineno, sql_statement_border.end_line_pos);
    {
      GMutexLock sql_statement_borders_mutex(_sql_statement_borders_mutex);
      _sql_statement_borders.splice(_sql_statement_borders.end(), sql_statement_borders);
    }
  }

  // errors
  {
    SqlErrors sql_errors;
    {
      GMutexLock sql_errors_mutex(_sql_errors_mutex);
      _sql_errors.swap(sql_errors);
    }
    BOOST_FOREACH (const SqlError &sql_error, sql_errors)
      if (sql_error.tag == _sql_checker_tag && _sql_parser_err_cb)
        _sql_parser_err_cb(sql_error.tok_lineno, sql_error.tok_line_pos, sql_error.tok_len, sql_error.msg);
  }

  return 0;
}


int Sql_editor::on_sql_check_finished()
{
  // usage of this signal is a temporary workaround for scintilla control rendering problem in osx version
  // it's needed to determine the moment when parsing (syntax check) is complete
  sql_check_finish_signal();

  return 0;
}


Sql_editor::SqlStatementBorder Sql_editor::get_sql_statement_border_by_line_pos(int lineno, int line_pos)
{
  GMutexLock sql_statement_borders_mutex(_sql_statement_borders_mutex);
  const SqlStatementBorder *last_checked_border= NULL;
  SqlStatementBorders sql_statement_borders= _sql_statement_borders;
  BOOST_FOREACH (const SqlStatementBorder &sql_statement_border, sql_statement_borders)
  {
    if ((lineno < sql_statement_border.begin_lineno) ||
      ((lineno == sql_statement_border.begin_lineno) && (line_pos < sql_statement_border.begin_line_pos)))
    {
      break;
    }
    last_checked_border= &sql_statement_border;
  }
  if (last_checked_border)
    return *last_checked_border;
  return SqlStatementBorder(-1, -1, -1, -1, -1);
}


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

/**
 * The given text replaces the the current selection in the existing text, if there is a selection 
 * within the bounds of the current sql. If there is no selection the text is inserted at the 
 * position of selection start (which should be the same as the editors caret position).
 */
void Sql_editor::set_selected_text(const std::string &new_text)
{
  if (replace_selected_text_slot.empty())
  {
    int start= (_selection_start < (int) _sql.size()) ? _selection_start : (int) _sql.size() - 1;
    int end= (_selection_end <= (int) _sql.size()) ? _selection_end : (int) _sql.size();
    
    std::string new_sql= _sql.substr(0, start);
    new_sql += new_text;
    if (end < (int) _sql.size())
      new_sql += _sql.substr(end, _sql.size() - end);
    sql(new_sql);
  }
  else
  {
    replace_selected_text_slot(new_text);
  }
}


void Sql_editor::insert_text(const std::string &new_text)
{
  if (insert_text_slot.empty())
  {
    size_t pos= (size_t)_cursor_pos;
    if (pos > _sql.size())
      pos= _sql.size();
    _sql.insert(pos, new_text);
    sql(_sql);
  }
  else
  {
    insert_text_slot(new_text);
  }
}

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

void Sql_editor::set_selected_range(int start, int end)
{
  _selection_start= start;
  _selection_end= end;
  text_selection_change_signal();
}


bool Sql_editor::selected_range(int &start, int &end)
{
  start= _selection_start;
  end= _selection_end;
  return start < end;
}

std::string Sql_editor::selected_text()
{
  int start, end;
  if (selected_range(start, end))
    return sql().substr(start, end-start);
  return "";
}

void Sql_editor::set_cursor_pos(int pos)
{
  _cursor_pos= pos;
}


std::string Sql_editor::current_statement()
{
  if (current_statement_slot)
    return current_statement_slot();
  log_error("current_statement_slot is not set");
  return "";
}


void Sql_editor::sql_check_progress_msg_throttle(time_t val)
{
  _sql_check_progress_msg_throttle= val;
}


bec::MenuItemList Sql_editor::get_context_menu()
{
  bec::MenuItemList items;
  
  bec::MenuItem item;
  item.type = bec::MenuAction;
  item.name= "undo";
  item.caption= _("Undo");
  items.push_back(item);

  item.name= "redo";
  item.caption= _("Redo");
  items.push_back(item);

  {
    bec::MenuItem sep;
    sep.type= bec::MenuSeparator;
    items.push_back(sep);
  }
  
  item.name= "cut";
  item.caption= _("Cut");
  items.push_back(item);
  
  item.name= "copy";
  item.caption= _("Copy");
  items.push_back(item);
  
  item.name= "paste";
  item.caption= _("Paste");
  items.push_back(item);

  item.name= "delete";
  item.caption= _("Delete");
  items.push_back(item);

  {
    bec::MenuItem sep;
    sep.type= bec::MenuSeparator;
    items.push_back(sep);
  }
  
  item.name= "select_all";
  item.caption= _("Select All");
  items.push_back(item);

  item.type = bec::MenuCheck;
  item.name= "toggle_wrap_lines";
  item.caption= _("Wrap Lines");
  items.push_back(item);
  
  std::list<std::string> groups;
  groups.push_back("Menu/Text");

  {
    bec::ArgumentPool argpool;
    argpool.add_entries_for_object("activeQueryBuffer", grtobj());
    argpool.add_entries_for_object("", grtobj());
    
    bec::MenuItemList plugin_items= grtm()->get_plugin_context_menu_items(groups, argpool);
    
    if (!plugin_items.empty())
    {
      bec::MenuItem sep;
      sep.type= bec::MenuSeparator;
      items.push_back(sep);
      
      items.insert(items.end(), plugin_items.begin(), plugin_items.end());
    }
  }

  bec::MenuItemList plugin_items;
  bec::ArgumentPool argpool;
  argpool.add_simple_value("selectedText", grt::StringRef(""));
  argpool.add_simple_value("document", grt::StringRef(""));

  groups.clear();
  groups.push_back("Filter");
  plugin_items = grtm()->get_plugin_context_menu_items(groups, argpool);  
  if (!plugin_items.empty())
  {
    bec::MenuItem sep;
    sep.type= bec::MenuSeparator;
    items.push_back(sep);
    
    bec::MenuItem submenu;
    submenu.name= "text";
    submenu.caption= _("Text");
    submenu.type= bec::MenuCascade;
    submenu.subitems= plugin_items;
    items.push_back(submenu);
  }

  return items;
}


void Sql_editor::activate_context_menu_item(const std::string &name)
{
  std::vector<std::string> parts= base::split(name, ":", 1);
  if (parts.size() == 2 && parts[0] == "plugin")
  {
    app_PluginRef plugin(grtm()->get_plugin_manager()->get_plugin(parts[1]));

    if (!replace_selected_text_slot)
      throw std::logic_error("replace_selected_text_slot not set in Sql_editor");
    
    if (!plugin.is_valid())
      throw std::runtime_error("Invalid plugin "+name);

    bec::ArgumentPool argpool;
    argpool.add_entries_for_object("activeQueryBuffer", grtobj());
    argpool.add_entries_for_object("", grtobj());
    
    bool input_was_selection= false;
    if (bec::ArgumentPool::needs_simple_input(plugin, "selectedText"))
    {
      if (_eol == "\r\n")
      {
        // Temporary solution. Convert crlf to lf when accessing plugins.
        std::string selection= selected_text();
        base::replace(selection, "\r\n", "\n");
        argpool.add_simple_value("selectedText", grt::StringRef(selection));
      }
      else
        argpool.add_simple_value("selectedText", grt::StringRef(selected_text()));
      input_was_selection= true;
    }
    
    if (bec::ArgumentPool::needs_simple_input(plugin, "document"))
      argpool.add_simple_value("document", grt::StringRef(sql()));

    bool is_filter= false;
    if (plugin->groups().get_index("Filter") != grt::BaseListRef::npos)
      is_filter= true;
    
    grt::BaseListRef fargs(argpool.build_argument_list(plugin));
    
    grt::ValueRef result= grtm()->get_plugin_manager()->execute_plugin_function(plugin, fargs);

    if (is_filter)
    {
      if (!result.is_valid() || !grt::StringRef::can_wrap(result))
        throw std::runtime_error(base::strfmt("plugin %s returned unexpected value", plugin->name().c_str()));

      // No need to convert line endings here. If necessary this will do the wrapper behind the
      // replacement slot.
      grt::StringRef str(grt::StringRef::cast_from(result));
      if (input_was_selection)
        replace_selected_text_slot(*str);
      else
      {
        is_refresh_enabled(true);
        sql(*str);
      }      
    }
  }
  else {
    g_warning("unhandled context menu item %s", name.c_str());
  }

}


