#include "stdafx.h"

#include <glib/gunicode.h>
#include "grt/common.h"
#include "grt/grt_manager.h"
#include "grtdb/charset_utils.h"

#include "mysql_sql_parser_fe.h"
#include <sstream>
#include "myx_statement_parser.h"

#define MYSQL_LEX 1
#define MYSQL_SERVER

extern int pthread_dummy(int) { return 0; }

typedef void * YYSTYPE;
#define YYSTYPE_IS_DECLARED

// to stop complaint from compiler about a certain ATTRIBUTE_FORMAT in my_dbug.h
#define DBUG_OFF

#include "myx_lex_helpers.h"
#include "sql_lex.h"
#include "my_sys.h"

const char *MYSQL_DEFAULT_CHARSET= "utf8_bin";


struct Context
{
  Mysql_sql_parser_fe::fe_process_sql_statement_callback cb;
  void* data;
  int err_count;
  bool ignore_dml;
  bool processing_create_statements;
  bool processing_alter_statements;
  bool processing_drop_statements;
  Mysql_sql_parser_fe::SqlMode sql_mode;
};


class Lex_helper
{
public:
  static CHARSET_INFO * charset() { return get_charset_by_name(MYSQL_DEFAULT_CHARSET, MYF(0)); }
  Lex_helper(const char *statement, const Mysql_sql_parser_fe::SqlMode &sql_mode)
  {
    lex_start(&_lex, reinterpret_cast<const unsigned char *>(statement), (unsigned int)strlen(statement));
    _lex.last_item = NULL;
    _lex.charset= charset();
    lex_args.arg1= &_yystype;
    lex_args.arg2= &_lex;
    myx_set_parser_source(statement);

    _lex.sql_mode.MODE_ANSI_QUOTES= sql_mode.MODE_ANSI_QUOTES;
    _lex.sql_mode.MODE_HIGH_NOT_PRECEDENCE= sql_mode.MODE_HIGH_NOT_PRECEDENCE;
    _lex.sql_mode.MODE_PIPES_AS_CONCAT= sql_mode.MODE_PIPES_AS_CONCAT;
    _lex.sql_mode.MODE_NO_BACKSLASH_ESCAPES= sql_mode.MODE_NO_BACKSLASH_ESCAPES;
    _lex.sql_mode.MODE_IGNORE_SPACE= sql_mode.MODE_IGNORE_SPACE;
    _lex.ignore_space= _lex.sql_mode.MODE_IGNORE_SPACE;
  }
  ~Lex_helper()
  {
    myx_free_parser_source();
  }
  LEX * lex() { return &_lex; }
private:
  YYSTYPE _yystype;
  LEX _lex;
};


#define LEX_HELPER(statement, sql_mode) Lex_helper _lex_helper(statement, sql_mode);


bool is_statement_relevant(const char *statement, const Context *context)
{
  LEX_HELPER(statement, context->sql_mode)

  void *token= NULL;
  yylex(&token);
  if (token)
  {
    MyxSQLTreeItem *item= static_cast<MyxSQLTreeItem *>(token);
    const char *value= item->value();
    if (are_strings_eq_ci("USE", value)
        || (context->processing_create_statements && are_strings_eq_ci("CREATE", value))
        || (context->processing_alter_statements && are_strings_eq_ci("ALTER", value))
        || (context->processing_drop_statements && are_strings_eq_ci("DROP", value))
        || are_strings_eq_ci("BEGIN", value)
        || are_strings_eq_ci("SELECT", value)
        )
      return true;
  }

  return false;
}


// this function removes comment braces of form /*!NUMBER */
// making their contents a part of the query itself

void remove_versioning_comments(std::string& sql, CHARSET_INFO *cs, bool *ignore_statement)
{
  const char *begin= sql.c_str();
  const char *ptr= begin;
  const char *endptr= ptr + sql.length();

  while(1)
  {
    for(; (ptr < endptr) && (*ptr != '/'); ptr += max(my_mbcharlen(cs, *ptr),1))
      ;

    if((ptr + 3) >= endptr)
      break;

    if((ptr[1] != '*') || (ptr[2] != '!'))
    {
      ptr += my_mbcharlen(cs, *ptr);
      continue;
    }
    
    const char *start_start= ptr;

    int digit_count= 0;
    for(ptr= ptr + 3; (ptr < endptr) && (my_isdigit(cs, *ptr)); ptr += max(my_mbcharlen(cs, *ptr),1), digit_count++)
      ;

    if(digit_count == 0)
      continue;

    const char *start_end= ptr;

    // in case of parsing mysqldump files, there are 'create table' statements embraced with comments.
    // they are to be ignored, because they relate to views, not tables.
    if (ignore_statement)
      *ignore_statement= (0 == strncmp(ptr, " CREATE TABLE", 13));

    {
      register bool quoted= false;
      register bool escaped= false;
      register bool commented= false;
      int nested_comments_count= 1;
      char quot_sym= 0;

      for(; ptr < endptr-1; ++ptr)
      {
        escaped= (!commented && quoted && ('\\' == ptr[0])) ? !escaped : false;

        switch (ptr[0])
        {
        case '#':
          if ((1 == nested_comments_count) && !quoted)
            commented= true;
          break;

        case '\n':
          commented= false;
          break;

        case '/':
          if (!commented && !quoted && ('*' == ptr[1]))
            ++nested_comments_count;
          break;

        case '*':
          if (!commented && !quoted && ('/' == ptr[1]))
          {
            if (0 == --nested_comments_count)
              goto end;
          }
          break;

        case '"':
        case '\'':
          if (!escaped && !commented)
          {
            if (quoted)
            {
              if (ptr[0] == quot_sym)
              {
                quoted= false;
                quot_sym= 0;
              }
            }
            else
            {
              quoted= true;
              quot_sym= ptr[0];
            }
          }
          break;
        }
      }
end: ;
    }

    if(ptr >= endptr)
      break;

    // replace comments in-place with spaces
    sql.replace(start_start - begin, start_end - start_start, start_end - start_start, ' ');
    sql.replace(ptr - begin, 2, 2, ' ');

    ptr += 2;
  }
}


Mysql_sql_parser_fe::Mysql_sql_parser_fe(grt::GRT *grt)
:
_grt(grt),
ignore_dml(true),
processing_create_statements(true),
processing_alter_statements(true),
processing_drop_statements(true)
{
  grt::ValueRef sql_mode_value= bec::GRTManager::get_instance_for(_grt)->get_app_option("SqlMode");
  if (sql_mode_value.is_valid() && grt::StringRef::can_wrap(sql_mode_value))
  {
    std::string sql_mode_string= toupper(grt::StringRef::cast_from(sql_mode_value));
    std::istringstream iss(sql_mode_string);
    std::string mode;
    while (std::getline(iss, mode, ','))
    {
      if (mode == "ANSI" || mode == "DB2" || mode == "MAXDB" || mode == "MSSQL" || mode == "ORACLE" || mode == "POSTGRESQL")
      {
        sql_mode.MODE_ANSI_QUOTES= true;
        sql_mode.MODE_PIPES_AS_CONCAT= true;
        sql_mode.MODE_IGNORE_SPACE= true;
      }
      else if (mode == "ANSI_QUOTES")
        sql_mode.MODE_ANSI_QUOTES= true;
      else if (mode == "PIPES_AS_CONCAT")
        sql_mode.MODE_PIPES_AS_CONCAT= true;
      else if (mode == "NO_BACKSLASH_ESCAPES")
        sql_mode.MODE_NO_BACKSLASH_ESCAPES= true;
      else if (mode == "IGNORE_SPACE")
        sql_mode.MODE_IGNORE_SPACE= true;
    }
  }
}


GStaticMutex _parser_fe_critical_section= G_STATIC_MUTEX_INIT;


void Mysql_sql_parser_fe::reset()
{
  MyxSQLTreeItem::tree(NULL);
  static bool initialized= false;
  if (!initialized)
  {
    initialized= true;
    lex_init();
  }
}


int Mysql_sql_parser_fe::parse_sql_script(const std::string &sql, fe_process_sql_statement_callback cb, void *user_data)
{
  bec::GStaticMutexLock parser_fe_critical_section(_parser_fe_critical_section);
  reset();
  Context context= {cb, user_data, 0, ignore_dml, processing_create_statements, processing_alter_statements, processing_drop_statements, sql_mode};
  myx_process_sql_statements(sql.c_str(), Lex_helper::charset(), &process_sql_statement_cb, &context, MYX_SPM_NORMAL_MODE);
  return context.err_count;
}


int Mysql_sql_parser_fe::parse_sql_script_file(const std::string &filename, fe_process_sql_statement_callback cb, void *user_data)
{
  bec::GStaticMutexLock parser_fe_critical_section(_parser_fe_critical_section);
  reset();
  Context context= {cb, user_data, 0, ignore_dml, processing_create_statements, processing_alter_statements, processing_drop_statements, sql_mode};
  myx_process_sql_statements_from_file(filename.c_str(), Lex_helper::charset(), &process_sql_statement_cb, &context, MYX_SPM_DELIMS_REQUIRED);
  return context.err_count;
}


int Mysql_sql_parser_fe::process_sql_statement_cb(const MyxStatementParser *splitter, const char *statement, void *context_ptr)
{
  // possible values for result:
  // -1 - statement was ignored
  // 0 - statement was successfully processed
  // 1 - error occured during statement processing
  int result= 0;

  Context *context= reinterpret_cast <Context *> (context_ptr);

  if (!context || !context->cb)
    return -1;

  // check if statement is in utf8 encoding
  if (!g_utf8_validate(statement, -1, NULL))
  {
    int stmt_lc= 1;
    {
      const char *c= statement - 1;
      while (c)
      {
        switch (*++c)
        {
        case '\n':
          ++stmt_lc;
        case '\r':
          break;
        default:
          c= NULL;
          break;
        }
      }
    }
    std::string err_msg= "SQL statement starting from pointed line contains non UTF8 characters";
    context->cb(context->data, splitter, statement, NULL, stmt_lc, 0, 0, err_msg);
    context->err_count++;
    return 1;
  }

  std::string sql(statement);

  // stripe comments before further statement processing because
  // mysqldump puts the whole DDL in comments e.g. for triggers
  bool ignore_statement= false;
  remove_versioning_comments(sql, Lex_helper::charset(), &ignore_statement);

  // filter inappropriate statements
  if (ignore_statement || (context->ignore_dml && !is_statement_relevant(sql.c_str(), context)))
    return -1; // ignored

  LEX_HELPER(sql.c_str(), context->sql_mode)
/*
  //! debugging
  yytokentype res;
  do
  {
    void *yyitem;
    res= (yytokentype) yylex(&yyitem);
    MyxSQLTreeItem *item= (MyxSQLTreeItem *) yyitem;
    std::cout << (item->get_name() ? item->get_name() : "") << ": " << item->get_value() << std::endl;
  }
  while (res);
*/
  myx_parse();

  //deep copy of LEX structure as a workaround for memory corruption (noticed for ubuntu 8.10 x64)
  LEX lex_copy;
  memcpy(&lex_copy, _lex_helper.lex(), sizeof(lex_copy));

  const MyxSQLTreeItem *tree= MyxSQLTreeItem::tree();
/*
  //! debugging
  if (tree)
  {
    std::ofstream fs("treedump.xml");
    fs << *tree;
    fs.flush();
  }
*/
  // in case of syntax error extend err message with context
  std::string err_msg= myx_get_err_msg();
  int err_tok_line_pos= 0;
  int err_tok_len= 0;
  int err_lineno= _lex_helper.lex()->yylineno;
  if (!tree && (0 == strcmp(err_msg.c_str(), "syntax error")))
  {
    //const MyxSQLTreeItem *item= *_lex_helper.lex()->last_item;
    const MyxSQLTreeItem *item = lex_copy.last_item;
    if (item)
    {
      static const size_t MAX_SQL_CONTEXT_SIZE= 80;
      std::string statement_= statement;
      size_t boffset= item->stmt_boffset();
      std::string err_context= statement_.substr(boffset,
        std::min<size_t>(statement_.size() - boffset, MAX_SQL_CONTEXT_SIZE));
      err_msg.clear();
      err_msg
        .append("SQL syntax error near '")
        .append(err_context)
        .append("'");

      determine_token_position(item, splitter, statement, err_lineno, err_tok_line_pos, err_tok_len);
    }
  }

  result= context->cb(context->data, splitter, sql.c_str(), tree, err_lineno, err_tok_line_pos, err_tok_len, err_msg);
  if (0 != result)
    context->err_count++;

  return result;
}


void Mysql_sql_parser_fe::determine_token_position(const MyxSQLTreeItem *item, const MyxStatementParser *splitter, const char *statement, int &lineno, int &token_line_pos, int &token_len)
{
  lineno= item->stmt_lineno();
  const char *tokenbeg= statement + item->stmt_boffset();
  const char *tokenend= statement + item->stmt_eoffset();
//unused  const char *stmtend= statement + strlen(statement);

  // fix token end
  {
    bool initial_token_end_was_lf= false;
    
    if (std::isspace(*(tokenend-1)))
    {
      // this is a case when token end is set to the beginning of the subsequent token
      --tokenend;
    }
    else if (*tokenend == '\n')
    {
      // if initial token end is set to LF then this LF doesn't change effective token lineno
      --tokenend;
      initial_token_end_was_lf= true;
    }

    if (std::isspace(*tokenend) && (tokenend-1 > tokenbeg))
    {
      // there can be multiple trailing token delimiters
      while (std::isspace(*tokenend) && (tokenend > tokenbeg))
      {
        // since every LF in the token increments token lineno, these increments must be discarded to get effective token lineno
        if (*tokenend == '\n')
          --lineno;
        --tokenend;
      }
      ++tokenend;
    }
    else if (initial_token_end_was_lf)
      ++tokenend;
  }

  // find beginning of the line
  const char *linebeg= tokenbeg;
  for (; (linebeg > statement) && ('\n' != *linebeg) && ('\r' != *linebeg); --linebeg) {}
  if (('\n' == *linebeg) || ('\r' == *linebeg))
    ++linebeg;

  CHARSET_INFO *cs= get_charset_by_name(MYSQL_DEFAULT_CHARSET, MYF(0));

  // translate boffset/eoffset into position within the line
  // taking into account active encoding
  const char *ptr= linebeg;
  while (ptr < tokenbeg)
  {
    ptr+= max(my_mbcharlen(cs, *ptr), 1);
    ++token_line_pos;
  }

  // calc token length
  // taking into account active encoding
  while (ptr < tokenend)
  {
    ++token_len;
    ptr+= max(my_mbcharlen(cs, *ptr), 1);
  }

  // first line is special because it may contain part of previous statement ending with statements delimiter
  // while token position is specified relative to current statement
  if (1 == lineno)
    token_line_pos+= splitter->statement_first_line_first_symbol_pos();
}
