#pragma once

#include <memory>

#include "Runtime/IFactory.hpp"
#include "Runtime/IObj.hpp"
#include "Runtime/IObjectStore.hpp"
#include "Runtime/IVParamObj.hpp"
#include "Runtime/RetroTypes.hpp"

namespace urde {
class IObjectStore;

/** Shared data-structure for CToken references, analogous to std::shared_ptr */
class CObjectReference {
  friend class CSimplePool;
  friend class CToken;

  u16 x0_refCount = 0;
  u16 x2_lockCount = 0;
  bool x3_loading = false; /* Rightmost bit of lockCount */
  SObjectTag x4_objTag;
  IObjectStore* xC_objectStore = nullptr;
  std::unique_ptr<IObj> x10_object;
  CVParamTransfer x14_params;

  /** Mechanism by which CToken decrements 1st ref-count, indicating CToken invalidation or reset.
   *  Reaching 0 indicates the CToken should delete the CObjectReference */
  u16 RemoveReference();

  CObjectReference(IObjectStore& objStore, std::unique_ptr<IObj>&& obj, const SObjectTag& objTag,
                   CVParamTransfer buildParams);
  CObjectReference(std::unique_ptr<IObj>&& obj);

  /** Indicates an asynchronous load transaction has been submitted and is not yet finished */
  bool IsLoading() const { return x3_loading; }

  /** Indicates an asynchronous load transaction has finished and object is completely loaded */
  bool IsLoaded() const { return x10_object.operator bool(); }

  /** Decrements 2nd ref-count, performing unload or async-load-cancel if 0 reached */
  void Unlock();

  /** Increments 2nd ref-count, performing async-factory-load if needed */
  void Lock();

  void CancelLoad();

  /** Pointer-synchronized object-destructor, another building Lock cycle may be performed after */
  void Unload();

  /** Synchronous object-fetch, guaranteed to return complete object on-demand, blocking build if not ready */
  IObj* GetObject();

public:
  const SObjectTag& GetObjectTag() const { return x4_objTag; }

  ~CObjectReference();
};

/** Counted meta-object, reference-counting against a shared CObjectReference
 *  This class is analogous to std::shared_ptr and C++11 rvalues have been implemented accordingly
 *  (default/empty constructor, move constructor/assign) */
class CToken {
  friend class CModel;
  friend class CSimplePool;

  CObjectReference* x0_objRef = nullptr;
  bool x4_lockHeld = false;

  void RemoveRef();

  CToken(CObjectReference* obj);

public:
  /* Added to test for non-null state */
  explicit operator bool() const { return HasReference(); }
  bool HasReference() const { return x0_objRef != nullptr; }

  void Unlock();
  void Lock();
  bool IsLocked() const { return x4_lockHeld; }
  bool IsLoaded() const;
  IObj* GetObj();
  const IObj* GetObj() const { return const_cast<CToken*>(this)->GetObj(); }
  CToken& operator=(const CToken& other);
  CToken& operator=(CToken&& other) noexcept;
  CToken() = default;
  CToken(const CToken& other);
  CToken(CToken&& other) noexcept;
  CToken(IObj* obj);
  CToken(std::unique_ptr<IObj>&& obj);
  const SObjectTag* GetObjectTag() const;
  ~CToken();
};

template <class T>
class TToken : public CToken {
public:
  static std::unique_ptr<TObjOwnerDerivedFromIObj<T>> GetIObjObjectFor(std::unique_ptr<T>&& obj) {
    return TObjOwnerDerivedFromIObj<T>::GetNewDerivedObject(std::move(obj));
  }
  TToken() = default;
  virtual ~TToken() = default;
  TToken(const CToken& other) : CToken(other) {}
  TToken(CToken&& other) : CToken(std::move(other)) {}
  TToken(std::unique_ptr<T>&& obj) : CToken(GetIObjObjectFor(std::move(obj))) {}
  TToken& operator=(std::unique_ptr<T>&& obj) {
    *this = CToken(GetIObjObjectFor(std::move(obj)));
    return this;
  }
  virtual void Unlock() { CToken::Unlock(); }
  virtual void Lock() { CToken::Lock(); }
  virtual T* GetObj() {
    TObjOwnerDerivedFromIObj<T>* owner = static_cast<TObjOwnerDerivedFromIObj<T>*>(CToken::GetObj());
    if (owner)
      return owner->GetObj();
    return nullptr;
  }
  virtual const T* GetObj() const { return const_cast<TToken<T>*>(this)->GetObj(); }
  virtual TToken& operator=(const CToken& other) {
    CToken::operator=(other);
    return *this;
  }
  T* operator->() { return GetObj(); }
  const T* operator->() const { return GetObj(); }
  T& operator*() { return *GetObj(); }
  const T& operator*() const { return *GetObj(); }
};

template <class T>
class TCachedToken : public TToken<T> {
protected:
  T* m_obj = nullptr;

public:
  TCachedToken() = default;
  TCachedToken(const CToken& other) : TToken<T>(other) {}
  TCachedToken(CToken&& other) : TToken<T>(std::move(other)) {}
  T* GetObj() override {
    if (!m_obj)
      m_obj = TToken<T>::GetObj();
    return m_obj;
  }
  const T* GetObj() const override { return const_cast<TCachedToken<T>*>(this)->GetObj(); }
  void Unlock() override {
    TToken<T>::Unlock();
    m_obj = nullptr;
  }

  TCachedToken& operator=(const TCachedToken& other) {
    TToken<T>::operator=(other);
    m_obj = nullptr;
    return *this;
  }
  TCachedToken& operator=(const CToken& other) override {
    TToken<T>::operator=(other);
    m_obj = nullptr;
    return *this;
  }
};

template <class T>
class TLockedToken : public TCachedToken<T> {
public:
  TLockedToken() = default;
  TLockedToken(const TLockedToken& other) : TCachedToken<T>(other) { CToken::Lock(); }
  TLockedToken& operator=(const TLockedToken& other) {
    CToken oldTok = std::move(*this);
    TCachedToken<T>::operator=(other);
    CToken::Lock();
    return *this;
  }
  TLockedToken(const CToken& other) : TCachedToken<T>(other) { CToken::Lock(); }
  TLockedToken& operator=(const CToken& other) override {
    CToken oldTok = std::move(*this);
    TCachedToken<T>::operator=(other);
    CToken::Lock();
    return *this;
  }
  TLockedToken(CToken&& other) {
    CToken oldTok = std::move(*this);
    *this = TCachedToken<T>(std::move(other));
    CToken::Lock();
  }
};

} // namespace urde