/*
 * Copyright (c) 2015, 2024, Oracle and/or its affiliates.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License, version 2.0, as
 * published by the Free Software Foundation.
 *
 * This program is designed to work with certain software (including
 * but not limited to OpenSSL) that is licensed under separate terms, as
 * designated in a particular file or component or in included license
 * documentation. The authors of MySQL hereby grant you an additional
 * permission to link the program and your derivative works with the
 * separately licensed software that they have either included with
 * the program or referenced in the documentation.
 *
 * Without limiting anything contained in the foregoing, this file,
 * which is part of Connector/C++, is also subject to the
 * Universal FOSS Exception, version 1.0, a copy of which can be found at
 * https://oss.oracle.com/licenses/universal-foss-exception.
 *
 * 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, version 2.0, 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 <mysqlx/xdevapi.h>

PUSH_SYS_WARNINGS
#include <time.h>
#include <forward_list>
#include <list>
POP_SYS_WARNINGS

#include "impl.h"

using namespace ::mysqlx::impl::common;
using namespace ::mysqlx::internal;
using namespace ::mysqlx;

/*
  Code in this file defines implementations for various CRUD operations used
  by X DevAPI. We use common implementations of these operations.

*/


auto Crud_factory::mk_sql(Session &sess, const mysqlx::string &query)
-> Impl*
{
  return new Op_sql(sess.m_impl, query);
}



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

/*
  Collection CRUD operations
  ==========================
*/


auto Crud_factory::mk_add(Collection &coll) -> Impl*
{
  return new Op_collection_add(
    coll.get_session(), Object_ref(coll)
  );
}


auto Crud_factory::mk_remove(
  Collection &coll, const mysqlx::string &expr
) -> Impl*
{
  return new Op_collection_remove(
    coll.get_session(), Object_ref(coll), expr
  );
}


auto Crud_factory::mk_find(Collection &coll) -> Impl*
{
  return new Op_collection_find(
    coll.get_session(), Object_ref(coll)
  );
}


auto Crud_factory::mk_find(
  Collection &coll, const mysqlx::string &expr
) -> Impl*
{
  return new Op_collection_find(
    coll.get_session(), Object_ref(coll), expr
  );
}


auto Crud_factory::mk_modify(
  Collection &coll, const mysqlx::string &expr
) -> Impl*
{
  return new Op_collection_modify(
    coll.get_session(), Object_ref(coll), expr
  );
}


struct Replace_cmd
  : public Executable<Result, Replace_cmd>
{
  Replace_cmd(
    internal::Shared_session_impl sess,
    const cdk::api::Object_ref &coll,
    const std::string &id,
    const cdk::Expression &doc
  )
  {
    reset(new Op_collection_replace(
      sess, coll, id, doc
    ));
  }
};


struct Upsert_cmd : public Executable<Result, Upsert_cmd>
{
  Upsert_cmd(
    internal::Shared_session_impl sess,
    const cdk::api::Object_ref &coll,
    const std::string &id,
    cdk::Expression &doc
  )
  {
    reset(new Op_collection_upsert(
      sess, coll, id, doc
    ));
  }
};


/*
  A helper class used by Collection_detail::add_or_replace_one().

  It is a wrapper around CDK expression m_expr that describes a document.
  The wrapper forwards this description to a processor, but at the same
  time checks if the value of the (top-level) "_id" field equals the value
  given in the constructor.
*/

struct Value_expr_check_id
  : cdk::Expression
    , cdk::Expression::Processor
    , cdk::Expression::Processor::Doc_prc
{
  mysqlx::Value_expr &m_expr;
  bool m_is_expr;

  Processor *m_prc;
  Doc_prc *m_doc_prc;

  /*
    This class defines m_any_prc member which is used below
    to check the value of "_id" field as reported by the
    source expression. Before using this class, m_id_prc must be
    set to point at the sub-processor that was given for processing
    the value of the "_id" field (this is done by key_val() callback in
    the main class).

    Then all callbacks are forwarded to this sub-processor (or its
    sub-processors) and in case of calling scalar str() callback that
    gives string value of the "_id" field, the check is done first.
  */

  struct Any_processor_check
      : cdk::Expression::Processor::Doc_prc::Any_prc
      , cdk::Expression::Processor::Doc_prc::Any_prc::Scalar_prc
      , cdk::Expression::Processor::Doc_prc::Any_prc::Scalar_prc::Value_prc
  {
    Any_prc *m_id_prc;
    Scalar_prc *m_scalar_prc;
    Value_prc *m_value_prc;
    const std::string &m_id;

    Any_processor_check(const std::string& id)
      : m_id(id)
    {}

    // Any processor implementation

    Scalar_prc* scalar() override
    {
      m_scalar_prc = m_id_prc->scalar();
      return m_scalar_prc ? this : nullptr;
    }

    List_prc* arr() override
    {
      return m_id_prc->arr();
    }

    Doc_prc* doc() override
    {
      return m_id_prc->doc();
    }

    //Scalar processor implementation

    Value_prc* val() override
    {
      m_value_prc = m_scalar_prc->val();
      return m_value_prc ? this : nullptr;
    }

    Args_prc* op(const char *name) override
    {
      return m_scalar_prc->op(name);
    }
    Args_prc* call(const Object_ref&obj) override
    {
      return m_scalar_prc->call(obj);
    }

    void ref(const Column_ref &col, const Doc_path *path) override
    {
      return m_scalar_prc->ref(col, path);
    }
    void ref(const Doc_path &path) override
    {
      return m_scalar_prc->ref(path);
    }

    void param(const string &val) override
    {
      return m_scalar_prc->param(val);
    }

    void param(uint16_t val) override
    {
      return m_scalar_prc->param(val);
    }

    void var(const string &name) override
    {
      m_scalar_prc->var(name);
    }

    // Value processor implementation

    void null() override { m_value_prc->null();}

    void value(cdk::Type_info type,
                       const cdk::Format_info &format,
                       cdk::foundation::bytes val) override
    {
      m_value_prc->value(type, format, val);
    }

    void str(const string &val) override
    {
      if (m_id != val)
        throw mysqlx::Error(R"(Replacement document has an _id that is different than the matched document.)");
      m_value_prc->str(val);
    }

    void num(int64_t  val) override { m_value_prc->num(val); }
    void num(uint64_t val) override { m_value_prc->num(val); }
    void num(float    val) override { m_value_prc->num(val); }
    void num(double   val) override { m_value_prc->num(val); }
    void yesno(bool   val) override { m_value_prc->yesno(val); }

  };

  Any_processor_check m_any_prc;

  Value_expr_check_id(mysqlx::Value_expr &expr, bool is_expr, const std::string& id)
    : m_expr(expr)
    , m_is_expr(is_expr)
    , m_any_prc(id)
  {}

  // Expression implementation

  void process(Processor& prc) const override
  {
    auto self = const_cast<Value_expr_check_id*>(this);
    self->m_prc = &prc;
    m_expr.process(*self);
  }

  // Expression processor implementation

  Scalar_prc* scalar() override
  {
    return m_prc->scalar();
  }

  List_prc* arr() override
  {
    return m_prc->arr();
  }

  Doc_prc* doc() override
  {
    m_doc_prc = m_prc->doc();
    return m_doc_prc ? this : nullptr;
  }

  // Doc_prc implementation

  void doc_begin() override
  {
    m_doc_prc->doc_begin();
  }

  void doc_end() override
  {
    m_doc_prc->doc_end();
  }

  Any_prc* key_val(const string &key) override
  {
    if (string("_id") == key )
    {
      if (m_is_expr)
        mysqlx::throw_error(
          R"(Document "_id" will be replaced by expression "_id")"
        );

      m_any_prc.m_id_prc = m_doc_prc->key_val(key);
      return m_any_prc.m_id_prc ? &m_any_prc : nullptr;
    }
    return m_doc_prc->key_val(key);
  }

};


Result
Collection_detail::add_or_replace_one(
  const mysqlx::string &id, mysqlx::Value &&doc, bool replace
)
{
  /*
    This is implemented by executing Replace_cmd or Upsert_command
    which internally use Op_collection_replace or Op_collection_upsert
    to perform relevant operation on the server.
  */

  Object_ref coll(get_schema().m_name, m_name);
  std::string id_str(id);


  if (!Value::Access::is_expr(doc) &&
      doc.getType() == Value::STRING)
  {
    doc = DbDoc(doc.get<string>());
  }

  /*
    expr is a CDK expression object which describes the document
    to be added.
  */

  Value_expr expr(doc, parser::Parser_mode::DOCUMENT);

  if (replace)
  {
    /*
      Replace_cmd executes Op_collection_replace which picks a document
      with the given id and replaes it with the document given as the last
      argument.

      The document expression is wrapped in Value_expr_check_id to check
      if the "_id" field (if present) stores the correct document id
      and throws error if it is not the case (otherwise Replace_cmd
      would modify the "_id" field to match the given id).
    */

    Value_expr_check_id check_id(expr, Value::Access::is_expr(doc), id_str);

    Replace_cmd cmd(m_sess, coll, id_str, check_id);
    return cmd.execute();
  }
  else
  {
    Upsert_cmd cmd(m_sess, coll, std::string(id), expr);
    return cmd.execute();
  }
}


void Collection_detail::index_drop(const mysqlx::string &name)
{
  Object_ref coll(get_schema().m_name, m_name);
  Op_idx_drop cmd(m_sess, coll, name);
  cmd.execute();
}

void
Collection_detail::index_create(
  const mysqlx::string &name, mysqlx::Value &&spec
)
{
  switch (spec.getType())
  {
  case Value::STRING:
    break;
  default:
    // TODO: support other forms: DbDoc, expr("{...}")?
    throw_error("Index specification must be a string.");
  }

  Object_ref coll(get_schema().m_name, m_name);
  Op_idx_create cmd(m_sess, coll, name, (std::string)spec);
  cmd.execute();

}


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

/*
  Table CRUD operations
  =====================
*/


auto Crud_factory::mk_insert(Table &table) -> Impl*
{
  return new Op_table_insert<Value>(
    table.get_session(), Object_ref(table)
  );
}


auto Crud_factory::mk_select(Table &table) -> Impl*
{
  return new Op_table_select(
    table.get_session(), Object_ref(table)
  );
}


auto Crud_factory::mk_update(Table &table) -> Impl*
{
  return new Op_table_update(
    table.get_session(), Object_ref(table)
  );
}


auto Crud_factory::mk_remove(Table &table) -> Impl*
{
  return new Op_table_remove(
    table.get_session(), Object_ref(table)
  );
}
