#pragma once

#include <array>
#include <cstdint>
#include <stdexcept>

namespace xcom {

using PlanRow = std::array<std::uint32_t, 9>;

struct Plan {
  PlanRow sender{};
  PlanRow receiver{};
};

inline unsigned logicalToPhysical(unsigned logical) {
  const unsigned pair = logical / 2;
  const unsigned lane = logical & 1u;
  const unsigned block = pair / 23;
  unsigned row = pair % 23;
  if (block & 1u) {
    row = 22 - row;
  }
  const unsigned col = (block / 2) * 4 + (block & 1u);
  return row * 64 + col + lane * 2;
}

namespace detail {

inline int foldedColumn(unsigned physical) {
  const int column = static_cast<int>((physical >> 2) & 15u);
  return column > 7 ? column ^ 15 : column;
}

// Signed route displacement used by the SDK's XCOM timing helpers.
inline int routeDisplacement(unsigned source, unsigned destination) {
  const int sourceRaw = static_cast<int>((source >> 2) & 15u);
  const int destinationRaw = static_cast<int>((destination >> 2) & 15u);
  const int sourceColumn = sourceRaw > 7 ? sourceRaw ^ 15 : sourceRaw;
  const int destinationColumn =
      destinationRaw > 7 ? destinationRaw ^ 15 : destinationRaw;
  const int sourceMux = sourceColumn + ((sourceRaw >> 3) ^ (source & 1u));
  const int base = (destinationColumn - sourceMux) * 6;
  const unsigned destinationLane = destination & 3u;
  const unsigned destinationHalf = destinationRaw >> 3;
  if (destinationLane > 1) {
    return base + (destinationHalf == (destination & 1u) ? 2 : 4);
  }
  return base + (destinationHalf == destinationLane ? 1 : 5);
}

inline unsigned direction(unsigned source, unsigned destination) {
  return routeDisplacement(source, destination) < 1 ? 2u : 1u;
}

inline int timeToMux(unsigned source, unsigned destination) {
  const int sourceRaw = static_cast<int>((source >> 2) & 15u);
  const int destinationRaw = static_cast<int>((destination >> 2) & 15u);
  const int sourceLow = static_cast<int>((source >> 2) & 7u);
  const int displacement = routeDisplacement(source, destination);

  const int sourceEdge =
      sourceRaw > 7 ? (sourceRaw * 4) ^ 60 : sourceRaw * 4;
  const int destinationEdge =
      destinationRaw > 7 ? (destinationRaw * 4) ^ 60 : destinationRaw * 4;
  const int local = ((source >> 2) & 8u) | ((source >> 3) & 3u);
  const int crossing = local - destinationRaw + ((sourceLow >> 1) ^ 3);
  const bool sameRegion = ((source ^ destination) & 0x20u) == 0;
  const int turn = sameRegion ? sourceLow + 1 : 16 - sourceLow;
  const int groupDelta =
      (static_cast<int>((source >> 6) & 31u) -
       static_cast<int>((destination >> 6) & 31u)) *
      2;
  return crossing + sourceEdge + turn - destinationEdge + groupDelta +
         (displacement < 0 ? -displacement : displacement) - 34;
}

inline std::uint32_t delay(unsigned cycles) {
  return 0x40a00000u | (cycles & 0x7ffffu);
}

inline std::uint32_t delayPic(unsigned a, unsigned b = 0, unsigned c = 0) {
  return 0x60000000u | ((a << 19) & 0x03f80000u) |
         ((b << 18) & 0x00040000u) | (c & 0x3ffffu);
}

inline std::uint32_t delayXPic(unsigned a, unsigned b = 0, unsigned c = 0) {
  return 0x64000000u | ((a << 14) & 0x03ffc000u) |
         ((b << 13) & 0x00002000u) | (c & 0x1fffu);
}

inline std::uint32_t send(unsigned countMinusOne, unsigned direction) {
  return 0x78000000u | ((countMinusOne << 21) & 0x07e00000u) |
         (direction & 7u);
}

inline std::uint32_t sendOff(unsigned countMinusOne, unsigned direction) {
  return 0x70000000u | ((countMinusOne << 21) & 0x07e00000u) |
         (((countMinusOne >> 6) << 14) & 0x000fc000u) | (direction & 7u);
}

} // namespace detail

inline Plan assemble(unsigned senderLogical, unsigned receiverLogical,
                     unsigned count) {
  if (senderLogical >= 1472 || receiverLogical >= 1472) {
    throw std::invalid_argument("logical tile must be in [0, 1471]");
  }
  if (senderLogical == receiverLogical) {
    throw std::invalid_argument("sender and receiver must differ");
  }
  // The receive count occupies a 12-bit delayxpic field after a 53-word prefix.
  if (count == 0 || count > 4148) {
    throw std::invalid_argument("exchange count must be in [1, 4148]");
  }

  const unsigned sender = logicalToPhysical(senderLogical);
  const unsigned receiver = logicalToPhysical(receiverLogical);
  const unsigned direction = detail::direction(sender, receiver);
  const int muxTime = detail::timeToMux(sender, receiver);
  // A delay immediate N advances to the event scheduled N + 1 cycles later.
  const int senderDelay = 111 - muxTime;
  if (senderDelay < -1 || senderDelay > 0x7ffff) {
    throw std::runtime_error("computed sender delay is not encodable");
  }

  Plan result;
  result.sender[0] = 0x41800003u; // sync 3
  unsigned senderEnd = 1;
  if (senderDelay >= 0) {
    result.sender[senderEnd++] =
        detail::delay(static_cast<unsigned>(senderDelay));
  }
  const unsigned firstPacket = count < 64 ? count : 64;
  result.sender[senderEnd++] = detail::send(firstPacket - 1, direction);
  if (count > 64) {
    result.sender[senderEnd++] = detail::sendOff(count - 65, direction);
  }
  const int trailingDelay = 4 - senderDelay - static_cast<int>(count);
  if (trailingDelay >= 0) {
    result.sender[senderEnd++] =
        detail::delay(static_cast<unsigned>(trailingDelay));
  }
  result.sender[senderEnd] = 0x43a00000u; // br $m10

  result.receiver[0] = 1; // patch code word 1 with the physical source ID
  result.receiver[1] = 0x41800003u;
  result.receiver[2] = detail::delayXPic(112);
  if (count <= 51) {
    result.receiver[3] = detail::delayXPic(count - 1, 0, 0x640);
    result.receiver[4] = detail::delayPic(51 - count);
    result.receiver[5] = detail::delay(count + 4);
    result.receiver[6] = 0x43a00000u;
  } else if (count == 52) {
    // The SDK emits an alignment-sensitive dual-issue bundle here. Splitting
    // the same 52 receive cycles avoids depending on the plan buffer address.
    result.receiver[3] = detail::delayPic(50);
    result.receiver[4] = detail::delayXPic(0, 0, 0x640);
    result.receiver[5] = detail::delay(56);
    result.receiver[6] = 0x43a00000u;
  } else {
    result.receiver[3] = detail::delayPic(51);
    result.receiver[4] = detail::delayXPic(count - 53, 0, 0x640);
    result.receiver[5] = detail::delay(56);
    result.receiver[6] = 0x43a00000u;
  }
  return result;
}

} // namespace xcom
