// license:BSD-3-Clause
// copyright-holders:Aaron Giles, Vas Crabb
/***************************************************************************

    devdelegate.h

    Delegates that are late-bound to MAME devices.

***************************************************************************/

#ifndef MAME_EMU_DEVDELEGATE_H
#define MAME_EMU_DEVDELEGATE_H

#pragma once

#include "delegate.h"

#include <array>
#include <functional>
#include <type_traits>
#include <utility>


namespace emu {

//**************************************************************************
//  FORWARD DECLARATIONS
//**************************************************************************

template <typename Signature, unsigned Count> class device_delegate_array;



//**************************************************************************
//  TYPE DEFINITIONS
//**************************************************************************

namespace detail {

// ======================> device_delegate_helper

// device_delegate_helper does non-template work
class device_delegate_helper
{
public:
	// accessors
	char const *finder_tag() const { return m_tag; }
	std::pair<device_t &, char const *> finder_target() const { return std::make_pair(m_base, m_tag); }

protected:
	// construct/assign
	device_delegate_helper(device_t &owner, char const *tag = nullptr) : m_base(owner), m_tag(tag) { }
	template <class DeviceClass, bool Required> device_delegate_helper(device_finder<DeviceClass, Required> const &finder);
	device_delegate_helper(device_delegate_helper const &) = default;
	device_delegate_helper &operator=(device_delegate_helper const &) = default;

	// internal helpers
	delegate_late_bind &bound_object() const;
	void set_tag(device_t &object) { m_base = object; m_tag = nullptr; }
	void set_tag(device_t &base, char const *tag) { m_base = base; m_tag = tag; }
	void set_tag(char const *tag);
	template <class DeviceClass, bool Required> void set_tag(device_finder<DeviceClass, Required> const &finder);

private:
	std::reference_wrapper<device_t> m_base;
	char const *m_tag;
};

} // namespace detail


// ======================> named_delegate
template <typename Signature>
class named_delegate : public delegate<Signature>
{
private:
	using basetype = delegate<Signature>;

	char const *m_name = nullptr; // name string

protected:
	template <class FunctionClass> using member_func_type = typename basetype::template member_func_type<FunctionClass>;
	template <class FunctionClass> using const_member_func_type = typename basetype::template const_member_func_type<FunctionClass>;
	template <class FunctionClass> using static_ref_func_type = typename basetype::template static_ref_func_type<FunctionClass>;

	template <typename T> using suitable_functoid = typename basetype::template suitable_functoid<T>;

public:
	// create a standard set of constructors
	named_delegate() = default;
	named_delegate(named_delegate const &) = default;
	named_delegate(named_delegate const &src, delegate_late_bind &object) : basetype(src, object), m_name(src.m_name) { }
	template <class FunctionClass> named_delegate(member_func_type<FunctionClass> funcptr, char const *name, FunctionClass *object) : basetype(funcptr, object), m_name(name) { }
	template <class FunctionClass> named_delegate(const_member_func_type<FunctionClass> funcptr, char const *name, FunctionClass *object) : basetype(funcptr, object), m_name(name) { }
	template <class FunctionClass> named_delegate(static_ref_func_type<FunctionClass> funcptr, char const *name, FunctionClass *object) : basetype(funcptr, object), m_name(name) { }
	template <typename T> named_delegate(T &&funcptr, std::enable_if_t<suitable_functoid<T>::value, char const *> name) : basetype(std::forward<T>(funcptr)), m_name(name) { }

	// allow assignment
	named_delegate &operator=(named_delegate const &src) = default;
	named_delegate &operator=(named_delegate &&src) = default;
	named_delegate &operator=(std::nullptr_t) noexcept { reset(); return *this; }

	// getters
	char const *name() const noexcept { return m_name; }

	// unsetter
	void reset() noexcept { basetype::reset(); m_name = nullptr; }
};

// ======================> device_delegate

// device_delegate is a delegate that wraps with a device tag and can be easily
// late bound without replicating logic everywhere
template <typename Signature> class device_delegate;

template <typename ReturnType, typename... Params>
class device_delegate<ReturnType (Params...)> : protected named_delegate<ReturnType (Params...)>, public detail::device_delegate_helper
{
private:
	using basetype = named_delegate<ReturnType (Params...)>;

	template <typename T>
	using suitable_functoid = typename basetype::template suitable_functoid<T>;

	template <class T, class U>
	using is_related_device_implementation = std::bool_constant<std::is_base_of_v<T, U> && std::is_base_of_v<device_t, U> >;
	template <class T, class U>
	using is_related_device_interface = std::bool_constant<std::is_base_of_v<T, U> && std::is_base_of_v<device_interface, U> && !std::is_base_of_v<device_t, U> >;
	template <class T, class U>
	using is_related_device = std::bool_constant<is_related_device_implementation<T, U>::value || is_related_device_interface<T, U>::value>;

	template <class T> static std::enable_if_t<is_related_device_implementation<T, T>::value, device_t &> get_device(T &object) { return object; }
	template <class T> static std::enable_if_t<is_related_device_interface<T, T>::value, device_t &> get_device(T &object) { return object.device(); }

public:
	template <unsigned Count> using array = device_delegate_array<ReturnType (Params...), Count>;

	template <typename T>
	using supports_callback = std::bool_constant<std::is_constructible_v<device_delegate, device_t &, char const *, T, char const *> >;

	// construct/assign
	explicit device_delegate(device_t &owner) : basetype(), detail::device_delegate_helper(owner) { }
	device_delegate(device_delegate const &) = default;
	device_delegate &operator=(device_delegate const &) = default;

	// construct with prototype and target
	device_delegate(basetype const &proto, device_t &object) : basetype(proto, object), detail::device_delegate_helper(object) { }
	device_delegate(device_delegate const &proto, device_t &object) : basetype(proto, object), detail::device_delegate_helper(object) { }

	// construct with base and tag
	template <class D>
	device_delegate(device_t &base, char const *tag, ReturnType (D::*funcptr)(Params...), char const *name)
		: basetype(funcptr, name, static_cast<D *>(nullptr))
		, detail::device_delegate_helper(base, tag)
	{ }
	template <class D>
	device_delegate(device_t &base, char const *tag, ReturnType (D::*funcptr)(Params...) const, char const *name)
		: basetype(funcptr, name, static_cast<D *>(nullptr))
		, detail::device_delegate_helper(base, tag)
	{ }
	template <class D>
	device_delegate(device_t &base, char const *tag, ReturnType (*funcptr)(D &, Params...), char const *name)
		: basetype(funcptr, name, static_cast<D *>(nullptr))
		, detail::device_delegate_helper(base, tag)
	{ }

	// construct with device finder
	template <class D, bool R, class E>
	device_delegate(device_finder<D, R> const &finder, ReturnType (E::*funcptr)(Params...), char const *name)
		: basetype(funcptr, name, static_cast<E *>(nullptr))
		, detail::device_delegate_helper(finder)
	{ }
	template <class D, bool R, class E>
	device_delegate(device_finder<D, R> const &finder, ReturnType (E::*funcptr)(Params...) const, char const *name)
		: basetype(funcptr, name, static_cast<E *>(nullptr))
		, detail::device_delegate_helper(finder)
	{ }
	template <class D, bool R, class E>
	device_delegate(device_finder<D, R> const &finder, ReturnType (*funcptr)(E &, Params...), char const *name)
		: basetype(funcptr, name, static_cast<E *>(nullptr))
		, detail::device_delegate_helper(finder)
	{ }

	// construct with target object
	template <class T, class D>
	device_delegate(T &object, ReturnType (D::*funcptr)(Params...), std::enable_if_t<is_related_device<D, T>::value, char const *> name)
		: basetype(funcptr, name, static_cast<D *>(&object))
		, detail::device_delegate_helper(get_device(object))
	{ }
	template <class T, class D>
	device_delegate(T &object, ReturnType (D::*funcptr)(Params...) const, std::enable_if_t<is_related_device<D, T>::value, char const *> name)
		: basetype(funcptr, name, static_cast<D *>(&object))
		, detail::device_delegate_helper(get_device(object))
	{ }
	template <class T, class D>
	device_delegate(T &object, ReturnType (*funcptr)(D &, Params...), std::enable_if_t<is_related_device<D, T>::value, char const *> name)
		: basetype(funcptr, name, static_cast<D *>(&object))
		, detail::device_delegate_helper(get_device(object))
	{ }

	// construct with callable object
	template <typename T>
	device_delegate(device_t &owner, T &&funcptr, std::enable_if_t<suitable_functoid<T>::value, char const *> name)
		: basetype(std::forward<T>(funcptr), name)
		, detail::device_delegate_helper(owner)
	{ basetype::operator=(basetype(std::forward<T>(funcptr), name)); }

	// setters that implicitly bind to the current device
	template <class D> void set(ReturnType (D::*funcptr)(Params...), char const *name)
	{ basetype::operator=(basetype(funcptr, name, static_cast<D *>(nullptr))); set_tag(nullptr); }
	template <class D> void set(ReturnType (D::*funcptr)(Params...) const, char const *name)
	{ basetype::operator=(basetype(funcptr, name, static_cast<D *>(nullptr))); set_tag(nullptr); }
	template <class D> void set(ReturnType (*funcptr)(D &, Params...), char const *name)
	{ basetype::operator=(basetype(funcptr, name, static_cast<D *>(nullptr))); set_tag(nullptr); }

	// setters that take a tag-like object specifying the target
	template <typename T, class D> void set(T &&tag, ReturnType (D::*funcptr)(Params...), char const *name)
	{ basetype::operator=(basetype(funcptr, name, static_cast<D *>(nullptr))); set_tag(std::forward<T>(tag)); }
	template <typename T, class D> void set(T &&tag, ReturnType (D::*funcptr)(Params...) const, char const *name)
	{ basetype::operator=(basetype(funcptr, name, static_cast<D *>(nullptr))); set_tag(std::forward<T>(tag)); }
	template <typename T, class D> void set(T &&tag, ReturnType (*funcptr)(D &, Params...), char const *name)
	{ basetype::operator=(basetype(funcptr, name, static_cast<D *>(nullptr))); set_tag(std::forward<T>(tag)); }

	// setters that take a target object
	template <class T, class D> std::enable_if_t<std::is_base_of<D, T>::value> set(T &object, ReturnType (D::*funcptr)(Params...), char const *name)
	{ basetype::operator=(basetype(funcptr, name, static_cast<D *>(&object))); set_tag(finder_target().first); }
	template <class T, class D> std::enable_if_t<std::is_base_of<D, T>::value> set(T &object, ReturnType (D::*funcptr)(Params...) const, char const *name)
	{ basetype::operator=(basetype(funcptr, name, static_cast<D *>(&object))); set_tag(finder_target().first); }
	template <class T, class D> std::enable_if_t<std::is_base_of<D, T>::value> set(T &object, ReturnType (*funcptr)(D &, Params...), char const *name)
	{ basetype::operator=(basetype(funcptr, name, static_cast<D *>(&object))); set_tag(finder_target().first); }

	// setter that takes a functoid
	template <typename T> std::enable_if_t<suitable_functoid<T>::value> set(T &&funcptr, char const *name)
	{ basetype::operator=(basetype(std::forward<T>(funcptr), name)); }

	// unsetter
	void set(std::nullptr_t)
	{ reset(); }
	void reset()
	{ basetype::reset(); set_tag(finder_target().first); }

	// perform the binding
	void resolve() { if (!basetype::isnull() && !basetype::has_object()) basetype::late_bind(bound_object()); }

	// accessors
	using basetype::operator();
	using basetype::isnull;
	using basetype::has_object;
	using basetype::name;
};


// simplifies constructing an array of delegates with a single owner
template <typename Signature, unsigned Count>
class device_delegate_array : public std::array<device_delegate<Signature>, Count>
{
private:
	template <unsigned... V>
	device_delegate_array(device_t &owner, std::integer_sequence<unsigned, V...> const &)
		: std::array<device_delegate<Signature>, Count>{{ make_one<V>(owner)... }}
	{
	}

	template <unsigned N>
	device_delegate<Signature> make_one(device_t &owner)
	{
		return device_delegate<Signature>(owner);
	}

public:
	using std::array<device_delegate<Signature>, Count>::array;

	device_delegate_array(device_t &owner)
		: device_delegate_array(owner, std::make_integer_sequence<unsigned, Count>())
	{
	}

	void resolve_all()
	{
		for (device_delegate<Signature> &elem : *this)
			elem.resolve();
	}
};

} // namespace emu


using emu::named_delegate;
using emu::device_delegate;

#endif // MAME_EMU_DEVDELEGATE_H
