/** @file
  Implementation of routines that deal with ext2/3 block maps.

  Copyright (c) 2022 Pedro Falcato All rights reserved.
  SPDX-License-Identifier: BSD-2-Clause-Patent
**/

#include <Ext4Dxe.h>

// Note: The largest path we can take uses up 4 indices
#define EXT4_MAX_BLOCK_PATH  4

typedef enum ext4_logical_block_type {
  EXT4_TYPE_DIRECT_BLOCK = 0,
  EXT4_TYPE_SINGLY_BLOCK,
  EXT4_TYPE_DOUBLY_BLOCK,
  EXT4_TYPE_TREBLY_BLOCK,
  EXT4_TYPE_BAD_BLOCK
} EXT4_LOGICAL_BLOCK_TYPE;

/**
   @brief Detect the type of path the logical block will follow

   @param[in] LogicalBlock The logical block
   @param[in] Partition    Pointer to an EXT4_PARTITION
   @return The type of path the logical block will need to follow
 */
STATIC
EXT4_LOGICAL_BLOCK_TYPE
Ext4DetectBlockType (
  IN UINT32                LogicalBlock,
  IN CONST EXT4_PARTITION  *Partition
  )
{
  UINT32  Entries;
  UINT32  MinSinglyBlock;
  UINT32  MinDoublyBlock;
  UINT32  MinTreblyBlock;
  UINT32  MinQuadBlock;

  Entries        = (Partition->BlockSize / sizeof (UINT32));
  MinSinglyBlock = EXT4_DBLOCKS;
  MinDoublyBlock = Entries + MinSinglyBlock;
  MinTreblyBlock = Entries * Entries + MinDoublyBlock;
  MinQuadBlock   = Entries * Entries * Entries + MinTreblyBlock; // Doesn't actually exist

  if (LogicalBlock < MinSinglyBlock) {
    return EXT4_TYPE_DIRECT_BLOCK;
  } else if ((LogicalBlock >= MinSinglyBlock) && (LogicalBlock < MinDoublyBlock)) {
    return EXT4_TYPE_SINGLY_BLOCK;
  } else if ((LogicalBlock >= MinDoublyBlock) && (LogicalBlock < MinTreblyBlock)) {
    return EXT4_TYPE_DOUBLY_BLOCK;
  } else if (((LogicalBlock >= MinTreblyBlock) && (LogicalBlock < MinQuadBlock))) {
    return EXT4_TYPE_TREBLY_BLOCK;
  } else {
    return EXT4_TYPE_BAD_BLOCK;
  }
}

/**
   @brief Get a block's path in indices

   @param[in]  Partition       Pointer to an EXT4_PARTITION
   @param[in]  LogicalBlock    Logical block
   @param[out] BlockPath       Pointer to an array of EXT4_MAX_BLOCK_PATH elements, where the
                               indices we'll need to read are inserted.
   @return The number of path elements that are required (and were inserted in BlockPath)
 */
UINTN
Ext4GetBlockPath (
  IN  CONST EXT4_PARTITION  *Partition,
  IN  UINT32                LogicalBlock,
  OUT EXT2_BLOCK_NR         BlockPath[EXT4_MAX_BLOCK_PATH]
  )
{
  // The logic behind the block map is very much like a page table
  // Let's think of blocks with 512 entries (exactly like a page table on x64).
  // On doubly indirect block paths, we subtract the min doubly blocks from the logical block.
  // The top 9 bits of the result are the index inside the dind block, the bottom 9 bits are the
  // index inside the ind block. Since Entries is always a power of 2, entries - 1 will give us
  // a mask of the BlockMapBits.
  // Note that all this math could be done with ands and shifts (similar implementations exist
  // in a bunch of other places), but I'm doing it a simplified way with divs and modulus,
  // since it's not going to be a bottleneck anyway.

  UINT32  Entries;
  UINT32  EntriesEntries;
  UINT32  MinSinglyBlock;
  UINT32  MinDoublyBlock;
  UINT32  MinTreblyBlock;

  EXT4_LOGICAL_BLOCK_TYPE  Type;

  Entries        = (Partition->BlockSize / sizeof (UINT32));
  EntriesEntries = Entries * Entries;

  MinSinglyBlock = EXT4_DBLOCKS;
  MinDoublyBlock = Entries + MinSinglyBlock;
  MinTreblyBlock = EntriesEntries + MinDoublyBlock;

  Type = Ext4DetectBlockType (LogicalBlock, Partition);

  switch (Type) {
    case EXT4_TYPE_DIRECT_BLOCK:
      BlockPath[0] = LogicalBlock;
      break;
    case EXT4_TYPE_SINGLY_BLOCK:
      BlockPath[0] = EXT4_IND_BLOCK;
      BlockPath[1] = LogicalBlock - EXT4_DBLOCKS;
      break;
    case EXT4_TYPE_DOUBLY_BLOCK:
      BlockPath[0]  = EXT4_DIND_BLOCK;
      LogicalBlock -= MinDoublyBlock;
      BlockPath[1]  = LogicalBlock / Entries;
      BlockPath[2]  = LogicalBlock % Entries;
      break;
    case EXT4_TYPE_TREBLY_BLOCK:
      BlockPath[0]  = EXT4_DIND_BLOCK;
      LogicalBlock -= MinTreblyBlock;
      BlockPath[1]  = LogicalBlock / EntriesEntries;
      BlockPath[2]  = (LogicalBlock % EntriesEntries) / Entries;
      BlockPath[3]  = (LogicalBlock % EntriesEntries) % Entries;
      break;
    default:
      // EXT4_TYPE_BAD_BLOCK
      break;
  }

  return Type + 1;
}

/**
   @brief Get an extent from a block map
   Note: Also parses file holes and creates uninitialized extents from them.

   @param[in]  Buffer          Buffer of block pointers
   @param[in]  IndEntries      Number of entries in this block pointer table
   @param[in]  StartIndex      The start index from which we want to find a contiguous extent
   @param[out] Extent          Pointer to the resulting EXT4_EXTENT
 */
VOID
Ext4GetExtentInBlockMap (
  IN CONST UINT32  *Buffer,
  IN CONST UINT32  IndEntries,
  IN UINT32        StartIndex,
  OUT EXT4_EXTENT  *Extent
  )
{
  UINT32  Index;
  UINT32  FirstBlock;
  UINT32  LastBlock;
  UINT16  Count;

  Count      = 1;
  LastBlock  = Buffer[StartIndex];
  FirstBlock = LastBlock;

  if (FirstBlock == EXT4_BLOCK_FILE_HOLE) {
    // File hole, let's see how many blocks this hole spans
    Extent->ee_start_hi = 0;
    Extent->ee_start_lo = 0;

    for (Index = StartIndex + 1; Index < IndEntries; Index++) {
      if (Count == EXT4_EXTENT_MAX_INITIALIZED - 1) {
        // We've reached the max size of an uninit extent, break
        break;
      }

      if (Buffer[Index] == EXT4_BLOCK_FILE_HOLE) {
        Count++;
      } else {
        break;
      }
    }

    // We mark the extent as uninitialized, although there's a difference between uninit
    // extents and file holes.
    Extent->ee_len = EXT4_EXTENT_MAX_INITIALIZED + Count;
    return;
  }

  for (Index = StartIndex + 1; Index < IndEntries; Index++) {
    if (Count == EXT4_EXTENT_MAX_INITIALIZED) {
      // We've reached the max size of an extent, break
      break;
    }

    if ((Buffer[Index] == LastBlock + 1) && (Buffer[Index] != EXT4_BLOCK_FILE_HOLE)) {
      Count++;
    } else {
      break;
    }

    LastBlock = Buffer[Index];
  }

  Extent->ee_start_lo = FirstBlock;
  Extent->ee_start_hi = 0;
  Extent->ee_len      = Count;
}

/**
   Retrieves an extent from an EXT2/3 inode (with a blockmap).
   @param[in]      Partition     Pointer to the opened EXT4 partition.
   @param[in]      File          Pointer to the opened file.
   @param[in]      LogicalBlock  Block number which the returned extent must cover.
   @param[out]     Extent        Pointer to the output buffer, where the extent will be copied to.

   @retval EFI_SUCCESS        Retrieval was successful.
   @retval EFI_NO_MAPPING     Block has no mapping.
**/
EFI_STATUS
Ext4GetBlocks (
  IN  EXT4_PARTITION  *Partition,
  IN  EXT4_FILE       *File,
  IN  EXT2_BLOCK_NR   LogicalBlock,
  OUT EXT4_EXTENT     *Extent
  )
{
  EXT4_INODE     *Inode;
  EXT2_BLOCK_NR  BlockPath[EXT4_MAX_BLOCK_PATH];
  UINTN          BlockPathLength;
  UINTN          Index;
  UINT32         *Buffer;
  EFI_STATUS     Status;
  UINT32         Block;
  UINT32         BlockIndex;

  Inode = File->Inode;

  BlockPathLength = Ext4GetBlockPath (Partition, LogicalBlock, BlockPath);

  if (BlockPathLength - 1 == EXT4_TYPE_BAD_BLOCK) {
    // Bad logical block (out of range)
    return EFI_NO_MAPPING;
  }

  Extent->ee_block = LogicalBlock;

  if (BlockPathLength == 1) {
    // Fast path for blocks 0 - 12 that skips allocations
    Ext4GetExtentInBlockMap (Inode->i_data, EXT4_DBLOCKS, BlockPath[0], Extent);

    return EFI_SUCCESS;
  }

  Buffer = AllocatePool (Partition->BlockSize);
  if (Buffer == NULL) {
    return EFI_OUT_OF_RESOURCES;
  }

  // Note the BlockPathLength - 1 so we don't end up reading the final block
  for (Index = 0; Index < BlockPathLength - 1; Index++) {
    BlockIndex = BlockPath[Index];

    if (Index == 0) {
      Block = Inode->i_data[BlockIndex];
    } else {
      Block = Buffer[BlockIndex];
    }

    if (Block == EXT4_BLOCK_FILE_HOLE) {
      FreePool (Buffer);
      return EFI_NO_MAPPING;
    }

    Status = Ext4ReadBlocks (Partition, Buffer, 1, Block);

    if (EFI_ERROR (Status)) {
      FreePool (Buffer);
      return Status;
    }
  }

  Ext4GetExtentInBlockMap (
    Buffer,
    Partition->BlockSize / sizeof (UINT32),
    BlockPath[BlockPathLength - 1],
    Extent
    );

  FreePool (Buffer);

  return EFI_SUCCESS;
}
