/* Copyright (C) 2010 Sergei Golubchik and Monty Program Ab

   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 Street, Fifth Floor, Boston, MA 02110-1335 USA */

#include "feedback.h"

/* MySQL functions/variables not declared in mysql_priv.h */
int fill_variables(THD *thd, TABLE_LIST *tables, COND *cond);
int fill_status(THD *thd, TABLE_LIST *tables, COND *cond);
extern ST_SCHEMA_TABLE schema_tables[];

namespace feedback {

#ifndef DBUG_OFF
ulong debug_startup_interval, debug_first_interval, debug_interval;
#endif

char server_uid_buf[SERVER_UID_SIZE+1]; ///< server uid will be written here

/* backing store for system variables */
static char *server_uid= server_uid_buf, *url, *http_proxy;
char *user_info;
ulong send_timeout, send_retry_wait;

/**
  these three are used to communicate the shutdown signal to the
  background thread
*/
mysql_mutex_t sleep_mutex;
mysql_cond_t sleep_condition;
volatile bool shutdown_plugin;
static pthread_t sender_thread;

#ifdef HAVE_PSI_INTERFACE
static PSI_mutex_key key_sleep_mutex;
static PSI_mutex_info mutex_list[]=
{{ &key_sleep_mutex, "sleep_mutex", PSI_FLAG_GLOBAL}};

static PSI_cond_key key_sleep_cond;
static PSI_cond_info cond_list[]=
{{ &key_sleep_cond, "sleep_condition", PSI_FLAG_GLOBAL}};

static PSI_thread_key key_sender_thread;
static PSI_thread_info	thread_list[] =
{{&key_sender_thread, "sender_thread", 0}};
#endif

Url **urls;             ///< list of urls to send the report to
uint url_count;

ST_SCHEMA_TABLE *i_s_feedback; ///< table descriptor for our I_S table

/*
  the column names *must* match column names in GLOBAL_VARIABLES and
  GLOBAL_STATUS tables otherwise condition pushdown below will not work
*/
static ST_FIELD_INFO feedback_fields[] =
{
  Show::Column("VARIABLE_NAME",  Show::Varchar(255),  NOT_NULL),
  Show::Column("VARIABLE_VALUE", Show::Varchar(1024), NOT_NULL),
  Show::CEnd()
};

static COND * const OOM= (COND*)1;

/**
  Generate the COND tree for the condition pushdown

  This function takes a list of strings and generates an Item tree
  corresponding to the following expression:

    field LIKE str1 OR field LIKE str2 OR field LIKE str3 OR ...

  where 'field' is the first field in the table - VARIABLE_NAME field -
  and str1, str2... are strings from the list.

  This condition is used to filter the selected rows, emulating

    SELECT * FROM INFORMATION_SCHEMA.GLOBAL_VARIABLES WHERE ...
*/
static COND* make_cond(THD *thd, TABLE_LIST *tables, LEX_STRING *filter)
{
  Item_cond_or *res= NULL;
  /* A reference to this context will be stored in Item_field */
  Name_resolution_context *nrc= new (thd->mem_root) Name_resolution_context;
  LEX_CSTRING &db= tables->db;
  LEX_CSTRING &table= tables->alias;
  LEX_CSTRING &field= tables->table->field[0]->field_name;
  CHARSET_INFO *cs= &my_charset_latin1;

  if (!filter->str || !nrc)
    return 0;

  nrc->init();
  nrc->resolve_in_table_list_only(tables);
  nrc->select_lex= tables->select_lex;

  res= new (thd->mem_root) Item_cond_or(thd);
  if (!res)
    return OOM;

  for (; filter->str; filter++)
  {
    Item_field  *fld= new (thd->mem_root) Item_field(thd, nrc, db, table,
                                                     field);
    Item_string *pattern= new (thd->mem_root) Item_string(thd, filter->str,
                                                          (uint) filter->length, cs);
    Item_string *escape= new (thd->mem_root) Item_string(thd, "\\", 1, cs);

    if (!fld || !pattern || !escape)
      return OOM;

    Item_func_like *like= new (thd->mem_root) Item_func_like(thd, fld, pattern,
                                                             escape, 0);

    if (!like)
      return OOM;

    res->add(like, thd->mem_root);
  }

  if (res->fix_fields(thd, (Item**)&res))
    return OOM;

  return res;
}

/**
  System variables that we want to see in the feedback report
*/
static LEX_STRING vars_filter[]= {
  {C_STRING_WITH_LEN("auto\\_increment%")},
  {C_STRING_WITH_LEN("binlog\\_format")},
  {C_STRING_WITH_LEN("character\\_set\\_%")},
  {C_STRING_WITH_LEN("collation%")},
  {C_STRING_WITH_LEN("engine\\_condition\\_pushdown")},
  {C_STRING_WITH_LEN("event\\_scheduler")},
  {C_STRING_WITH_LEN("feedback\\_%")},
  {C_STRING_WITH_LEN("ft\\_m%")},
  {C_STRING_WITH_LEN("have\\_%")},
  {C_STRING_WITH_LEN("%\\_size")},
  {C_STRING_WITH_LEN("innodb_f%")},
  {C_STRING_WITH_LEN("%\\_length%")},
  {C_STRING_WITH_LEN("%\\_timeout")},
  {C_STRING_WITH_LEN("large\\_%")},
  {C_STRING_WITH_LEN("lc_time_names")},
  {C_STRING_WITH_LEN("log")},
  {C_STRING_WITH_LEN("log_bin")},
  {C_STRING_WITH_LEN("log_output")},
  {C_STRING_WITH_LEN("log_slow_queries")},
  {C_STRING_WITH_LEN("log_slow_time")},
  {C_STRING_WITH_LEN("lower_case%")},
  {C_STRING_WITH_LEN("max_allowed_packet")},
  {C_STRING_WITH_LEN("max_connections")},
  {C_STRING_WITH_LEN("max_prepared_stmt_count")},
  {C_STRING_WITH_LEN("max_sp_recursion_depth")},
  {C_STRING_WITH_LEN("max_user_connections")},
  {C_STRING_WITH_LEN("max_write_lock_count")},
  {C_STRING_WITH_LEN("myisam_recover_options")},
  {C_STRING_WITH_LEN("myisam_repair_threads")},
  {C_STRING_WITH_LEN("myisam_stats_method")},
  {C_STRING_WITH_LEN("myisam_use_mmap")},
  {C_STRING_WITH_LEN("net\\_%")},
  {C_STRING_WITH_LEN("new")},
  {C_STRING_WITH_LEN("old%")},
  {C_STRING_WITH_LEN("optimizer%")},
  {C_STRING_WITH_LEN("profiling")},
  {C_STRING_WITH_LEN("query_cache%")},
  {C_STRING_WITH_LEN("secure_auth")},
  {C_STRING_WITH_LEN("slow_launch_time")},
  {C_STRING_WITH_LEN("sql%")},
  {C_STRING_WITH_LEN("default_storage_engine")},
  {C_STRING_WITH_LEN("sync_binlog")},
  {C_STRING_WITH_LEN("table_definition_cache")},
  {C_STRING_WITH_LEN("table_open_cache")},
  {C_STRING_WITH_LEN("thread_handling")},
  {C_STRING_WITH_LEN("time_zone")},
  {C_STRING_WITH_LEN("version%")},
  {0, 0}
};

/**
  Status variables that we want to see in the feedback report

  (empty list = no WHERE condition)
*/
static LEX_STRING status_filter[]= {{0, 0}};

/**
  Fill our I_S table with data

  This function works by invoking fill_variables() and
  fill_status() of the corresponding I_S tables - to have
  their data UNION-ed in the same target table.
  After that it invokes our own fill_* functions
  from the utils.cc - to get the data that aren't available in the
  I_S.GLOBAL_VARIABLES and I_S.GLOBAL_STATUS.
*/
int fill_feedback(THD *thd, TABLE_LIST *tables, COND *unused)
{
  int res;
  COND *cond;

  tables->schema_table= schema_tables + SCH_GLOBAL_VARIABLES;
  cond= make_cond(thd, tables, vars_filter);
  res= (cond == OOM) ? 1 : fill_variables(thd, tables, cond);

  tables->schema_table= schema_tables + SCH_GLOBAL_STATUS;
  if (!res)
  {
    cond= make_cond(thd, tables, status_filter);
    res= (cond == OOM) ? 1 : fill_status(thd, tables, cond);
  }

  tables->schema_table= i_s_feedback;
  res= res || fill_plugin_version(thd, tables)
           || fill_misc_data(thd, tables)
           || fill_linux_info(thd, tables)
           || fill_collation_statistics(thd, tables);

  return res;
}

/**
   plugin initialization function
*/
static int init(void *p)
{
  i_s_feedback= (ST_SCHEMA_TABLE*) p;
  /* initialize the I_S descriptor structure */
  i_s_feedback->fields_info= feedback_fields; ///< field descriptor
  i_s_feedback->fill_table= fill_feedback;    ///< how to fill the I_S table
  i_s_feedback->idx_field1 = 0;               ///< virtual index on the 1st col

#ifdef HAVE_PSI_INTERFACE
#define PSI_register(X) \
  if(PSI_server) PSI_server->register_ ## X("feedback", X ## _list, array_elements(X ## _list))
#else
#define PSI_register(X) /* no-op */
#endif

  PSI_register(mutex);
  PSI_register(cond);
  PSI_register(thread);

  if (calculate_server_uid(server_uid_buf))
    return 1;

  prepare_linux_info();

#ifndef DBUG_OFF
  if (startup_interval != debug_startup_interval ||
      first_interval != debug_first_interval ||
      interval != debug_interval)
  {
    startup_interval= debug_startup_interval;
    first_interval= debug_first_interval;
    interval= debug_interval;
    user_info= const_cast<char*>("mysql-test");
  }
#endif

  url_count= 0;
  if (*url)
  {
    // now we split url on spaces and store them in Url objects
    int slot;
    char *s, *e;

    for (s= url, url_count= 1; *s; s++)
      if (*s == ' ')
        url_count++;

    urls= (Url **)my_malloc(PSI_INSTRUMENT_ME, url_count*sizeof(Url*), MYF(MY_WME));
    if (!urls)
      return 1;

    for (s= url, e = url+1, slot= 0; e[-1]; e++)
      if (*e == 0 || *e == ' ')
      {
        if (e > s && (urls[slot]= Url::create(s, e - s)))
        {
          if (urls[slot]->set_proxy(http_proxy,
                                    http_proxy ? strlen(http_proxy) : 0))
            sql_print_error("feedback plugin: invalid proxy '%s'",
                            http_proxy ? http_proxy : "");
          slot++;
        }
        else
        {
          if (e > s)
            sql_print_error("feedback plugin: invalid url '%.*s'", (int)(e-s), s);
          url_count--;
        }
        s= e + 1;
      }

    // create a background thread to handle urls, if any
    if (url_count)
    {
      mysql_mutex_init(0, &sleep_mutex, 0);
      mysql_cond_init(0, &sleep_condition, 0);
      shutdown_plugin= false;

      pthread_attr_t attr;
      pthread_attr_init(&attr);
      pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
      if (pthread_create(&sender_thread, &attr, background_thread, 0) != 0)
      {
        sql_print_error("feedback plugin: failed to start a background thread");
        return 1;
      }
    }
    else
      my_free(urls);
  }

  return 0;
}

/**
   plugin deinitialization function
*/
static int free(void *p)
{
  if (url_count)
  {
    mysql_mutex_lock(&sleep_mutex);
    shutdown_plugin= true;
    mysql_cond_signal(&sleep_condition);
    mysql_mutex_unlock(&sleep_mutex);

    for (uint i= 0; i < url_count; i++)
      urls[i]->abort();

    pthread_join(sender_thread, NULL);

    mysql_mutex_destroy(&sleep_mutex);
    mysql_cond_destroy(&sleep_condition);

    for (uint i= 0; i < url_count; i++)
      delete urls[i];
    my_free(urls);
  }
  return 0;
}

#ifdef HAVE_OPENSSL
#define DEFAULT_PROTO "https://"
#else
#define DEFAULT_PROTO "http://"
#endif

static MYSQL_SYSVAR_STR(server_uid, server_uid,
       PLUGIN_VAR_READONLY | PLUGIN_VAR_NOCMDOPT,
       "Automatically calculated server unique id hash.", NULL, NULL, 0);
static MYSQL_SYSVAR_STR(user_info, user_info,
       PLUGIN_VAR_READONLY | PLUGIN_VAR_RQCMDARG,
       "User specified string that will be included in the feedback report.",
       NULL, NULL, "");
static MYSQL_SYSVAR_STR(url, url, PLUGIN_VAR_READONLY | PLUGIN_VAR_RQCMDARG,
       "Space separated URLs to send the feedback report to.", NULL, NULL,
       DEFAULT_PROTO "feedback.mariadb.org/rest/v1/post");
static MYSQL_SYSVAR_ULONG(send_timeout, send_timeout, PLUGIN_VAR_RQCMDARG,
       "Timeout (in seconds) for the sending the report.",
       NULL, NULL, 60, 1, 60*60*24, 1);
static MYSQL_SYSVAR_ULONG(send_retry_wait, send_retry_wait, PLUGIN_VAR_RQCMDARG,
       "Wait this many seconds before retrying a failed send.",
       NULL, NULL, 60, 1, 60*60*24, 1);
static MYSQL_SYSVAR_STR(http_proxy, http_proxy,
                        PLUGIN_VAR_READONLY | PLUGIN_VAR_RQCMDARG,
       "Proxy server host:port.", NULL, NULL,0);

#ifndef DBUG_OFF
static MYSQL_SYSVAR_ULONG(debug_startup_interval, debug_startup_interval,
       PLUGIN_VAR_RQCMDARG, "for debugging only",
       NULL, NULL, startup_interval, 1, INT_MAX32, 1);
static MYSQL_SYSVAR_ULONG(debug_first_interval, debug_first_interval,
       PLUGIN_VAR_RQCMDARG, "for debugging only",
       NULL, NULL, first_interval, 1, INT_MAX32, 1);
static MYSQL_SYSVAR_ULONG(debug_interval, debug_interval,
       PLUGIN_VAR_RQCMDARG, "for debugging only",
       NULL, NULL, interval, 1, INT_MAX32, 1);
#endif

static struct st_mysql_sys_var* settings[] = {
  MYSQL_SYSVAR(server_uid),
  MYSQL_SYSVAR(user_info),
  MYSQL_SYSVAR(url),
  MYSQL_SYSVAR(send_timeout),
  MYSQL_SYSVAR(send_retry_wait),
  MYSQL_SYSVAR(http_proxy),
#ifndef DBUG_OFF
  MYSQL_SYSVAR(debug_startup_interval),
  MYSQL_SYSVAR(debug_first_interval),
  MYSQL_SYSVAR(debug_interval),
#endif
  NULL
};


static struct st_mysql_information_schema feedback =
{ MYSQL_INFORMATION_SCHEMA_INTERFACE_VERSION };

} // namespace feedback

maria_declare_plugin(feedback)
{
  MYSQL_INFORMATION_SCHEMA_PLUGIN,
  &feedback::feedback,
  "FEEDBACK",
  "Sergei Golubchik",
  "MariaDB User Feedback Plugin",
  PLUGIN_LICENSE_GPL,
  feedback::init,
  feedback::free,
  0x0101,
  NULL,
  feedback::settings,
  "1.1",
  MariaDB_PLUGIN_MATURITY_STABLE
}
maria_declare_plugin_end;