// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#ifndef dap_any_h
#define dap_any_h

#include "typeinfo.h"

#include <assert.h>
#include <stdint.h>

namespace dap {

template <typename T>
struct TypeOf;
class Deserializer;
class Serializer;

// any provides a type-safe container for values of any of dap type (boolean,
// integer, number, array, variant, any, null, dap-structs).
class any {
 public:
  // constructors
  inline any() = default;
  inline any(const any& other) noexcept;
  inline any(any&& other) noexcept;

  template <typename T>
  inline any(const T& val);

  // destructors
  inline ~any();

  // replaces the contained value with a null.
  inline void reset();

  // assignment
  inline any& operator=(const any& rhs);
  inline any& operator=(any&& rhs) noexcept;
  template <typename T>
  inline any& operator=(const T& val);
  inline any& operator=(const std::nullptr_t& val);

  // get() returns the contained value of the type T.
  // If the any does not contain a value of type T, then get() will assert.
  template <typename T>
  inline T& get() const;

  // is() returns true iff the contained value is of type T.
  template <typename T>
  inline bool is() const;

 private:
  friend class Deserializer;
  friend class Serializer;

  static inline void* alignUp(void* val, size_t alignment);
  inline void alloc(size_t size, size_t align);
  inline void free();
  inline bool isInBuffer(void* ptr) const;

  void* value = nullptr;
  const TypeInfo* type = nullptr;
  void* heap = nullptr;  // heap allocation
  uint8_t buffer[32];    // or internal allocation
};

inline any::~any() {
  reset();
}

template <typename T>
inline any::any(const T& val) {
  *this = val;
}

any::any(const any& other) noexcept : type(other.type) {
  if (other.value != nullptr) {
    alloc(type->size(), type->alignment());
    type->copyConstruct(value, other.value);
  }
}

any::any(any&& other) noexcept : type(other.type) {
  if (other.isInBuffer(other.value)) {
    alloc(type->size(), type->alignment());
    type->copyConstruct(value, other.value);
  } else {
    value = other.value;
  }
  other.value = nullptr;
  other.type = nullptr;
}

void any::reset() {
  if (value != nullptr) {
    type->destruct(value);
    free();
  }
  value = nullptr;
  type = nullptr;
}

any& any::operator=(const any& rhs) {
  reset();
  type = rhs.type;
  if (rhs.value != nullptr) {
    alloc(type->size(), type->alignment());
    type->copyConstruct(value, rhs.value);
  }
  return *this;
}

any& any::operator=(any&& rhs) noexcept {
  reset();
  type = rhs.type;
  if (rhs.isInBuffer(rhs.value)) {
    alloc(type->size(), type->alignment());
    type->copyConstruct(value, rhs.value);
  } else {
    value = rhs.value;
  }
  rhs.value = nullptr;
  rhs.type = nullptr;
  return *this;
}

template <typename T>
any& any::operator=(const T& val) {
  if (!is<T>()) {
    reset();
    type = TypeOf<T>::type();
    alloc(type->size(), type->alignment());
    type->copyConstruct(value, &val);
  } else {
#ifdef __clang_analyzer__
    assert(value != nullptr);
#endif
    *reinterpret_cast<T*>(value) = val;
  }
  return *this;
}

any& any::operator=(const std::nullptr_t&) {
  reset();
  return *this;
}

template <typename T>
T& any::get() const {
  static_assert(!std::is_same<T, std::nullptr_t>::value,
                "Cannot get nullptr from 'any'.");
  assert(is<T>());
  return *reinterpret_cast<T*>(value);
}

template <typename T>
bool any::is() const {
  return type == TypeOf<T>::type();
}

template <>
inline bool any::is<std::nullptr_t>() const {
  return value == nullptr;
}

void* any::alignUp(void* val, size_t alignment) {
  auto ptr = reinterpret_cast<uintptr_t>(val);
  return reinterpret_cast<void*>(alignment *
                                 ((ptr + alignment - 1) / alignment));
}

void any::alloc(size_t size, size_t align) {
  assert(value == nullptr);
  value = alignUp(buffer, align);
  if (isInBuffer(reinterpret_cast<uint8_t*>(value) + size - 1)) {
    return;
  }
  heap = new uint8_t[size + align];
  value = alignUp(heap, align);
}

void any::free() {
  assert(value != nullptr);
  if (heap != nullptr) {
    delete[] reinterpret_cast<uint8_t*>(heap);
    heap = nullptr;
  }
  value = nullptr;
}

bool any::isInBuffer(void* ptr) const {
  auto addr = reinterpret_cast<uintptr_t>(ptr);
  return addr >= reinterpret_cast<uintptr_t>(buffer) &&
         addr < reinterpret_cast<uintptr_t>(buffer + sizeof(buffer));
}

}  // namespace dap

#endif  // dap_any_h
