/*
 * Copyright (c) 2017, 2018, Oracle and/or its affiliates. All rights reserved.
 *
 * 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 also distributed 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 included with
 * MySQL.
 *
 * Without limiting anything contained in the foregoing, this file,
 * which is part of MySQL Connector/C++, is also subject to the
 * Universal FOSS Exception, version 1.0, a copy of which can be found at
 * http://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
 */

#ifndef MYSQLX_COMMON_SETTINGS_H
#define MYSQLX_COMMON_SETTINGS_H

/*
  Classes and code handling session settings. They are used to process session
  creation options, check their consistency and present the settings in the
  form expected by CDK.

  Known session options and their values are defined
  in mysql_common_constants.h header as SESSION_OPTION_LIST() and accompanying
  macros.
*/

#include "../common_constants.h"
#include "value.h"

#include <vector>
#include <map>
#include <bitset>
#include <sstream>

namespace cdk {
namespace ds {

class Multi_source;

}}


namespace mysqlx {
namespace common {


/*
  Class for storing session configuration settings.
*/

class PUBLIC_API Settings_impl
{
public:

  /*
    Enumerations with available session options and their values.
  */

#define SETTINGS_OPT_ENUM_str(X,N)  X = N,
#define SETTINGS_OPT_ENUM_num(X,N)  X = N,
#define SETTINGS_OPT_ENUM_any(X,N)  X = N,

  enum class Option_impl {
    SESSION_OPTION_LIST(SETTINGS_OPT_ENUM)
    LAST
  };

  /*
    Enumerations with available client options and their values.
  */

#define CLIENT_OPT_ENUM_str(X,N)  X = N,
#define CLIENT_OPT_ENUM_bool(X,N)  X = N,
#define CLIENT_OPT_ENUM_num(X,N)  X = N,
#define CLIENT_OPT_ENUM_any(X,N)  X = N,
#define CLIENT_OPT_ENUM_end(X,N)  X = N,

  enum class Client_option_impl {
    CLIENT_OPTION_LIST(CLIENT_OPT_ENUM)
  };

  static  const char* option_name(Option_impl opt);
  static  const char* option_name(Client_option_impl opt);


#define SETTINGS_VAL_ENUM(X,N)  X = N,

  enum class SSL_mode {
    SSL_MODE_LIST(SETTINGS_VAL_ENUM)
    LAST
  };

  static  const char* ssl_mode_name(SSL_mode mode);


  enum class Auth_method {
    AUTH_METHOD_LIST(SETTINGS_VAL_ENUM)
    LAST
  };

  static  const char* auth_method_name(Auth_method method);

protected:

  using Value = common::Value;
  using opt_val_t = std::pair<Option_impl, Value>;
  // TODO: use multimap instead?
  using option_list_t = std::vector<opt_val_t>;
  using iterator = option_list_t::const_iterator;

  using client_option_list_t = std::map<Client_option_impl, Value>;
  using client_iterator = option_list_t::const_iterator;

public:

  /*
    Examine settings stored in this object.
  */

  bool has_option(Option_impl) const;
  const Value& get(Option_impl) const;

  bool has_option(Client_option_impl) const;
  const Value& get(Client_option_impl) const;


  // Iterating over options stored in this object.

  iterator begin() const
  {
    return m_data.m_options.cbegin();
  }

  iterator end() const
  {
    return m_data.m_options.cend();
  }

  /*
    Clear individual or all settings.
  */

  void clear();
  void erase(Option_impl);
  void erase(Client_option_impl);

  /*
    Session options include connection options that specify data source
    (one or many) for which given session is created. This method initializes
    CDK Multi_source object to describe the data source(s) based on the
    connection options.
  */

  void get_data_source(cdk::ds::Multi_source&);


  // Set options based on URI

  void set_from_uri(const std::string &);

  // Set Client options based on JSON object

  void set_client_opts(const std::string &);

  // Set Client options from othe Settings object

  void set_client_opts(const Settings_impl &);

  /*
    Public API has no methods to directly set individual options. Instead,
    to change session settings implementation should create a Setter object
    and use its methods to do the changes. A Settings_impl::Setter object
    provides "transactional" semantics for changing session options -- only
    consistent option changes modify the original Settings_impl object.

    Note: This Setter class is defined in "implementation" header
    common/settings.h. The public API leaves it undefined.
  */

  class Setter;

protected:

  struct PUBLIC_API Data
  {
    DLL_WARNINGS_PUSH
    option_list_t           m_options;
    client_option_list_t    m_client_options;
    DLL_WARNINGS_POP
    unsigned m_host_cnt = 0;
    bool m_user_priorities = false;
    bool m_ssl_ca = false;
    SSL_mode m_ssl_mode = SSL_mode::LAST;
    bool m_tcpip = false; // set to true if TCPIP connection was specified
    bool m_sock = false;  // set to true if socket connection was specified

    void erase(Option_impl);
    void erase(Client_option_impl);
  };

  Data m_data;

};


#define SETTINGS_OPT_NAME_str(X,N)  case N: return #X;
#define SETTINGS_OPT_NAME_num(X,N)  case N: return #X;
#define SETTINGS_OPT_NAME_any(X,N)  case N: return #X;

inline
const char* Settings_impl::option_name(Option_impl opt)
{
  switch (unsigned(opt))
  {
    SESSION_OPTION_LIST(SETTINGS_OPT_NAME)
  default:
    return nullptr;
  }
}

#define CLIENT_OPT_NAME_str(X,N)  case N: return #X;
#define CLIENT_OPT_NAME_bool(X,N)  case N: return #X;
#define CLIENT_OPT_NAME_num(X,N)  case N: return #X;
#define CLIENT_OPT_NAME_any(X,N)  case N: return #X;
#define CLIENT_OPT_NAME_end(X,N)  case N: throw_error("Unexpected Option"); return #X;


inline
const char* Settings_impl::option_name(Client_option_impl opt)
{
  switch (unsigned(opt))
  {
    CLIENT_OPTION_LIST(CLIENT_OPT_NAME)
  default:
    return nullptr;
  }
}


#define SETTINGS_VAL_NAME(X,N) case N: return #X;

inline
const char* Settings_impl::ssl_mode_name(SSL_mode mode)
{
  switch (unsigned(mode))
  {
    SSL_MODE_LIST(SETTINGS_VAL_NAME)
  default:
    return nullptr;
  }
}

inline
const char* Settings_impl::auth_method_name(Auth_method method)
{
  switch (unsigned(method))
  {
    SSL_MODE_LIST(SETTINGS_VAL_NAME)
  default:
    return nullptr;
  }
}


/*
  Note: For options that can repeat, returns the last value.
*/

inline
const common::Value& Settings_impl::get(Option_impl opt) const
{
  using std::find_if;

  auto it = find_if(m_data.m_options.crbegin(), m_data.m_options.crend(),
    [opt](opt_val_t el) -> bool { return el.first == opt; }
  );

  static Value null_value;

  if (it == m_data.m_options.crend())
    return null_value;

  return it->second;
}

inline
const common::Value& Settings_impl::get(Client_option_impl opt) const
{
  static Value null_value;

  auto it = m_data.m_client_options.find(opt);

  if (it == m_data.m_client_options.cend())
    return null_value;

  return it->second;
}


inline
bool Settings_impl::has_option(Option_impl opt) const
{
  return m_data.m_options.cend() !=
    find_if(m_data.m_options.cbegin(), m_data.m_options.cend(),
      [opt](opt_val_t el) -> bool { return el.first == opt; }
    );
}


inline
bool Settings_impl::has_option(Client_option_impl opt) const
{
  return m_data.m_client_options.find(opt) != m_data.m_client_options.cend();
}

inline
void Settings_impl::erase(Option_impl opt)
{
  m_data.erase(opt);
}

inline
void Settings_impl::erase(Client_option_impl opt)
{
  m_data.erase(opt);
}

/*
  Note: Removes all occurrences of the given option. Also updates the context
  used for checking option consistency.
*/

inline
void Settings_impl::Data::erase(Option_impl opt)
{
  remove_from(m_options,
    [opt](opt_val_t el) -> bool
    {
      return el.first == opt;
    }
  );

  /*
    TODO: removing HOST from multi-host settings can leave "orphaned"
    PORT/PRIORITY settings. Do we correctly detect that?
  */

  switch (opt)
  {
  case Option_impl::HOST:
    m_host_cnt = 0;
    FALLTHROUGH;
  case Option_impl::PORT:
    if (0 == m_host_cnt)
      m_tcpip = false;
    break;
  case Option_impl::SOCKET:
    m_sock = false;
    break;
  case Option_impl::PRIORITY:
    m_user_priorities = false;
    break;
  case Option_impl::SSL_CA:
    m_ssl_ca = false;
    break;
  case Option_impl::SSL_MODE:
    m_ssl_mode = SSL_mode::LAST;
    break;
  default:
    break;
  }
}

inline
void Settings_impl::Data::erase(Client_option_impl opt)
{
  m_client_options.erase(opt);
}

}  // common namespace
}  // mysqlx namespace

#endif