/************************************************************************************
   Copyright (C) 2022 MariaDB Corporation AB

   This library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Library General Public
   License as published by the Free Software Foundation; either
   version 2.1 of the License, or (at your option) any later version.

   This library 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
   Library General Public License for more details.

   You should have received a copy of the GNU Library General Public
   License along with this library; if not see <http://www.gnu.org/licenses>
   or write to the Free Software Foundation, Inc.,
   51 Franklin St., Fifth Floor, Boston, MA 02110, USA
*************************************************************************************/

#include "Results.h"
#include "ServerPrepareResult.h"
#include "ClientSidePreparedStatement.h"
#include "ServerSidePreparedStatement.h"
#include "CmdInformationSingle.h"
#include "CmdInformationMultiple.h"
#include "CmdInformationBatch.h"
#include "Protocol.h"
#include "interface/ResultSet.h"
#include "ResultSetMetaData.h"

namespace mariadb
{
  /**
   * Single Text query. /! use internally, because autoincrement value is not right for
   * multi-queries !/
   */
  Results::Results()
    : cmdInformation(nullptr)
    , resultSetScrollType(ResultSet::TYPE_FORWARD_ONLY)
    , sql("") 
  {
  }

  /**
   * Default constructor.
   *
   * @param statement current statement
   * @param fetchSize fetch size
   * @param batch select result possible
   * @param expectedSize expected size
   * @param binaryFormat use binary protocol
   * @param resultSetScrollType one of the following <code>ResultSet</code> constants: <code>
   *     ResultSet.SQL_CURSOR_FORWARD_ONLY</code>, <code>ResultSet.TYPE_SCROLL_INSENSITIVE</code>, or
   *     <code>ResultSet.TYPE_SCROLL_SENSITIVE</code>
   * @param resultSetConcurrency a concurrency type; one of <code>ResultSet.CONCUR_READ_ONLY</code>
   *     or <code>ResultSet.CONCUR_UPDATABLE</code>
   * @param autoGeneratedKeys a flag indicating whether auto-generated keys should be returned; one
   *     of <code>Statement.RETURN_GENERATED_KEYS</code> or <code>Statement.NO_GENERATED_KEYS</code>
   * @param autoIncrement Connection auto-increment value
   * @param sql sql command
   * @param parameters parameters
   */
  Results::Results(
    ClientSidePreparedStatement* _statement,
    int32_t fetchSize,
    bool _batch,
    std::size_t expectedSize,
    bool binaryFormat,
    int32_t resultSetScrollType,
    const SQLString& _sql,
    MYSQL_BIND* _parameters)
    : statement(_statement)
    , parameters(_parameters)
    , cmdInformation(nullptr)
    , fetchSize(fetchSize)
    , batch(_batch)
    , expectedSize(expectedSize)
    , sql(_sql)
    , binaryFormat(binaryFormat)
    , resultSetScrollType(resultSetScrollType)
  {
  }


  Results::Results(
    ServerSidePreparedStatement* _statement,
    int32_t fetchSize,
    bool _batch,
    std::size_t expectedSize,
    bool binaryFormat,
    int32_t resultSetScrollType,
    const SQLString& _sql,
    MYSQL_BIND* _parameters)
    : statement(_statement)
    , parameters(_parameters)
    , cmdInformation(nullptr)
    , fetchSize(fetchSize)
    , batch(_batch)
    , expectedSize(expectedSize)
    , sql(_sql)
    , binaryFormat(binaryFormat)
    , resultSetScrollType(resultSetScrollType)
    , serverPrepResult(dynamic_cast<ServerPrepareResult*>(_statement->getPrepareResult()))
  {
  }


  Results::Results(
    PreparedStatement* _statement,
    int32_t fetchSize,
    bool _batch,
    std::size_t expectedSize,
    bool binaryFormat,
    int32_t resultSetScrollType,
    const SQLString& _sql,
    MYSQL_BIND* _parameters)
    : statement(_statement)
    , parameters(_parameters)
    , cmdInformation(nullptr)
    , fetchSize(fetchSize)
    , batch(_batch)
    , expectedSize(expectedSize)
    , sql(_sql)
    , binaryFormat(binaryFormat)
    , resultSetScrollType(resultSetScrollType)
  {
  }


  Results::~Results() {
    if (resultSet != nullptr) {
      resultSet->close();
      // Mutually forgetting each other. While we should probably just close the RS
      //resultSet->setStatement(nullptr);
    }
    if (statement && statement->getProtocol()->getActiveStreamingResult() == this) {
      statement->getProtocol()->skipAllResults();
      statement->getProtocol()->removeActiveStreamingResult();
    }
  }

  /**
   * Add execution statistics.
   *
   * @param updateCount number of updated rows
   * @param insertId primary key
   * @param moreResultAvailable is there additional packet
   */
  void Results::addStats(int64_t updateCount, bool moreResultAvailable) {
    if (!cmdInformation){
      if (batch){
        cmdInformation.reset(new CmdInformationBatch(expectedSize));
      }else if (moreResultAvailable){
        cmdInformation.reset(new CmdInformationMultiple(expectedSize));
        statement->getProtocol()->setActiveStreamingResult(this);
      }else {
        cmdInformation.reset(new CmdInformationSingle(updateCount));
        return;
      }
    }
    cmdInformation->addSuccessStat(updateCount);
  }

  /**
   * Indicate that result is an Error, to set appropriate results.
   *
   * @param moreResultAvailable indicate if other results (ResultSet or updateCount) are available.
   */
  void Results::addStatsError(bool moreResultAvailable){
    if (!cmdInformation){
      if (batch){
        cmdInformation.reset(new CmdInformationBatch(expectedSize));
      }else if (moreResultAvailable){
        cmdInformation.reset(new CmdInformationMultiple(expectedSize));
      }else {
        cmdInformation.reset(new CmdInformationSingle(0));
        return;
      }
    }
    cmdInformation->addErrorStat();
  }


  int32_t Results::getCurrentStatNumber(){
    return (!cmdInformation) ? 0 : cmdInformation->getCurrentStatNumber();
  }

  /**
   * Add resultSet to results.
   *
   * @param resultSet new resultSet.
   * @param moreResultAvailable indicate if other results (ResultSet or updateCount) are available.
   */
  void Results::addResultSet(ResultSet* _resultSet,bool moreResultAvailable)
  {
    /* Not sure if we gonna need to treat callable resultset differently, but atm it just complicates things w/out much justification */
    /*if (_resultSet->isCallableResult()){
      callableResultSet.reset(_resultSet);
      return;
    }*/

    executionResults.emplace_back(_resultSet);
    if (cachingLocally) {
      _resultSet->cacheCompleteLocally();
    }

    if (!cmdInformation) {
      if (batch) {
        cmdInformation.reset(new CmdInformationBatch(expectedSize));
      } else if (moreResultAvailable) {
        cmdInformation.reset(new CmdInformationMultiple(expectedSize));
        statement->getProtocol()->setActiveStreamingResult(this);
      } else {
        cmdInformation.reset(new CmdInformationSingle(CmdInformation::RESULT_SET_VALUE));
        return;
      }
    }
    cmdInformation->addResultSetStat();
  }


  CmdInformation* Results::getCmdInformation(){
    return cmdInformation.get();
  }


  void Results::setCmdInformation(CmdInformation* _cmdInformation){
    cmdInformation.reset(_cmdInformation);
  }

  /**
   * Indicate that command / batch is finished, so set current resultSet if needed.
   *
   * @return true id has cmdInformation
   */
  bool Results::commandEnd(){
    // It should be NULL here anyway
    resultSet= nullptr;
    if (cmdInformation)
    {
      if (!executionResults.empty() && !cmdInformation->isCurrentUpdateCount())
      {
        currentRs.reset(executionResults.begin()->release());
        executionResults.pop_front();
      }else {
        currentRs.reset(nullptr);
      }
      cmdInformation->setRewrite(rewritten);
      return true;
    }
    else {
      currentRs.reset(nullptr);
    }
    return false;
  }


  ResultSet* Results::getResultSet(){
    return resultSet ? resultSet : currentRs.get();
  }


  /* ResultSet may be used internally, and externally. Once it is released to external user, i.e. application, then it owns it.
     Otherwise we have to care about its destruction */
  ResultSet* Results::releaseResultSet() {
    if (resultSet != nullptr) {
      resultSet->fetchRemaining();
    }
    resultSet= currentRs.release();
    return resultSet;
  }


  ResultSet* Results::getCallableResultSet(){
    return callableResultSet.get();
  }

  /**
   * Load fully current results.
   *
   * <p><i>Lock must be set before using this method</i>
   *
   * @param skip must result be available afterwhile
   * @throws SQLException if any connection error occur
   */
  void Results::loadFully(bool skip, Protocol *guard) {

    ResultSet* rs= nullptr;

    // Only very last of already loaded resultsets might need fetching of remaining rows or caching on our side
    // (i.e. making copy of datat that is already cached on C/C side)
    if (!executionResults.empty()) {
      rs= executionResults.front().get();
    }
    if (rs == nullptr) {
      rs= currentRs.get();
    }
    if (rs == nullptr) {
      rs= resultSet;
    }
    if (rs) {
      if (skip) {
        rs->close();
      }
      else {
        fetchSize= 0;
        //cacheCompleteLocally does fetchRemaining in case of RS streaming
        rs->cacheCompleteLocally();
      }
    }
    
    if (skip) {
      guard->skipAllResults();
      return;
    }

    cachingLocally= true;
    while (guard->hasMoreResults()) {
      // moveToNextResult does that - getResult in case of success
      guard->moveToNextResult(this, serverPrepResult);
    }
    cachingLocally= false;
  }

  /**
   * Connection.abort() has been called, abort remaining active result-set
   *
   * @throws SQLException exception
   */
  void Results::abort()
  {
    if (fetchSize != 0){
      fetchSize= 0;
      if (resultSet)
      {
        resultSet->abort();
      }
      else
      {
        auto firstResult= executionResults.begin();
        if (firstResult != executionResults.end())
        {
          (*firstResult)->abort();
        }
      }
    }
  }

  /**
   * Indicate if result contain result-set that is still streaming from server.
   *
   * @param protocol current protocol
   * @return true if streaming is finished
   */
  bool Results::isFullyLoaded(){
    if (fetchSize == 0 || !resultSet){
      return true;
    }
    return resultSet->isFullyLoaded() && executionResults.empty() && !statement->hasMoreResults();
  }

  /**
   * Position to next resultSet.
   *
   * @param current one of the following <code>Statement</code> constants indicating what should
   *     happen to current <code>ResultSet</code> objects obtained using the method <code>
   *     getResultSet</code>: <code>Statement.CLOSE_CURRENT_RESULT</code>, <code>
   *     Statement.KEEP_CURRENT_RESULT</code>, or <code>Statement.CLOSE_ALL_RESULTS</code>
   * @param protocol current protocol
   * @return true if other resultSet exists.
   * @throws SQLException if any connection error occur.
   */
  bool Results::getMoreResults(bool closeCurrent, Protocol *guard) {

    auto rs= currentRs.get() ? currentRs.get() : resultSet;
    if (rs) {

      if (closeCurrent) {
        rs->close();
      }
      else {
        // for binary results we need to copy everything on our side even if we are not streaming, as it won't be available otherwise
        // once we move to the next result
        rs->cacheCompleteLocally();
      }
    }

    bool haveCachedResult= cmdInformation && cmdInformation->moreResults() && !batch;
    if (!haveCachedResult && guard->hasMoreResults(this)) {
      guard->moveToNextResult(this, serverPrepResult);
      haveCachedResult= true;
    }

    if (!haveCachedResult) {
      return false;
    }

    if (!cmdInformation->isCurrentUpdateCount()) {
      if (closeCurrent && resultSet) {
        resultSet->close();
      }
      if (!executionResults.empty()) {
        currentRs.reset(executionResults.begin()->release());
        executionResults.pop_front();
      }
      return (currentRs.get() != nullptr);
    } else {

      currentRs.reset(nullptr);
      return false;
    }
  }


  /**
   * Tells if there is at least one more result
   *
   * @param protocol current protocol
   * @return true if there is any additional result.
   * @throws SQLException if any connection error occur.
   */
  bool Results::hasMoreResults(Protocol *guard) {
    // cmdInformation->moreResults() as a matter of fact increments internal result index, so it's not good
    bool haveCachedResult= cmdInformation && cmdInformation->hasMoreResults() && !batch;
    return haveCachedResult || guard->hasMoreResults(this);
  }


  int32_t Results::getFetchSize(){
    return fetchSize;
  }


  PreparedStatement* Results::getStatement() {
    return statement;
  }

  bool Results::isBatch(){
    return batch;
  }

  std::size_t Results::getExpectedSize(){
    return expectedSize;
  }

  bool Results::isBinaryFormat(){
    return binaryFormat;
  }

  void Results::removeFetchSize(){
    fetchSize= 0;
  }

  int32_t Results::getResultSetScrollType() const {
    return resultSetScrollType;
  }

  const SQLString& Results::getSql() const {
    return sql;
  }

  MYSQL_BIND* Results::getParameters(){
    return parameters;
  }

  void Results::close(){
    if (resultSet != nullptr) {
      resultSet->close();
      // We don't need to remember it any more
      resultSet= nullptr;
    }
    statement= nullptr;
    fetchSize= 0;
  }


  bool Results::isRewritten(){
    return rewritten;
  }


  void Results::setRewritten(bool rewritten){
    this->rewritten= rewritten;
  }

  /* Resets remembered bare ptr of the current resultSet if it's equal the one checking out.
     @param ptr to the resultset object being destructed
   */
  void Results::checkOut(ResultSet* iamleaving)
  {
    /*This has to be guarded, mutex should be acquired by the caller */
    if (resultSet == iamleaving) {
      resultSet= nullptr;
    }
  }

  /* Loads next result(if needed) and checks if there is no other in queue */
  bool Results::nextIsLast(Protocol* protocol)
  {
    // If we have another resultset already loaded - current is not out params for sure
    if (executionResults.size() == 0)
    {
      if (statement->hasMoreResults()) {
        auto rs= resultSet;
        if (rs == nullptr && currentRs) {
          rs= currentRs.get();
        }
        if (rs) {
          // Technically its cheating since the function is only used for verifying if current result is out params, and it can have only 1 row
          if (rs->rowsCount() > 1) {
            return false;
          }
          rs->cacheCompleteLocally();
        }
        try {
          // If next result is error - it will throw it as an exception
          protocol->moveToNextResult(this, serverPrepResult);
        }
        catch (int /*e*/) {
          // if it's an error - not the last for sure
          return false;
        }
        return !protocol->hasMoreResults(this);
      }
    }
    return false;
  }

} // namespace mariadb
