/**
 * address_map for ES40 VGA register dispatch.
 * Adapted from MAME for code re-use compatibility in VGA/S3 Implementation
 *
 *     map(lo, hi).lrw8(NAME(read_lambda), NAME(write_lambda))
 *     map(lo, hi).lr8(NAME(read_lambda))
 *     map(lo, hi).lw8(NAME(write_lambda))
 *     map(lo, hi).noprw()
 *     map(lo, hi).mirror(bits).lrw8(...)
 *     map.unmap_value_high() / map.unmap_value_low()
 *     map.global_mask(mask)
 *
 *     u8  val  = m_crtc_map.read_byte(addr);
 *     m_crtc_map.write_byte(addr, data);
 *
 */

#ifndef ES40_ADDRESS_MAP_H
#define ES40_ADDRESS_MAP_H

#include <functional>
#include <memory>
#include <cstdint>
#include <vector>
#include "mame_shims.h"

using offs_t = uint32_t;
using u8 = uint8_t;
using u16 = uint16_t;
using u32 = uint32_t;
using u64 = uint64_t;

#ifndef NAME
#define NAME(x) x, #x
#endif

class address_map;


// address_map_entry — returned by map(lo, hi), chains .lr8/.lw8/.lrw8/.mirror

class address_map_entry
{
public:
  address_map_entry(address_map& parent, unsigned lo, unsigned hi)
    : m_map(parent), m_lo(lo), m_hi(hi), m_mirror(0) {
  }

  /// .lrw8(read_fn, read_name, write_fn, write_name)
  template <typename T, typename U>
  address_map_entry& lrw8(T&& read_fn, const char* read_name,
    U&& write_fn, const char* write_name);

  /// .lr8(read_fn, read_name)
  template <typename T>
  address_map_entry& lr8(T&& read_fn, const char* name);

  /// .lw8(write_fn, write_name)
  template <typename T>
  address_map_entry& lw8(T&& write_fn, const char* name);

  /// .noprw() 
  address_map_entry& noprw();

  /// .mirror(bits)
  address_map_entry& mirror(unsigned bits)
  {
    m_mirror = bits;
    return *this;
  }

private:
  /// Expand mirror bits and call fn for each effective address
  void for_each_addr(std::function<void(unsigned)> fn) const;

  address_map& m_map;
  unsigned      m_lo;
  unsigned      m_hi;
  unsigned      m_mirror;
};


// address_map — dispatch table

class address_map
{
public:
  explicit address_map(unsigned size = 256)
    : m_size(size), m_slots(size), m_unmapval(0xFF), m_globalmask(0)
  {
  }

  address_map_entry operator()(unsigned lo, unsigned hi)
  {
    return address_map_entry(*this, lo, hi);
  }

  // map-level 

  void unmap_value_high() { m_unmapval = 0xFF; }
  void unmap_value_low() { m_unmapval = 0x00; }
  void global_mask(offs_t mask) { m_globalmask = mask; }

  // Dispatch 

  u8 read_byte(unsigned addr) const
  {
    if (m_globalmask) addr &= m_globalmask;
    if (addr < m_size && m_slots[addr].read_fn)
      return m_slots[addr].read_fn(static_cast<offs_t>(addr - m_slots[addr].read_base));
    return m_unmapval;
  }

  void write_byte(unsigned addr, u8 data)
  {
    if (m_globalmask) addr &= m_globalmask;
    if (addr < m_size && m_slots[addr].write_fn)
      m_slots[addr].write_fn(static_cast<offs_t>(addr - m_slots[addr].write_base), data);
  }

  bool has_handler(unsigned addr) const
  {
    if (m_globalmask) addr &= m_globalmask;
    if (addr >= m_size) return false;
    return m_slots[addr].read_fn || m_slots[addr].write_fn;
  }

  const char* debug_name(unsigned addr) const
  {
    if (m_globalmask) addr &= m_globalmask;
    return (addr < m_size) ? m_slots[addr].name : nullptr;
  }

  u8 unmapval() const { return m_unmapval; }

  // called by address_map_entry

  void install_read(unsigned addr, std::function<u8(offs_t)> fn, unsigned base, const char* name)
  {
    if (addr < m_size)
    {
      m_slots[addr].read_fn = std::move(fn);
      m_slots[addr].read_base = base;
      if (name) m_slots[addr].name = name;
    }
  }

  void install_write(unsigned addr, std::function<void(offs_t, u8)> fn, unsigned base, const char* name)
  {
    if (addr < m_size)
    {
      m_slots[addr].write_fn = std::move(fn);
      m_slots[addr].write_base = base;
      if (name) m_slots[addr].name = name;
    }
  }

  void install_nop(unsigned addr, unsigned base, const char* name)
  {
    if (addr < m_size)
    {
      m_slots[addr].read_fn = [](offs_t) -> u8 { return 0; };
      m_slots[addr].write_fn = [](offs_t, u8) {};
      m_slots[addr].read_base = base;
      m_slots[addr].write_base = base;
      if (name) m_slots[addr].name = name;
    }
  }

  unsigned size() const { return m_size; }

private:
  struct Slot
  {
    std::function<u8(offs_t)>       read_fn;
    std::function<void(offs_t, u8)> write_fn;
    unsigned                        read_base = 0;
    unsigned                        write_base = 0;
    const char* name = nullptr;
  };

  unsigned           m_size;
  std::vector<Slot>  m_slots;
  u8                 m_unmapval;
  offs_t             m_globalmask;
};


//  mirror expansion helper

inline void address_map_entry::for_each_addr(std::function<void(unsigned)> fn) const
{
  if (m_mirror == 0)
  {
    for (unsigned a = m_lo; a <= m_hi; ++a)
      fn(a);
  }
  else
  {
    // combinations of mirror bits
    // any mirror bit can be 0 or 1 independently... i think
    std::vector<unsigned> mirror_bits;
    for (unsigned b = 0; b < 32; ++b)
      if (m_mirror & (1u << b))
        mirror_bits.push_back(1u << b);

    unsigned combos = 1u << mirror_bits.size();
    for (unsigned c = 0; c < combos; ++c)
    {
      unsigned mirror_val = 0;
      for (unsigned i = 0; i < mirror_bits.size(); ++i)
        if (c & (1u << i))
          mirror_val |= mirror_bits[i];

      for (unsigned a = m_lo; a <= m_hi; ++a)
        fn(a | mirror_val);
    }
  }
}


// address_map_entry templates

template <typename T, typename U>
address_map_entry& address_map_entry::lrw8(T&& read_fn, const char* read_name,
  U&& write_fn, const char* write_name)
{
  auto rfn = std::make_shared<std::function<u8(offs_t)>>(std::forward<T>(read_fn));
  auto wfn = std::make_shared<std::function<void(offs_t, u8)>>(std::forward<U>(write_fn));
  unsigned base = m_lo;

  for_each_addr([&](unsigned addr) {
    m_map.install_read(addr, [rfn](offs_t off) { return (*rfn)(off); }, base, read_name);
    m_map.install_write(addr, [wfn](offs_t off, u8 d) { (*wfn)(off, d); }, base, write_name);
    });
  return *this;
}

template <typename T>
address_map_entry& address_map_entry::lr8(T&& read_fn, const char* name)
{
  auto rfn = std::make_shared<std::function<u8(offs_t)>>(std::forward<T>(read_fn));
  unsigned base = m_lo;

  for_each_addr([&](unsigned addr) {
    m_map.install_read(addr, [rfn](offs_t off) { return (*rfn)(off); }, base, name);
    });
  return *this;
}

template <typename T>
address_map_entry& address_map_entry::lw8(T&& write_fn, const char* name)
{
  auto wfn = std::make_shared<std::function<void(offs_t, u8)>>(std::forward<T>(write_fn));
  unsigned base = m_lo;

  for_each_addr([&](unsigned addr) {
    m_map.install_write(addr, [wfn](offs_t off, u8 d) { (*wfn)(off, d); }, base, name);
    });
  return *this;
}

inline address_map_entry& address_map_entry::noprw()
{
  unsigned base = m_lo;
  for_each_addr([&](unsigned addr) {
    m_map.install_nop(addr, base, "noprw");
    });
  return *this;
}


#endif // ES40_ADDRESS_MAP_H
