// file      : odb/mysql/statement.cxx
// license   : GNU GPL v2; see accompanying LICENSE file

#include <cstring> // std::strlen, std::memmove, std::memset
#include <cassert>

#include <odb/tracer.hxx>

#include <odb/mysql/mysql.hxx>
#include <odb/mysql/database.hxx>
#include <odb/mysql/connection.hxx>
#include <odb/mysql/statement.hxx>
#include <odb/mysql/error.hxx>

#include <odb/mysql/details/config.hxx> // LIBODB_MYSQL_MARIADB

using namespace std;

namespace odb
{
  namespace mysql
  {
    // statement
    //

    statement::
    statement (connection_type& conn,
               const string& text,
               statement_kind sk,
               const binding* process,
               bool optimize)
        : conn_ (conn)
    {
      if (process == 0)
      {
        text_copy_ = text;
        text_ = text_copy_.c_str ();
      }
      else
        text_ = text.c_str (); // Temporary, see init().

      init (text.size (), sk, process, optimize);
    }

    statement::
    statement (connection_type& conn,
               const char* text,
               statement_kind sk,
               const binding* process,
               bool optimize,
               bool copy)
        : conn_ (conn)
    {
      size_t n;

      if (process == 0 && copy)
      {
        text_copy_ = text;
        text_ = text_copy_.c_str ();
        n = text_copy_.size ();
      }
      else
      {
        text_ = text; // Potentially temporary, see init().
        n = strlen (text_);
      }

      init (n, sk, process, optimize);
    }

    void statement::
    init (size_t text_size,
          statement_kind sk,
          const binding* proc,
          bool optimize)
    {
      if (proc != 0)
      {
        switch (sk)
        {
        case statement_select:
          process_select (text_copy_,
                          text_,
                          &proc->bind->buffer, proc->count, sizeof (MYSQL_BIND),
                          '`', '`',
                          optimize);
          break;
        case statement_insert:
          process_insert (text_copy_,
                          text_,
                          &proc->bind->buffer, proc->count, sizeof (MYSQL_BIND),
                          '?');
          break;
        case statement_update:
          process_update (text_copy_,
                          text_,
                          &proc->bind->buffer, proc->count, sizeof (MYSQL_BIND),
                          '?');
          break;
        case statement_delete:
          assert (false);
        }

        text_ = text_copy_.c_str ();
        text_size = text_copy_.size ();
      }

      // Empty statement.
      //
      if (*text_ == '\0')
        return;

      stmt_.reset (conn_.alloc_stmt_handle ());

      conn_.clear ();

      {
        odb::tracer* t;
        if ((t = conn_.transaction_tracer ()) ||
            (t = conn_.tracer ()) ||
            (t = conn_.database ().tracer ()))
          t->prepare (conn_, *this);
      }

      if (mysql_stmt_prepare (stmt_,
                              text_,
                              static_cast<unsigned long> (text_size)) != 0)
        translate_error (conn_, stmt_);
    }

    size_t statement::
    process_bind (MYSQL_BIND* b, size_t n)
    {
      size_t shifts (0);
      for (MYSQL_BIND* e (b + n); b != e;)
      {
        if (b->buffer == 0)
        {
          // It is possible that this array has already been processed
          // (shared among multiple statements).
          //
          if (b->length != 0)
          {
            n -= e - b;
            break;
          }

          e--;

          // Shift the rest of the entries to the left.
          //
          memmove (b, b + 1, (e - b) * sizeof (MYSQL_BIND));

          // Store the original position of the NULL entry at the end.
          //
          e->buffer = 0;
          e->length = reinterpret_cast<unsigned long*> (b + shifts);

          shifts++;
          continue;
        }

        b++;
      }

      return n - shifts;
    }

    void statement::
    restore_bind (MYSQL_BIND* b, size_t n)
    {
      for (MYSQL_BIND* e (b + n - 1); e->buffer == 0 && e->length != 0;)
      {
        MYSQL_BIND* p (reinterpret_cast<MYSQL_BIND*> (e->length));

        // Shift the entries from the specified position to the right.
        //
        memmove (p + 1, p, (e - p) * sizeof (MYSQL_BIND));

        // Restore the original NULL entry.
        //
        memset (p, 0, sizeof (MYSQL_BIND));
      }
    }

    statement::
    ~statement ()
    {
      if (stmt_ != 0)
      {
        {
          odb::tracer* t;
          if ((t = conn_.transaction_tracer ()) ||
              (t = conn_.tracer ()) ||
              (t = conn_.database ().tracer ()))
            t->deallocate (conn_, *this);
        }

        // Let the connection handle the release of the statement (it
        // may delay the actual freeing if it will mess up the currently
        // active statement).
        //
        conn_.free_stmt_handle (stmt_);
      }
    }

    const char* statement::
    text () const
    {
      return text_;
    }

    void statement::
    cancel ()
    {
    }

    // select_statement
    //

    select_statement::
    ~select_statement ()
    {
      assert (freed_);
    }

    select_statement::
    select_statement (connection_type& conn,
                      const string& text,
                      bool process,
                      bool optimize,
                      binding& param,
                      binding& result)
        : statement (conn,
                     text, statement_select,
                     (process ? &result : 0), optimize),
          end_ (false),
          cached_ (false),
          freed_ (true),
          rows_ (0),
          param_ (&param),
          param_version_ (0),
          result_ (result),
          result_version_ (0)
    {
    }

    select_statement::
    select_statement (connection_type& conn,
                      const char* text,
                      bool process,
                      bool optimize,
                      binding& param,
                      binding& result,
                      bool copy_text)
        : statement (conn,
                     text, statement_select,
                     (process ? &result : 0), optimize,
                     copy_text),
          end_ (false),
          cached_ (false),
          freed_ (true),
          rows_ (0),
          param_ (&param),
          param_version_ (0),
          result_ (result),
          result_version_ (0)
    {
    }

    select_statement::
    select_statement (connection_type& conn,
                      const string& text,
                      bool process,
                      bool optimize,
                      binding& result)
        : statement (conn,
                     text, statement_select,
                     (process ? &result : 0), optimize),
          end_ (false),
          cached_ (false),
          freed_ (true),
          rows_ (0),
          param_ (0),
          result_ (result),
          result_version_ (0)
    {
    }

    select_statement::
    select_statement (connection_type& conn,
                      const char* text,
                      bool process,
                      bool optimize,
                      binding& result,
                      bool copy_text)
        : statement (conn,
                     text, statement_select,
                     (process ? &result : 0), optimize,
                     copy_text),
          end_ (false),
          cached_ (false),
          freed_ (true),
          rows_ (0),
          param_ (0),
          result_ (result),
          result_version_ (0)
    {
    }

    void select_statement::
    execute ()
    {
      assert (freed_);

      conn_.clear ();

      end_ = false;
      rows_ = 0;

      if (mysql_stmt_reset (stmt_))
        translate_error (conn_, stmt_);

      if (param_ != 0 && param_version_ != param_->version)
      {
        // For now cannot have NULL entries.
        //
        if (mysql_stmt_bind_param (stmt_, param_->bind))
          translate_error (conn_, stmt_);

        param_version_ = param_->version;
      }

      {
        odb::tracer* t;
        if ((t = conn_.transaction_tracer ()) ||
            (t = conn_.tracer ()) ||
            (t = conn_.database ().tracer ()))
          t->execute (conn_, *this);
      }

      if (mysql_stmt_execute (stmt_))
        translate_error (conn_, stmt_);

      // This flag appears to be cleared once we start processing the
      // result, so we have to cache it for free_result() below.
      //
#if MYSQL_VERSION_ID >= 50503
      out_params_ = (conn_.handle ()->server_status & SERVER_PS_OUT_PARAMS);
#endif

      freed_ = false;
      conn_.active (this);
    }

    void select_statement::
    cache ()
    {
      if (!cached_)
      {
        if (!end_)
        {
          if (mysql_stmt_store_result (stmt_))
            translate_error (conn_, stmt_);

          // mysql_stmt_num_rows() returns the number of rows that have been
          // fetched by store_result for MySQL and the total number of rows
          // in the result set for MariaDB.
          //
#ifndef LIBODB_MYSQL_MARIADB
          size_ = rows_ + static_cast<size_t> (mysql_stmt_num_rows (stmt_));
#else
          size_ = static_cast<size_t> (mysql_stmt_num_rows (stmt_));
#endif
        }
        else
          size_ = rows_;

        cached_ = true;
      }
    }

    select_statement::result select_statement::
    fetch (bool next)
    {
      if (result_version_ != result_.version)
      {
        size_t count (process_bind (result_.bind, result_.count));

        // Make sure that the number of columns in the result returned by
        // the database matches the number that we expect. A common cause
        // of this assertion is a native view with a number of data members
        // not matching the number of columns in the SELECT-list.
        //
        assert (mysql_stmt_field_count (stmt_) == count);

        if (mysql_stmt_bind_result (stmt_, result_.bind))
          translate_error (conn_, stmt_);

        if (count != result_.count)
          restore_bind (result_.bind, result_.count);

        result_version_ = result_.version;
      }

      if (!next && rows_ != 0)
      {
        assert (cached_);
        mysql_stmt_data_seek (stmt_, static_cast<my_ulonglong> (rows_ - 1));
      }

      int r (mysql_stmt_fetch (stmt_));

      switch (r)
      {
      case 0:
        {
          if (next)
            rows_++;
          return success;
        }
      case MYSQL_NO_DATA:
        {
          end_ = true;
          return no_data;
        }
      case MYSQL_DATA_TRUNCATED:
        {
          if (next)
            rows_++;
          return truncated;
        }
      default:
        {
          translate_error (conn_, stmt_);
          return no_data; // Never reached.
        }
      }
    }

    void select_statement::
    refetch ()
    {
      // Re-fetch columns that were truncated.
      //
      unsigned int col (0);
      for (size_t i (0); i < result_.count; ++i)
      {
        MYSQL_BIND& b (result_.bind[i]);

        if (b.buffer == 0) // Skip NULL entries.
          continue;

        if (*b.error)
        {
          *b.error = 0;

          if (mysql_stmt_fetch_column (stmt_, &b, col, 0))
            translate_error (conn_, stmt_);
        }

        col++;
      }
    }

    void select_statement::
    free_result ()
    {
      if (!freed_)
      {
        // If this is a stored procedure call, then we have multiple
        // results. The first is the rowset that is the result of the
        // procedure (actually, it can be several rowsets if, for
        // example, the procedure executes multiple SELECT statements,
        // but we don't support this). This result we have just handled.
        // Next, there could be another rowset with just one row which
        // contains the output variable values (OUT and INOUT; actually,
        // if the procedure does not have any SELECT statements, then
        // this will be the first result and we have just handled it).
        // Finally, the last result is always the stored procedure status
        // (not clear how to get this value; all MySQL sample code simply
        // ignores it).
        //
        // So what we need to do here is read and ignore these other
        // results since otherwise MySQL won't let us execute any
        // subsequent statements. Calling mysql_stmt_next_result()
        // until it tells us there is no more results seems to do
        // the trick.
        //
        // mysql_stmt_next_result() is only available since 5.5.3.
        // Checking the source code reveals that it does call
        // mysql_next_result() (which is available prior to 5.5.3)
        // but also does some other house keeping. So it is not
        // clear if it is possible to emulate the below logic for
        // prior MySQL versions.
        //

        // Handling OUT parameters requires a special Voodoo dance:
        // we have to fetch the row itself and we have to call fetch
        // again and get MYSQL_NO_DATA. Without doing these exact
        // steps the server simply drops the connection. Go figure.
        //
#if MYSQL_VERSION_ID >= 50503
        if (out_params_)
        {
          if (mysql_stmt_fetch (stmt_) != MYSQL_NO_DATA)
            translate_error (conn_, stmt_);
        }
#endif

        if (mysql_stmt_free_result (stmt_))
          translate_error (conn_, stmt_);

#if MYSQL_VERSION_ID >= 50503
        {
          int s;
          while ((s = mysql_stmt_next_result (stmt_)) == 0)
          {
            if (mysql_stmt_field_count (stmt_) != 0)
            {
              // The same Voodoo dance as above.
              //
              if (conn_.handle ()->server_status & SERVER_PS_OUT_PARAMS)
              {
                if (mysql_stmt_fetch (stmt_))
                  translate_error (conn_, stmt_);

                if (mysql_stmt_fetch (stmt_) != MYSQL_NO_DATA)
                  translate_error (conn_, stmt_);
              }

              if (mysql_stmt_free_result (stmt_))
                translate_error (conn_, stmt_);
            }
          }

          if (s != -1)
            translate_error (conn_, stmt_);
        }
#endif

        if (conn_.active () == this)
          conn_.active (0);

        end_ = true;
        cached_ = false;
        freed_ = true;
        rows_ = 0;
      }
    }

    void select_statement::
    cancel ()
    {
      // If we cached the result, don't free it just yet.
      //
      if (!cached_ || end_)
        free_result ();
      else
        conn_.active (0);
    }

    // insert_statement
    //

    insert_statement::
    ~insert_statement ()
    {
    }

    insert_statement::
    insert_statement (connection_type& conn,
                      const string& text,
                      bool process,
                      binding& param,
                      binding* returning)
        : statement (conn,
                     text, statement_insert,
                     (process ? &param : 0), false),
          param_ (param),
          param_version_ (0),
          returning_ (returning)
    {
    }

    insert_statement::
    insert_statement (connection_type& conn,
                      const char* text,
                      bool process,
                      binding& param,
                      binding* returning,
                      bool copy_text)
        : statement (conn,
                     text, statement_insert,
                     (process ? &param : 0), false,
                     copy_text),
          param_ (param),
          param_version_ (0),
          returning_ (returning)
    {
    }

    bool insert_statement::
    execute ()
    {
      conn_.clear ();

      if (mysql_stmt_reset (stmt_))
        translate_error (conn_, stmt_);

      if (param_version_ != param_.version)
      {
        // It turns out that MariaDB requires the variable referenced by
        // MYSQL_BIND::length member to be initialized even for the NULL
        // values. Otherwise, mysql_stmt_execute() may fail with the
        // CR_OUT_OF_MEMORY error.
        //
#ifdef LIBODB_MYSQL_MARIADB
        for (MYSQL_BIND *b (param_.bind), *e (b + param_.count); b != e; ++b)
        {
          if (b->is_null != 0 && *b->is_null && b->length != 0)
            *b->length = 0;
        }
#endif

        size_t count (process_bind (param_.bind, param_.count));

        if (mysql_stmt_bind_param (stmt_, param_.bind))
          translate_error (conn_, stmt_);

        if (count != param_.count)
          restore_bind (param_.bind, param_.count);

        param_version_ = param_.version;
      }

      {
        odb::tracer* t;
        if ((t = conn_.transaction_tracer ()) ||
            (t = conn_.tracer ()) ||
            (t = conn_.database ().tracer ()))
          t->execute (conn_, *this);
      }

      if (mysql_stmt_execute (stmt_))
      {
        // An auto-assigned object id should never cause a duplicate
        // primary key.
        //
        if (returning_ == 0 && mysql_stmt_errno (stmt_) == ER_DUP_ENTRY)
          return false;
        else
          translate_error (conn_, stmt_);
      }

      if (returning_ != 0)
      {
        unsigned long long i (mysql_stmt_insert_id (stmt_));

        MYSQL_BIND& b (returning_->bind[0]);
        void* v (b.buffer);

        switch (b.buffer_type)
        {
        case MYSQL_TYPE_TINY:
          *static_cast<unsigned char*> (v) = static_cast<unsigned char> (i);
          break;
        case MYSQL_TYPE_SHORT:
          *static_cast<unsigned short*> (v) = static_cast<unsigned short> (i);
          break;
        case MYSQL_TYPE_LONG:
          *static_cast<unsigned int*> (v) = static_cast<unsigned int> (i);
          break;
        case MYSQL_TYPE_LONGLONG:
          *static_cast<unsigned long long*> (v) = i;
          break;
        default:
          assert (false); // Auto id column type is not an integer.
        }

        *b.is_null = false;
      }

      return true;
    }

    // update_statement
    //

    update_statement::
    ~update_statement ()
    {
    }

    update_statement::
    update_statement (connection_type& conn,
                      const string& text,
                      bool process,
                      binding& param)
        : statement (conn,
                     text, statement_update,
                     (process ? &param : 0), false),
          param_ (param),
          param_version_ (0)
    {
    }

    update_statement::
    update_statement (connection_type& conn,
                      const char* text,
                      bool process,
                      binding& param,
                      bool copy_text)
        : statement (conn,
                     text, statement_update,
                     (process ? &param : 0), false,
                     copy_text),
          param_ (param),
          param_version_ (0)
    {
    }

    unsigned long long update_statement::
    execute ()
    {
      conn_.clear ();

      if (mysql_stmt_reset (stmt_))
        translate_error (conn_, stmt_);

      if (param_version_ != param_.version)
      {
        // It turns out that MariaDB requires the variable referenced by
        // MYSQL_BIND::length member to be initialized even for the NULL
        // values (see above for details).
        //
#ifdef LIBODB_MYSQL_MARIADB
        for (MYSQL_BIND *b (param_.bind), *e (b + param_.count); b != e; ++b)
        {
          if (b->is_null != 0 && *b->is_null && b->length != 0)
            *b->length = 0;
        }
#endif

        size_t count (process_bind (param_.bind, param_.count));

        if (mysql_stmt_bind_param (stmt_, param_.bind))
          translate_error (conn_, stmt_);

        if (count != param_.count)
          restore_bind (param_.bind, param_.count);

        param_version_ = param_.version;
      }

      {
        odb::tracer* t;
        if ((t = conn_.transaction_tracer ()) ||
            (t = conn_.tracer ()) ||
            (t = conn_.database ().tracer ()))
          t->execute (conn_, *this);
      }

      if (mysql_stmt_execute (stmt_))
        translate_error (conn_, stmt_);

      my_ulonglong r (mysql_stmt_affected_rows (stmt_));

      if (r == static_cast<my_ulonglong> (-1))
        translate_error (conn_, stmt_);

      return static_cast<unsigned long long> (r);
    }

    // delete_statement
    //

    delete_statement::
    ~delete_statement ()
    {
    }

    delete_statement::
    delete_statement (connection_type& conn,
                      const string& text,
                      binding& param)
        : statement (conn,
                     text, statement_delete,
                     0, false),
          param_ (param),
          param_version_ (0)
    {
    }

    delete_statement::
    delete_statement (connection_type& conn,
                      const char* text,
                      binding& param,
                      bool copy_text)
        : statement (conn,
                     text, statement_delete,
                     0, false,
                     copy_text),
          param_ (param),
          param_version_ (0)
    {
    }

    unsigned long long delete_statement::
    execute ()
    {
      conn_.clear ();

      if (mysql_stmt_reset (stmt_))
        translate_error (conn_, stmt_);

      if (param_version_ != param_.version)
      {
        // Cannot have NULL entries for now.
        //
        if (mysql_stmt_bind_param (stmt_, param_.bind))
          translate_error (conn_, stmt_);

        param_version_ = param_.version;
      }

      {
        odb::tracer* t;
        if ((t = conn_.transaction_tracer ()) ||
            (t = conn_.tracer ()) ||
            (t = conn_.database ().tracer ()))
          t->execute (conn_, *this);
      }

      if (mysql_stmt_execute (stmt_))
        translate_error (conn_, stmt_);

      my_ulonglong r (mysql_stmt_affected_rows (stmt_));

      if (r == static_cast<my_ulonglong> (-1))
        translate_error (conn_, stmt_);

      return static_cast<unsigned long long> (r);
    }
  }
}
