//    OpenVPN -- An application to securely tunnel IP networks
//               over a single port, with support for SSL/TLS-based
//               session authentication and key exchange,
//               packet encryption, packet authentication, and
//               packet compression.
//
//    Copyright (C) 2012-2017 OpenVPN Inc.
//
//    This program is free software: you can redistribute it and/or modify
//    it under the terms of the GNU Affero General Public License Version 3
//    as published by the Free Software Foundation.
//
//    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 Affero General Public License for more details.
//
//    You should have received a copy of the GNU Affero General Public License
//    along with this program in the COPYING file.
//    If not, see <http://www.gnu.org/licenses/>.

#ifndef OPENVPN_PKI_X509TRACK_H
#define OPENVPN_PKI_X509TRACK_H

#include <string>
#include <vector>

#include <openvpn/common/exception.hpp>
#include <openvpn/common/options.hpp>
#include <openvpn/common/arraysize.hpp>
#include <openvpn/common/string.hpp>
#include <openvpn/common/to_string.hpp>

namespace openvpn {
  namespace X509Track {

    enum Type {
      UNDEF=-1,
      SERIAL,
      SERIAL_HEX,
      SHA1,
      CN,
      C,
      L,
      ST,
      O,
      OU,
      EMAIL,
      N_TYPES,
    };

    static const char *const names[] = { // CONST GLOBAL
      "SERIAL",
      "SERIAL_HEX",
      "SHA1",
      "CN",
      "C",
      "L",
      "ST",
      "O",
      "OU",
      "emailAddress",
    };

    OPENVPN_EXCEPTION(x509_track_error);

    inline const char *name(const Type type)
    {
      static_assert(N_TYPES == array_size(names), "x509 names array inconsistency");
      if (type >= 0 && type < N_TYPES)
	return names[type];
      else
	return "UNDEF";
    }

    inline Type parse_type(const std::string& name)
    {
      for (size_t i = 0; i < N_TYPES; ++i)
	if (name == names[i])
	  return Type(i);
      return UNDEF;
    }

    struct Config
    {
      Config(const Type type_arg, const bool full_chain_arg)
	: type(type_arg),
	  full_chain(full_chain_arg)
      {
      }

      Config(const std::string& spec)
      {
	full_chain = (spec.length() > 0 && spec[0] == '+');
	type = parse_type(spec.substr(full_chain ? 1 : 0));
	if (type == UNDEF)
	  throw Exception("cannot parse attribute '" + spec + "'");
      }

      std::string to_string() const
      {
	std::string ret;
	if (full_chain)
	  ret += '+';
	ret += name(type);
	return ret;
      }

      bool depth_match(const int depth) const
      {
	return !depth || full_chain;
      }

      Type type;
      bool full_chain;
    };

    struct ConfigSet : public std::vector<Config>
    {
      ConfigSet() {}

      ConfigSet(const OptionList& opt,
		const bool include_serial,
		const bool include_serial_hex)
      {
	const auto* xt = opt.get_index_ptr("x509-track");
	if (xt)
	  {
	    for (const auto &i : *xt)
	      {
		try {
		  const Option& o = opt[i];
		  o.touch();
		  emplace_back(o.get(1, 64));
		}
		catch (const std::exception& e)
		  {
		    throw x509_track_error(e.what());
		  }
	      }
	  }

	if (include_serial && !exists(SERIAL))
	  emplace_back(SERIAL, true);
	if (include_serial_hex && !exists(SERIAL_HEX))
	  emplace_back(SERIAL_HEX, true);
      }

      bool exists(const Type t) const
      {
	for (auto &c : *this)
	  if (c.type == t)
	    return true;
	return false;
      }

      std::string to_string() const
      {
	std::string ret;
	for (auto &c : *this)
	  {
	    ret += c.to_string();
	    ret += '\n';
	  }
	return ret;
      }
    };

    struct KeyValue
    {
      KeyValue(const Type type_arg,
	       const int depth_arg,
	       std::string value_arg)
	: type(type_arg),
	  depth(depth_arg),
	  value(std::move(value_arg))
      {
      }

      std::string to_string(const bool omi_form) const
      {
	std::string ret;
	ret.reserve(128);
	if (omi_form)
	  ret += ">CLIENT:ENV,";
	ret += key_name();
	ret += '=';
	ret += string::reduce_spaces(value, ' ');
	return ret;
      }

      std::string key_name() const
      {
	switch (type)
	  {
	  case SERIAL:
	    return "tls_serial_" + openvpn::to_string(depth);
	  case SERIAL_HEX:
	    return "tls_serial_hex_" + openvpn::to_string(depth);
	  default:
	    return "X509_" + openvpn::to_string(depth) + '_' + name(type);
	  }
      }

      Type type = UNDEF;
      int depth = 0;
      std::string value;
    };

    struct Set : public std::vector<KeyValue>
    {
      std::string to_string(const bool omi_form) const
      {
	std::string ret;
	ret.reserve(512);
	for (auto &kv : *this)
	  {
	    ret += kv.to_string(omi_form);
	    if (omi_form)
	      ret += '\r';
	    ret += '\n';
	  }
	return ret;
      }
    };

  }
}

#endif
