/**
 ******************************************************************************
 * Xenia : Xbox 360 Emulator Research Project                                 *
 ******************************************************************************
 * Copyright 2022 Ben Vanik. All rights reserved.                             *
 * Released under the BSD license - see LICENSE in the root for more details. *
 ******************************************************************************
 */

#include "xenia/gpu/texture_util.h"

#include <algorithm>
#include <cstring>

#include "xenia/base/assert.h"
#include "xenia/base/math.h"
#include "xenia/gpu/texture_address.h"

namespace xe {
namespace gpu {
namespace texture_util {

void GetSubresourcesFromFetchConstant(
    const xenos::xe_gpu_texture_fetch_t& fetch, uint32_t* width_minus_1_out,
    uint32_t* height_minus_1_out, uint32_t* depth_or_array_size_minus_1_out,
    uint32_t* base_page_out, uint32_t* mip_page_out,
    uint32_t* mip_min_level_out, uint32_t* mip_max_level_out) {
  uint32_t width_minus_1 = 0;
  uint32_t height_minus_1 = 0;
  uint32_t depth_or_array_size_minus_1 = 0;
  switch (fetch.dimension) {
    case xenos::DataDimension::k1D:
      assert_false(fetch.stacked);
      assert_false(fetch.tiled);
      assert_false(fetch.packed_mips);
      width_minus_1 = fetch.size_1d.width;
      break;
    case xenos::DataDimension::k2DOrStacked:
      width_minus_1 = fetch.size_2d.width;
      height_minus_1 = fetch.size_2d.height;
      depth_or_array_size_minus_1 =
          fetch.stacked ? fetch.size_2d.stack_depth : 0;
      break;
    case xenos::DataDimension::k3D:
      assert_false(fetch.stacked);
      width_minus_1 = fetch.size_3d.width;
      height_minus_1 = fetch.size_3d.height;
      depth_or_array_size_minus_1 = fetch.size_3d.depth;
      break;
    case xenos::DataDimension::kCube:
      assert_false(fetch.stacked);
      assert_true(fetch.size_2d.stack_depth == 5);
      width_minus_1 = fetch.size_2d.width;
      height_minus_1 = fetch.size_2d.height;
      depth_or_array_size_minus_1 = 5;
      break;
  }
  if (width_minus_1_out) {
    *width_minus_1_out = width_minus_1;
  }
  if (height_minus_1_out) {
    *height_minus_1_out = height_minus_1;
  }
  if (depth_or_array_size_minus_1_out) {
    *depth_or_array_size_minus_1_out = depth_or_array_size_minus_1;
  }

  uint32_t longest_axis_minus_1 = std::max(width_minus_1, height_minus_1);
  if (fetch.dimension == xenos::DataDimension::k3D) {
    longest_axis_minus_1 =
        std::max(longest_axis_minus_1, depth_or_array_size_minus_1);
  }
  uint32_t size_mip_max_level =
      xe::log2_floor(longest_axis_minus_1 + uint32_t(1));

  uint32_t base_page = fetch.base_address & 0x1FFFF;
  uint32_t mip_page = fetch.mip_address & 0x1FFFF;

  uint32_t mip_min_level, mip_max_level;
  // Not taking mip_filter == kBaseMap into account for mip_max_level because
  // the mip filter may be overridden by shader fetch instructions.
  if (mip_page == 0) {
    mip_min_level = 0;
    mip_max_level = 0;
  } else {
    mip_min_level = std::min(uint32_t(fetch.mip_min_level), size_mip_max_level);
    mip_max_level =
        std::max(std::min(uint32_t(fetch.mip_max_level), size_mip_max_level),
                 mip_min_level);
  }
  if (mip_max_level != 0) {
    if (base_page == 0) {
      mip_min_level = std::max(mip_min_level, uint32_t(1));
    }
    if (mip_min_level != 0) {
      base_page = 0;
    }
  } else {
    mip_page = 0;
  }

  if (base_page_out) {
    *base_page_out = base_page;
  }
  if (mip_page_out) {
    *mip_page_out = mip_page;
  }
  if (mip_min_level_out) {
    *mip_min_level_out = mip_min_level;
  }
  if (mip_max_level_out) {
    *mip_max_level_out = mip_max_level;
  }
}

bool GetPackedMipOffset(uint32_t width, uint32_t height, uint32_t depth,
                        xenos::TextureFormat format, uint32_t mip,
                        uint32_t& x_blocks, uint32_t& y_blocks,
                        uint32_t& z_blocks) {
  // Tile size is 32x32, and once textures go <=16 they are packed into a
  // single tile together. The math here is insane. Most sourced from
  // graph paper, looking at dds dumps and executable reverse engineering.
  //   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
  // 0         +.4x4.+ +.....8x8.....+ +............16x16............+
  // 1         +.4x4.+ +.....8x8.....+ +............16x16............+
  // 2         +.4x4.+ +.....8x8.....+ +............16x16............+
  // 3         +.4x4.+ +.....8x8.....+ +............16x16............+
  // 4 x               +.....8x8.....+ +............16x16............+
  // 5                 +.....8x8.....+ +............16x16............+
  // 6                 +.....8x8.....+ +............16x16............+
  // 7                 +.....8x8.....+ +............16x16............+
  // 8 2x2                             +............16x16............+
  // 9 2x2                             +............16x16............+
  // 0                                 +............16x16............+
  // ...                                            .....
  //
  // The 2x2 and 1x1 squares are packed in their specific positions because
  // each square is the size of at least one block (which is 4x4 pixels max)
  //
  // if (tile_aligned(w) > tile_aligned(h)) {
  //   // wider than tall, so packed horizontally
  // } else if (tile_aligned(w) < tile_aligned(h)) {
  //   // taller than wide, so packed vertically
  // } else {
  //   square
  // }
  // It's important to use logical sizes here, as the input sizes will be
  // for the entire packed tile set, not the actual texture.
  // The minimum dimension is what matters most: if either width or height
  // is <= 16 this mode kicks in.

  uint32_t log2_width = xe::log2_ceil(width);
  uint32_t log2_height = xe::log2_ceil(height);
  uint32_t log2_size = std::min(log2_width, log2_height);
  if (log2_size > 4 + mip) {
    // The shortest dimension is bigger than 16, not packed.
    x_blocks = 0;
    y_blocks = 0;
    z_blocks = 0;
    return false;
  }
  uint32_t packed_mip_base = (log2_size > 4) ? (log2_size - 4) : 0;
  uint32_t packed_mip = mip - packed_mip_base;

  // Find the block offset of the mip.
  if (packed_mip < 3) {
    if (log2_width > log2_height) {
      // Wider than tall. Laid out vertically.
      x_blocks = 0;
      y_blocks = 16 >> packed_mip;
    } else {
      // Taller than wide. Laid out horizontally.
      x_blocks = 16 >> packed_mip;
      y_blocks = 0;
    }
    z_blocks = 0;
  } else {
    uint32_t offset;
    if (log2_width > log2_height) {
      // Wider than tall. Laid out horizontally.
      offset = (1 << (log2_width - packed_mip_base)) >> (packed_mip - 2);
      x_blocks = offset;
      y_blocks = 0;
    } else {
      // Taller than wide. Laid out vertically.
      x_blocks = 0;
      offset = (1 << (log2_height - packed_mip_base)) >> (packed_mip - 2);
      y_blocks = offset;
    }
    if (offset < 4) {
      // Pack 1x1 Z mipmaps along Z - not reached for 2D.
      uint32_t log2_depth = xe::log2_ceil(depth);
      if (log2_depth > 1 + packed_mip) {
        z_blocks = (log2_depth - packed_mip) * 4;
      } else {
        z_blocks = 4;
      }
    } else {
      z_blocks = 0;
    }
  }

  x_blocks >>= FormatInfo::GetWidthShift(format);
  y_blocks >>= FormatInfo::GetHeightShift(format);
  return true;
}

TextureGuestLayout GetGuestTextureLayout(
    xenos::DataDimension dimension, uint32_t base_pitch_texels_div_32,
    uint32_t width_texels, uint32_t height_texels, uint32_t depth_or_array_size,
    bool is_tiled, xenos::TextureFormat format, bool has_packed_levels,
    bool has_base, uint32_t max_level) {
  TextureGuestLayout layout;

  if (dimension == xenos::DataDimension::k1D) {
    assert_false(is_tiled);
    // GetPackedMipOffset may result in packing along Y for `width > height`
    // textures.
    assert_false(has_packed_levels);
    height_texels = 1;
  }
  uint32_t depth =
      dimension == xenos::DataDimension::k3D ? depth_or_array_size : 1;
  switch (dimension) {
    case xenos::DataDimension::k2DOrStacked:
      layout.array_size = depth_or_array_size;
      break;
    case xenos::DataDimension::kCube:
      layout.array_size = 6;
      break;
    default:
      layout.array_size = 1;
  }

  // For safety, for instance, with empty resolve regions (extents calculation
  // may overflow otherwise due to the assumption of at least one row, for
  // example, but an empty texture is empty anyway).
  if (!width_texels || !height_texels || !depth || !layout.array_size) {
    std::memset(&layout, 0, sizeof(layout));
    return layout;
  }

  // For safety, clamp the maximum level.
  uint32_t max_level_for_dimensions =
      xe::log2_floor(std::max(std::max(width_texels, height_texels), depth));
  assert_true(max_level <= max_level_for_dimensions);
  max_level = std::min(max_level, max_level_for_dimensions);
  layout.max_level = max_level;

  layout.packed_level = has_packed_levels
                            ? GetPackedMipLevel(width_texels, height_texels)
                            : UINT32_MAX;

  // Clear unused level layouts to zero strides/sizes.
  if (!has_base) {
    std::memset(&layout.base, 0, sizeof(layout.base));
  }
  if (layout.packed_level != 0) {
    std::memset(&layout.mips[0], 0, sizeof(layout.mips[0]));
  }
  uint32_t max_stored_level =
      std::min(max_level, uint32_t(layout.packed_level));
  {
    uint32_t mips_end = max_stored_level + 1;
    assert_true(mips_end <= xe::countof(layout.mips));
    uint32_t mips_unused_count = uint32_t(xe::countof(layout.mips)) - mips_end;
    if (mips_unused_count) {
      std::memset(&layout.mips[mips_end], 0,
                  sizeof(layout.mips[0]) * mips_unused_count);
      std::memset(&layout.mip_offsets_bytes[mips_end], 0,
                  sizeof(layout.mip_offsets_bytes[0]) * mips_unused_count);
    }
  }
  layout.mips_total_extent_bytes = 0;

  const FormatInfo* format_info = FormatInfo::Get(format);
  uint32_t bytes_per_block = format_info->bytes_per_block();

  // The loop counter can mean two things depending on whether the packed mip
  // tail is stored as mip 0, because in this case, it would be ambiguous since
  // both the base and the mips would be on "level 0", but stored separately and
  // possibly with a different layout.
  uint32_t loop_level_last;
  if (layout.packed_level == 0) {
    // Packed mip tail is the level 0 - may need to load mip tails for the base,
    // the mips, or both.
    // Loop iteration 0 - base packed mip tail.
    // Loop iteration 1 - mips packed mip tail.
    loop_level_last = uint32_t(max_level != 0);
  } else {
    // Packed mip tail is not the level 0.
    // Loop iteration is the actual level being loaded.
    loop_level_last = max_stored_level;
  }
  uint32_t mip_offset_bytes = 0;
  for (uint32_t loop_level = has_base ? 0 : 1; loop_level <= loop_level_last;
       ++loop_level) {
    bool is_base = loop_level == 0;
    uint32_t level = (layout.packed_level == 0) ? 0 : loop_level;
    TextureGuestLayout::Level& level_layout =
        is_base ? layout.base : layout.mips[level];

    // Calculate the strides.
    // Mips have row / depth slice strides calculated from a mip of a texture
    // whose base size is a power of two.
    // The base mip has tightly packed depth slices, and takes the row pitch
    // from the fetch constant.
    // For stride calculation purposes, mip dimensions are always aligned to
    // 32x32x4 blocks (or x1 for the missing dimensions), including for linear
    // textures.
    // Linear texture rows are 256-byte-aligned.
    uint32_t row_pitch_texels_unaligned;
    uint32_t z_slice_stride_texel_rows_unaligned;
    if (is_base) {
      row_pitch_texels_unaligned = base_pitch_texels_div_32 << 5;
      z_slice_stride_texel_rows_unaligned = height_texels;
    } else {
      row_pitch_texels_unaligned =
          std::max(xe::next_pow2(width_texels) >> level, uint32_t(1));
      z_slice_stride_texel_rows_unaligned =
          std::max(xe::next_pow2(height_texels) >> level, uint32_t(1));
    }
    uint32_t row_pitch_blocks_tile_aligned = xe::align(
        xe::align(row_pitch_texels_unaligned, format_info->block_width) /
            format_info->block_width,
        xenos::kTextureTileWidthHeight);
    level_layout.row_pitch_bytes =
        row_pitch_blocks_tile_aligned * bytes_per_block;
    // Assuming the provided pitch is already 256-byte-aligned for linear, but
    // considering the guest-provided pitch more important (no information about
    // how the GPU actually handles unaligned rows).
    if (!is_tiled && !is_base) {
      level_layout.row_pitch_bytes = xe::align(
          level_layout.row_pitch_bytes, xenos::kTextureLinearRowAlignmentBytes);
    }
    level_layout.z_slice_stride_block_rows =
        dimension != xenos::DataDimension::k1D
            ? xe::align(xe::align(z_slice_stride_texel_rows_unaligned,
                                  format_info->block_height) /
                            format_info->block_height,
                        xenos::kTextureTileWidthHeight)
            : 1;
    level_layout.array_slice_stride_bytes =
        level_layout.row_pitch_bytes * level_layout.z_slice_stride_block_rows;
    uint32_t z_stride_bytes = level_layout.array_slice_stride_bytes;
    if (dimension == xenos::DataDimension::k3D) {
      level_layout.array_slice_stride_bytes *=
          xe::align(depth_or_array_size, xenos::kTextureTileDepth);
    }
    level_layout.array_slice_stride_bytes =
        xe::align(level_layout.array_slice_stride_bytes,
                  xenos::kTextureSubresourceAlignmentBytes);

    // Estimate the memory amount actually referenced by the texture, which may
    // be smaller (especially in the 1280x720 linear k_8_8_8_8 case in 4E4D083E,
    // for which memory exactly for 1280x720 is allocated, and aligning the
    // height to 32 would cause access of an unallocated page) or bigger than
    // the stride.
    if (level == layout.packed_level) {
      // Calculate the portion of the mip tail actually used by the needed mips.
      // The actually used region may be significantly smaller than the full
      // 32x32-texel-aligned (and, for mips, calculated from the base dimensions
      // rounded to powers of two - 58410A7A has an 80x260 tiled texture with
      // packed mips at level 3 containing a mip ending at Y = 36, while
      // 260 >> 3 == 32, but 512 >> 3 == 64) tail. A 2x2 texture (for example,
      // in 494707D4, there's a 2x2 k_8_8_8_8 linear texture with packed mips),
      // for instance, would have its 2x2 base at (16, 0) and its 1x1 mip at
      // (8, 0) - and we need 2 or 1 rows in these cases, not 32 - the 32 rows
      // in a linear texture (with 256-byte pitch alignment) would span two 4 KB
      // pages rather than one.
      level_layout.x_extent_blocks = 0;
      level_layout.y_extent_blocks = 0;
      level_layout.z_extent = 0;
      uint32_t packed_sublevel_last = is_base ? 0 : max_level;
      for (uint32_t packed_sublevel = layout.packed_level;
           packed_sublevel <= packed_sublevel_last; ++packed_sublevel) {
        uint32_t packed_sublevel_x_blocks;
        uint32_t packed_sublevel_y_blocks;
        uint32_t packed_sublevel_z;
        GetPackedMipOffset(width_texels, height_texels, depth, format,
                           packed_sublevel, packed_sublevel_x_blocks,
                           packed_sublevel_y_blocks, packed_sublevel_z);
        level_layout.x_extent_blocks = std::max(
            level_layout.x_extent_blocks,
            packed_sublevel_x_blocks +
                xe::align(
                    std::max(width_texels >> packed_sublevel, uint32_t(1)),
                    format_info->block_width) /
                    format_info->block_width);
        level_layout.y_extent_blocks = std::max(
            level_layout.y_extent_blocks,
            packed_sublevel_y_blocks +
                xe::align(
                    std::max(height_texels >> packed_sublevel, uint32_t(1)),
                    format_info->block_height) /
                    format_info->block_height);
        level_layout.z_extent =
            std::max(level_layout.z_extent,
                     packed_sublevel_z +
                         std::max(depth >> packed_sublevel, uint32_t(1)));
      }
    } else {
      level_layout.x_extent_blocks =
          xe::align(std::max(width_texels >> level, uint32_t(1)),
                    format_info->block_width) /
          format_info->block_width;
      level_layout.y_extent_blocks =
          xe::align(std::max(height_texels >> level, uint32_t(1)),
                    format_info->block_height) /
          format_info->block_height;
      level_layout.z_extent = std::max(depth >> level, uint32_t(1));
    }
    if (is_tiled) {
      uint32_t bytes_per_block_log2 = xe::log2_floor(bytes_per_block);
      if (dimension == xenos::DataDimension::k3D) {
        level_layout.array_slice_data_extent_bytes =
            uint32_t(GetTiledAddressUpperBound3D(
                level_layout.x_extent_blocks, level_layout.y_extent_blocks,
                level_layout.z_extent, row_pitch_blocks_tile_aligned,
                level_layout.z_slice_stride_block_rows, bytes_per_block_log2));
      } else {
        level_layout.array_slice_data_extent_bytes =
            GetTiledAddressUpperBound2D(
                level_layout.x_extent_blocks, level_layout.y_extent_blocks,
                row_pitch_blocks_tile_aligned, bytes_per_block_log2);
      }
    } else {
      level_layout.array_slice_data_extent_bytes =
          z_stride_bytes * (level_layout.z_extent - 1) +
          level_layout.row_pitch_bytes * (level_layout.y_extent_blocks - 1) +
          bytes_per_block * level_layout.x_extent_blocks;
    }
    level_layout.level_data_extent_bytes =
        level_layout.array_slice_stride_bytes * (layout.array_size - 1) +
        level_layout.array_slice_data_extent_bytes;

    if (!is_base) {
      layout.mip_offsets_bytes[level] = mip_offset_bytes;
      layout.mips_total_extent_bytes =
          std::max(layout.mips_total_extent_bytes,
                   mip_offset_bytes + level_layout.level_data_extent_bytes);
      mip_offset_bytes +=
          level_layout.array_slice_stride_bytes * layout.array_size;
    }
  }

  return layout;
}

uint32_t GetTiledAddressUpperBound2D(uint32_t right, uint32_t bottom,
                                     uint32_t pitch_aligned,
                                     uint32_t bytes_per_block_log2) {
  if (!right || !bottom) {
    return 0;
  }
  // Get the origin of the 32x32 tile containing the last texel.
  uint32_t upper_bound = uint32_t(texture_address::Tiled2D(
      int32_t((right - 1) & ~(xenos::kTextureTileWidthHeight - 1)),
      int32_t((bottom - 1) & ~(xenos::kTextureTileWidthHeight - 1)),
      pitch_aligned, bytes_per_block_log2));
  switch (bytes_per_block_log2) {
    case 0:
      // Independent addressing within 128x128 portions, but the extent is 0xA00
      // bytes from the 32x32 tile origin.
      upper_bound += 0xA00;
      break;
    case 1:
      // Independent addressing within 64x64 portions, but the extent is 0xC00
      // bytes from the 32x32 tile origin.
      upper_bound += 0xC00;
      break;
    default:
      upper_bound += UINT32_C(0x400) << bytes_per_block_log2;
      break;
  }
  return upper_bound;
}

uint64_t GetTiledAddressUpperBound3D(uint32_t right, uint32_t bottom,
                                     uint32_t back, uint32_t pitch_aligned,
                                     uint32_t height_aligned,
                                     uint32_t bytes_per_block_log2) {
  if (!right || !bottom || !back) {
    return 0;
  }
  // Get the origin of the 32x32x4 tile containing the last texel.
  uint64_t upper_bound = uint64_t(texture_address::Tiled3D(
      int32_t((right - 1) & ~(xenos::kTextureTileWidthHeight - 1)),
      int32_t((bottom - 1) & ~(xenos::kTextureTileWidthHeight - 1)),
      int32_t((back - 1) & ~(xenos::kTextureTileDepth - 1)), pitch_aligned,
      height_aligned, bytes_per_block_log2));
  switch (bytes_per_block_log2) {
    case 0:
      // 64x32x8 portions have independent addressing.
      // Extent relative to the 32x32x4 tile origin:
      // - Pitch = 32, 96, 160...: (Pitch / 64) * 0x1000 + 0x1000
      // - Pitch = 64, 128, 192...: (Pitch / 64) * 0x1000 + 0xC00
      upper_bound += ((pitch_aligned >> 6) << 12) + 0xC00 +
                     ((pitch_aligned & (1 << 5)) << (10 - 5));
      break;
    default:
      // 32x32x8 portions have independent addressing.
      // Extent: ((Pitch / 32) * 0x1000 + 0x1000) * (BPB / 2)
      // Or: ((Pitch / 32) * 0x1000 / 2 + 0x1000 / 2) * BPB
      upper_bound += ((pitch_aligned << (12 - 5 - 1)) + (0x1000 >> 1))
                     << bytes_per_block_log2;
      break;
  }
  return upper_bound;
}

uint8_t SwizzleSigns(const xenos::xe_gpu_texture_fetch_t& fetch) {
  uint8_t signs = 0;
  bool any_not_signed = false, any_signed = false;
  // 0b00 or 0b01 for each component, whether it's constant 0/1.
  uint8_t constant_mask = 0b00000000;
  for (uint32_t i = 0; i < 4; ++i) {
    uint32_t swizzle = (fetch.swizzle >> (i * 3)) & 0b111;
    if (swizzle & 0b100) {
      constant_mask |= uint8_t(1) << (i * 2);
    } else {
      xenos::TextureSign sign =
          xenos::TextureSign((fetch.dword_0 >> (2 + swizzle * 2)) & 0b11);
      signs |= uint8_t(sign) << (i * 2);
      if (sign == xenos::TextureSign::kSigned) {
        any_signed = true;
      } else {
        any_not_signed = true;
      }
    }
  }
  xenos::TextureSign constants_sign = xenos::TextureSign::kUnsigned;
  if (constant_mask == 0b01010101) {
    // If only constant components, choose according to the original format
    // (what would more likely be loaded if there were non-constant components).
    // If all components would be signed, use signed.
    // Textures with only constant components must still be bound to shaders for
    // various queries (such as filtering weights) not involving the color data
    // itself.
    if (((fetch.dword_0 >> 2) & 0b11111111) ==
        uint32_t(xenos::TextureSign::kSigned) * 0b01010101) {
      constants_sign = xenos::TextureSign::kSigned;
    }
  } else {
    // If only signed and constant components, reading just from the signed host
    // view is enough.
    if (any_signed && !any_not_signed) {
      constants_sign = xenos::TextureSign::kSigned;
    }
  }
  signs |= uint8_t(constants_sign) * constant_mask;
  return signs;
}

void GetClampModesForDimension(const xenos::xe_gpu_texture_fetch_t& fetch,
                               xenos::ClampMode& clamp_x_out,
                               xenos::ClampMode& clamp_y_out,
                               xenos::ClampMode& clamp_z_out) {
  clamp_x_out = xenos::ClampMode::kClampToEdge;
  clamp_y_out = xenos::ClampMode::kClampToEdge;
  clamp_z_out = xenos::ClampMode::kClampToEdge;
  switch (fetch.dimension) {
    case xenos::DataDimension::k3D:
      clamp_z_out = fetch.clamp_z;
      [[fallthrough]];
    case xenos::DataDimension::k2DOrStacked:
      clamp_y_out = fetch.clamp_y;
      [[fallthrough]];
    case xenos::DataDimension::k1D:
      clamp_x_out = fetch.clamp_x;
      break;
    default:
      // Not applicable to cube textures.
      break;
  }
}

}  // namespace texture_util
}  // namespace gpu
}  // namespace xe
