//    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_APPLECRYPTO_CF_CF_H
#define OPENVPN_APPLECRYPTO_CF_CF_H

#include <string>
#include <iostream>
#include <sstream>
#include <algorithm>
#include <utility>
#include <exception>

#include <CoreFoundation/CoreFoundation.h>

// Wrapper classes for Apple Core Foundation objects.

#define OPENVPN_CF_WRAP(cls, castmeth, cftype, idmeth) \
    template <> \
    struct Type<cftype> \
    { \
      static CFTypeRef cast(CFTypeRef obj) \
      { \
	if (obj && CFGetTypeID(obj) == idmeth()) \
	  return obj; \
	else \
	  return nullptr; \
      } \
    }; \
    typedef Wrap<cftype> cls; \
    inline cls castmeth(CFTypeRef obj) \
    { \
      CFTypeRef o = Type<cftype>::cast(obj); \
      if (o) \
	return cls(cftype(o), GET);	\
      else \
	return cls(); \
    }

namespace openvpn {
  namespace CF
  {
    enum Rule {
      CREATE,    // create rule
      GET        // get rule
    };

    template <typename T> struct Type {};

    template <typename T>
    class Wrap
    {
    public:
      Wrap() : obj_(nullptr) {}

      explicit Wrap(T obj, const Rule rule=CREATE)
      {
	if (rule == GET && obj)
	  CFRetain(obj);
	obj_ = obj;
      }

      Wrap(const Wrap& other)
      {
	obj_ = other.obj_;
	if (obj_)
	  CFRetain(obj_);
      }

      Wrap& operator=(const Wrap& other)
      {
	if (other.obj_)
	  CFRetain(other.obj_);
	if (obj_)
	  CFRelease(obj_);
	obj_ = other.obj_;
	return *this;
      }

      Wrap(Wrap&& other) noexcept
      {
	obj_ = other.obj_;
	other.obj_ = nullptr;
      }

      Wrap& operator=(Wrap&& other) noexcept
      {
	if (obj_)
	  CFRelease(obj_);
	obj_ = other.obj_;
	other.obj_ = nullptr;
	return *this;
      }

      void swap(Wrap& other)
      {
	std::swap(obj_, other.obj_);
      }

      void reset(T obj=nullptr, const Rule rule=CREATE)
      {
	if (rule == GET && obj)
	  CFRetain(obj);
	if (obj_)
	  CFRelease(obj_);
	obj_ = obj;
      }

      bool defined() const { return obj_ != nullptr; }

      explicit operator bool() const noexcept
      {
	return defined();
      }

      T operator()() const { return obj_; }

      CFTypeRef generic() const { return (CFTypeRef)obj_; }

      static T cast(CFTypeRef obj) { return T(Type<T>::cast(obj)); }

      static Wrap from_generic(CFTypeRef obj, const Rule rule=CREATE)
      {
	return Wrap(cast(obj), rule);
      }

      T release()
      {
	T ret = obj_;
	obj_ = nullptr;
	return ret;
      }

      CFTypeRef generic_release()
      {
	T ret = obj_;
	obj_ = nullptr;
	return (CFTypeRef)ret;
      }

      // Intended for use with Core Foundation methods that require
      // a T* for saving a create-rule return value
      T* mod_ref()
      {
	if (obj_)
	  {
	    CFRelease(obj_);
	    obj_ = nullptr;
	  }
	return &obj_;
      }

      void show() const
      {
	if (obj_)
	  CFShow(obj_);
	else
	  std::cerr << "CF_UNDEFINED" << std::endl;
      }

      virtual ~Wrap()
      {
	if (obj_)
	  CFRelease(obj_);
      }

    private:
      Wrap& operator=(T obj) = delete; // prevent use because no way to pass rule parameter

      T obj_;
    };

    // common CF types

    OPENVPN_CF_WRAP(String, string_cast, CFStringRef, CFStringGetTypeID)
    OPENVPN_CF_WRAP(Number, number_cast, CFNumberRef, CFNumberGetTypeID)
    OPENVPN_CF_WRAP(Bool, bool_cast, CFBooleanRef, CFBooleanGetTypeID)
    OPENVPN_CF_WRAP(Data, data_cast, CFDataRef, CFDataGetTypeID)
    OPENVPN_CF_WRAP(Array, array_cast, CFArrayRef, CFArrayGetTypeID)
    OPENVPN_CF_WRAP(MutableArray, mutable_array_cast, CFMutableArrayRef, CFArrayGetTypeID)
    OPENVPN_CF_WRAP(Dict, dict_cast, CFDictionaryRef, CFDictionaryGetTypeID)
    OPENVPN_CF_WRAP(MutableDict, mutable_dict_cast, CFMutableDictionaryRef, CFDictionaryGetTypeID)
    OPENVPN_CF_WRAP(Error, error_cast, CFErrorRef, CFErrorGetTypeID);

    // generic CFTypeRef wrapper

    typedef Wrap<CFTypeRef> Generic;

    inline Generic generic_cast(CFTypeRef obj)
    {
      return Generic(obj, GET);
    }

    // constructors

    inline String string(const char *str)
    {
      return String(CFStringCreateWithCString(kCFAllocatorDefault, str, kCFStringEncodingUTF8));
    }

    inline String string(CFStringRef str)
    {
      return String(str, GET);
    }

    inline String string(const String& str)
    {
      return String(str);
    }

    inline String string(const std::string& str)
    {
      return String(CFStringCreateWithCString(kCFAllocatorDefault, str.c_str(), kCFStringEncodingUTF8));
    }

    inline String string(const std::string* str)
    {
      return String(CFStringCreateWithCString(kCFAllocatorDefault, str->c_str(), kCFStringEncodingUTF8));
    }

    inline Number number_from_int(const int n)
    {
      return Number(CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &n));
    }

    inline Number number_from_int32(const SInt32 n)
    {
      return Number(CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &n));
    }

    inline Number number_from_long_long(const long long n)
    {
      return Number(CFNumberCreate(kCFAllocatorDefault, kCFNumberLongLongType, &n));
    }

    inline Number number_from_index(const CFIndex n)
    {
      return Number(CFNumberCreate(kCFAllocatorDefault, kCFNumberCFIndexType, &n));
    }

    inline Data data(const void *bytes, CFIndex length)
    {
      return Data(CFDataCreate(kCFAllocatorDefault, (const UInt8 *)bytes, length));
    }

    inline Array array(const void **values, CFIndex numValues)
    {
      return Array(CFArrayCreate(kCFAllocatorDefault, values, numValues, &kCFTypeArrayCallBacks));
    }

    inline Dict dict(const void **keys, const void **values, CFIndex numValues)
    {
      return Dict(CFDictionaryCreate(kCFAllocatorDefault,
				     keys,
				     values,
				     numValues,
				     &kCFTypeDictionaryKeyCallBacks,
				     &kCFTypeDictionaryValueCallBacks));
    }

    inline Dict const_dict(MutableDict& mdict)
    {
      return Dict(mdict(), CF::GET);
    }

    inline Array const_array(MutableArray& marray)
    {
      return Array(marray(), CF::GET);
    }

    inline Dict empty_dict()
    {
      return Dict(CFDictionaryCreate(kCFAllocatorDefault,
				     nullptr,
				     nullptr,
				     0,
				     &kCFTypeDictionaryKeyCallBacks,
				     &kCFTypeDictionaryValueCallBacks));
    }

    inline MutableArray mutable_array(const CFIndex capacity=0)
    {
      return MutableArray(CFArrayCreateMutable(kCFAllocatorDefault, capacity, &kCFTypeArrayCallBacks));
    }

    inline MutableDict mutable_dict(const CFIndex capacity=0)
    {
      return MutableDict(CFDictionaryCreateMutable(kCFAllocatorDefault, capacity, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks));
    }

    template <typename DICT>
    inline MutableDict mutable_dict_copy(const DICT& dict, const CFIndex capacity=0)
    {
      if (dict.defined())
	return MutableDict(CFDictionaryCreateMutableCopy(kCFAllocatorDefault, capacity, dict()));
      else
	return mutable_dict(capacity);
    }

    inline Error error(CFStringRef domain, CFIndex code, CFDictionaryRef userInfo)
    {
      return Error(CFErrorCreate(kCFAllocatorDefault, domain, code, userInfo));
    }

    // accessors

    template <typename ARRAY>
    inline CFIndex array_len(const ARRAY& array)
    {
      if (array.defined())
	return CFArrayGetCount(array());
      else
	return 0;
    }

    template <typename DICT>
    inline CFIndex dict_len(const DICT& dict)
    {
      if (dict.defined())
	return CFDictionaryGetCount(dict());
      else
	return 0;
    }

    template <typename ARRAY>
    inline CFTypeRef array_index(const ARRAY& array, const CFIndex idx)
    {
      if (array.defined() && CFArrayGetCount(array()) > idx)
	return CFArrayGetValueAtIndex(array(), idx);
      else
	return nullptr;
    }

    template <typename DICT, typename KEY>
    inline CFTypeRef dict_index(const DICT& dict, const KEY& key)
    {
      if (dict.defined())
	{
	  String keystr = string(key);
	  if (keystr.defined())
	    return CFDictionaryGetValue(dict(), keystr());
	}
      return nullptr;
    }

    // string methods

    struct cppstring_error : public std::exception
    {
      virtual const char* what() const throw()
      {
	return "cppstring_error";
      }
    };

    inline std::string cppstring(CFStringRef str)
    {
      const CFStringEncoding encoding = kCFStringEncodingUTF8;
      if (str)
	{
	  const CFIndex len = CFStringGetLength(str);
	  if (len > 0)
	    {
	      const CFIndex maxsize = CFStringGetMaximumSizeForEncoding(len, encoding);
	      char *buf = new char[maxsize];
	      const Boolean status = CFStringGetCString(str, buf, maxsize, encoding);
	      if (status)
		{
		  std::string ret(buf);
		  delete [] buf;
		  return ret;
		}
	      else
		{
		  delete [] buf;
		  throw cppstring_error();
		}
	    }
	}
      return "";
    }

    inline std::string cppstring(const String& str)
    {
      return cppstring(str());
    }

    inline std::string description(CFTypeRef obj)
    {
      if (obj)
	{
	  String s(CFCopyDescription(obj));
	  return cppstring(s);
	}
      else
	return "UNDEF";
    }

    // format an array of strings (non-string elements in array are ignored)
    template <typename ARRAY>
    inline std::string array_to_string(const ARRAY& array, const char delim=',')
    {
      std::ostringstream os;
      const CFIndex len = array_len(array);
      if (len)
	{
	  bool sep = false;
	  for (CFIndex i = 0; i < len; ++i)
	    {
	      const String v(string_cast(array_index(array, i)));
	      if (v.defined())
		{
		  if (sep)
		    os << delim;
		  os << cppstring(v);
		  sep = true;
		}
	    }
	}
      return os.str();
    }

    inline bool string_equal(const String& s1, const String& s2, const CFStringCompareFlags compareOptions = 0)
    {
      return s1.defined() && s2.defined() && CFStringCompare(s1(), s2(), compareOptions) == kCFCompareEqualTo;
    }

    // property lists
    inline Data plist(CFTypeRef obj)
    {
      return Data(CFPropertyListCreateData(kCFAllocatorDefault,
					   obj,
					   kCFPropertyListBinaryFormat_v1_0,
					   0,
					   nullptr));
    }

  } // namespace CF
} // namespace openvpn

#endif // OPENVPN_APPLECRYPTO_CF_CF_H
