#### libremiliacr
#### Copyright(C) 2020-2024 Remilia Scarlet <remilia@posteo.jp>
####
#### This program is free software: you can redistribute it and/or modify
#### it under the terms of the GNU General Public License as published
#### the Free Software Foundation, either version 3 of the License, or
#### (at your option) any later version.
####
#### This program is distributed in the hope that it will be useful,
#### but WITHOUT ANY WARRANTY; without even the implied warranty of
#### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
#### GNU General Public License for more details.
####
#### You should have received a copy of the GNU General Public License
#### along with this program.If not, see<http:####www.gnu.org/licenses/.>
require "./extensions"

lib LibC
  enum MsyncFlags
    Async = 1
    Invalidate = 2
    Sync = 4
  end

  {% if flag?(:unix) %}
    fun msync(addr : Void*, length : SizeT, flags : MsyncFlags) : LibC::Int
  {% end %}
end

module RemiLib
  # The `MmappedFile` class provides a convenient way to read from and write to
  # files using [mmap()](https://en.wikipedia.org/wiki/Mmap).  This is only
  # supported on POSIX systems, and is not meant to be a complete interface to
  # everything you can do with `mmap()`.
  #
  # TODO: This should eventually inherit from `::IO`.
  class MmappedFile
    # An error while handling an `MmappedFile`.
    class MmappedFileError < Exception
    end

    # An error when synchronizing an `MmappedFile`.
    class MmappedFileSyncError < MmappedFileError
      getter errno : Errno

      def initialize(@message : String?, @errno : Errno)
      end
    end

    # An error when attempting to read or write past the end of an
    # `MmappedFile`.
    class MmappedFileEofError < MmappedFileError
    end

    # A `Flags` enumeration that dictates the read/write state of an
    # `MmappedFile`.
    enum Mode
      Read  = 0x01
      ReadWrite = 0x03
    end

    # A `Flags` enumeration that dictates the flags for an `MmappedFile`.
    @[Flags]
    enum Mapping
      Shared    = 0x01
      Private   = 0x02
      Locked    = 0x2000
      NoReserve = 0x4000
    end

    # How an `MmappedFile` is synchronized.
    alias MsyncFlags = LibC::MsyncFlags

    # The mmapped address.
    @addr : Pointer(UInt8) = Pointer(UInt8).null

    # Length of the file.
    @len : LibC::SizeT = 0

    # The file descriptor this mapping was created from.
    @fd : Int64 = 0

    # The associated file.
    @file = uninitialized File

    # Current read position.
    getter pos : LibC::SizeT = 0

    # The `Mode` this instance was opened with.
    getter mode : Mode = Mode::Read

    # The `Mapping` this instance was created with.
    getter mapping : Mapping = Mapping::Private

    # Specifies the behavior when `#sync` or `#close` are called and this
    # instance is writeable.  If the instance isn't writeable, this is ignored.
    property syncMode : MsyncFlags = MsyncFlags::Async

    # Creates a new `MmappedFile` instance by mapping the file at *path* into
    # the memory space.
    #
    # If *mode* is `Mode::Read`, then the file must already exist.
    #
    # If *mode* is `Mode::ReadWrite`, then the file is always overwritten if it
    # exists and is recreated with the requested size.  If it does not exist, it
    # is created with the requested size.
    #
    # In all cases, *size* must be at least 1.
    #
    # When you are finished with the instance, you should call `#close` to unmap
    # the file and release the resources.  This can also happen during
    # finalization.
    def initialize(path : Path|String, size : Int,
                   *, @mode : Mode = Mode::Read, @mapping : Mapping = Mapping::Private,
                   @syncMode : MsyncFlags = MsyncFlags::Async)
      raise "Size must be at least 1" if size <= 0

      {% if flag?(:unix) %}
        # Open the file so we can get a file descriptor.
        modeStr = case @mode
                  in .read_write? then "w+b" # This automatically overwrites existing files.
                  in .read? then "rb"
                  end

        @file = File.open(path, modeStr)
        raise "Bad file descriptor" unless @file.fd >= 0
        @fd = @file.fd.to_i64

        # Do we need to size the file so that it's the correct size?
        if @mode.read_write?
          @file.seek((size.to_u64 - 1).to_i64, File::Seek::Set)
          @file.write_byte(0)
          @file.flush
          @file.seek(0)
        end

        # Get the file size.  If the user passed in a size, then it must be less
        # than or equal to the file's actual size.
        @len = (size || @file.size).to_u64!
        raise MmappedFileError.new("Bad size: #{@len} > #{@file.size.to_u64!}") unless @len <= @file.size.to_u64!

        # Call mmap()
        newAddr = LibC.mmap(Pointer(Void).null, @len, @mode.value, @mapping.value,
                            @file.fd, 0) # Offset is always 0
        raise "Null address returned" unless !newAddr.null?

        # Check for an error.
        unless ((1u128 << 64) - 1).to_u64 != newAddr.address
          raise MmappedFileError.new("Cannot map file: #{Errno.value}")
        end

        # We cast the Pointer(Void) to a Pointer(UInt8) so that we can do
        # everything with bytes.
        @addr = Pointer(UInt8).new(newAddr.address)
      {% else %}
        raise "MmappedFile is not supported on this platform"
      {% end %}
    end

    # Creates a new `MmappedFile` instance by mapping the file at *path* into
    # the memory spacem, then yields that instance and executes the block.  The
    # file must already exist.
    #
    # Look at `MmappedFile.new` for the explanation of the other parameters.
    #
    # This will automatically call `#close` at the end.
    def self.open(path : Path|String, size : Int,
                  *, mode : Mode = Mode::Read, mapping : Mapping = Mapping::Private, &)
      file = MmappedFile.new(path, size, mode: mode, mapping: mapping)
      begin
        yield file
      ensure
        file.close
      end
    end

    def finalize
      self.close
    end

    # Returns `true` if this instance is open, or `false` otherwise.
    def open? : Bool
      !@addr.nil?
    end

    def sync : Nil
      raise MmappedFileError.new("File is closed") if @addr.null?
      {% if flag?(:unix) %}
        unless @mapping.includes?(Mapping::Private) # msync does nothing with a private mapping.
          unless LibC.msync(@addr, @len, @syncMode) == 0
            err = Errno.value
            raise MmappedFileSyncError.new("Failed to synchronize MmappedFile: #{err}", err)
          end
        end
      {% else %}
        raise "MmappedFile is not supported on this platform"
      {% end %}
    end

    # Unmaps the file from memory.
    def close : Nil
      unless @addr.null?
        {% if flag?(:unix) %}
          self.sync if @mode.read_write?

          if LibC.munmap(@addr, @len) != 0
            raise MmappedFileError.new("Cannot unmap file: #{Errno.value}")
          else
            @addr = Pointer(UInt8).null
          end

          @file.close
        {% else %}
          raise "MmappedFile is not supported on this platform"
        {% end %}
      end
    end

    # Length of the file.
    def len : LibC::SizeT
      raise MmappedFileError.new("File is closed") if @addr.null?
      @len
    end

    # The file descriptor this mapping was created from.
    def fd : Int64
      raise MmappedFileError.new("File is closed") if @addr.null?
      @fd
    end

    def pos=(position : LibC::SizeT) : Nil
      raise MmappedFileError.new("File is closed") if @addr.null?
      if position > @len
        raise MmappedFileError.new("Cannot seek to position, it is past the end of the file")
      elsif position < 0
        raise MmappedFileError.new("Cannot seek to a negative position")
      end
      @pos = position
    end

    def rewind : Nil
      self.pos = 0
    end

    ###
    ### "read" method variants.  These change @pos.
    ###
    ### These don't use the "get" variants internally so that there's no double
    ### checks on the position.
    ###

    # Reads a `UInt8` from the file at the current position.  Raises an
    # `MmappedFileEofError` if there are not enough bytes to read a `UInt8`.
    @[AlwaysInline]
    def readUInt8 : UInt8
      raise MmappedFileError.new("File is closed") if @addr.null?
      raise MmappedFileEofError.new("No more bytes to read") if @pos >= @len
      ret = @addr[@pos]
      @pos += 1
      ret
    end

    # Reads a `UInt16` from the file at the current position.  Raises an
    # `MmappedFileEofError` if there are not enough bytes to read a `UInt16`.
    @[AlwaysInline]
    def readUInt16 : UInt16
      raise MmappedFileError.new("File is closed") if @addr.null?
      if @pos > @len - 2
        raise MmappedFileEofError.new("Cannot read 16-bit integer: no more bytes to read")
      end
      raise MmappedFileEofError.new("No more bytes to read") if @pos + 2 > @len
      ret = (@addr + @pos).unsafe_as(Pointer(UInt16))[0]
      @pos += 2
      ret
    end

    # Reads three bytes from the file at the current position and returns a
    # 24-bit integer as a `UInt32`.  Raises an `MmappedFileEofError` if there
    # are not enough bytes to read a 24-bit unsigned integer.
    @[AlwaysInline]
    def readUInt24 : UInt32
      raise MmappedFileError.new("File is closed") if @addr.null?
      if @pos > @len - 3
        raise MmappedFileEofError.new("Cannot read 24-bit integer: no more bytes to read")
      end
      ret = (@addr[@pos + 2].to_u32! << 16) | (@addr[@pos + 1].to_u32! << 8) | @addr[@pos]
      @pos += 3
      ret & 0xFFFFFF
    end

    # Reads a `UInt32` from the file at the current position.  Raises an
    # `MmappedFileEofError` if there are not enough bytes to read a `UInt32`.
    @[AlwaysInline]
    def readUInt32 : UInt32
      raise MmappedFileError.new("File is closed") if @addr.null?
      if @pos > @len - 4
        raise MmappedFileEofError.new("Cannot read 32-bit integer: no more bytes to read")
      end
      ret = (@addr + @pos).unsafe_as(Pointer(UInt32))[0]
      @pos += 4
      ret
    end

    # Reads a `UInt64` from the file at the current position.  Raises an
    # `MmappedFileEofError` if there are not enough bytes to read a `UInt64`.
    @[AlwaysInline]
    def readUInt64 : UInt64
      raise MmappedFileError.new("File is closed") if @addr.null?
      if @pos > @len - 8
        raise MmappedFileEofError.new("Cannot read 64-bit integer: no more bytes to read")
      end
      ret = (@addr + @pos).unsafe_as(Pointer(UInt64))[0]
      @pos += 8
      ret
    end

    # Reads a `UInt128` from the file at the current position.  Raises an
    # `MmappedFileEofError` if there are not enough bytes to read a `UInt128`.
    @[AlwaysInline]
    def readUInt128 : UInt128
      raise MmappedFileError.new("File is closed") if @addr.null?
      if @pos > @len - 16
        raise MmappedFileEofError.new("Cannot read 128-bit integer: no more bytes to read")
      end
      ret = (@addr + @pos).unsafe_as(Pointer(UInt128))[0]
      @pos += 16
      ret
    end

    # Reads *count* bytes, then returns a new `Bytes` slice containing those
    # bytes.
    @[AlwaysInline]
    def readBytes(count : Int32) : Bytes
      raise MmappedFileError.new("File is closed") if @addr.null?
      if @pos > @len - count
        raise MmappedFileEofError.new("Cannot read #{count} bytes: not enough data")
      end
      ret = Bytes.new(count)
      (@addr + @pos).copy_to(ret.to_unsafe, count)
      @pos += count
      ret
    end

    # Reads a `String` of *count* bytes long, then returns the new `String`.
    @[AlwaysInline]
    def readString(count : Int32) : String
      String.new(readBytes(count))
    end

    # Reads a `Int8` from the file at the current position.  Raises an
    # `MmappedFileEofError` if there are not enough bytes to read a `Int8`.
    @[AlwaysInline]
    def readInt8 : Int8
      readUInt8.to_i8!
    end

    # Reads a `Int16` from the file at the current position.  Raises an
    # `MmappedFileEofError` if there are not enough bytes to read a `Int16`.
    @[AlwaysInline]
    def readInt16 : Int16
      readUInt16.to_i16!
    end

    # Reads three bytes from the file at the current position and returns a
    # 24-bit integer as an `Int32`.  Raises an `MmappedFileEofError` if there
    # are not enough bytes to read a 24-bit integer.
    @[AlwaysInline]
    def readInt24 : Int32
      ret = readUInt24
      if bitflag?(ret, 0x800000)
        # Convert to negative
        -8388608_i32 + (ret & 0x7FFFFF).to_i32!
      else
        (ret & 0xFFFFFF).to_i32!
      end
    end

    # Reads an `Int32` from the file at the current position.  Raises an
    # `MmappedFileEofError` if there are not enough bytes to read an `Int32`.
    @[AlwaysInline]
    def readInt32 : Int32
      readUInt32.to_i32!
    end

    # Reads an `Int64` from the file at the current position.  Raises an
    # `MmappedFileEofError` if there are not enough bytes to read an `Int64`.
    @[AlwaysInline]
    def readInt64 : Int64
      readUInt64.to_i64!
    end

    # Reads an `Int128` from the file at the current position.  Raises an
    # `MmappedFileEofError` if there are not enough bytes to read an `Int128`.
    @[AlwaysInline]
    def readInt128 : Int128
      readUInt128.to_i128!
    end

    ###
    ### "get" method variants.  These take a position and don't change @pos.
    ###

    # Returns a `UInt8` from the file at the given position.  Raises an
    # `MmappedFileEofError` if there are not enough bytes to read a UInt8 at the
    # given position.  This does not change the internal position of the
    # internal cursor (`#pos`).
    @[AlwaysInline]
    def getUInt8(position : LibC::SizeT) : UInt8
      raise MmappedFileError.new("File is closed") if @addr.null?
      if position > @len
        raise MmappedFileEofError.new("Bad position to get a UInt8 for a file of size #{@len} bytes")
      end
      @addr[position]
    end

    # Returns a `UInt16` from the file at the given position.  Raises an
    # `MmappedFileEofError` if there are not enough bytes to read a UInt16 at
    # the given position.  This does not change the internal position of the
    # internal cursor (`#pos`).
    @[AlwaysInline]
    def getUInt16(position : LibC::SizeT) : UInt16
      raise MmappedFileError.new("File is closed") if @addr.null?
      if position > @len - 2
        raise MmappedFileEofError.new("Bad position to get a UInt16 for a file of size #{@len} bytes")
      end
      (@addr + position).unsafe_as(Pointer(UInt16))[0]
    end

    # Gets three bytes from *position* and returns a `UInt32`.
    # `MmappedFileEofError` if there are not enough bytes to read a 24-bit
    # number at the given position.  This does not change the internal position
    # of the internal cursor (`#pos`).
    @[AlwaysInline]
    def getUInt24(position : LibC::SizeT) : UInt32
      raise MmappedFileError.new("File is closed") if @addr.null?
      if position > @len - 3
        raise MmappedFileEofError.new("Bad position to get a 24-bit UInt32 for a file of size #{@len} bytes")
      end
      ret = (@addr[position + 2].to_u32! << 16) |
            (@addr[position + 1].to_u32! << 8) |
            @addr[position]
      ret & 0xFFFFFF
    end

    # Returns a `UInt32` from the file at the given position.  Raises an
    # `MmappedFileEofError` if there are not enough bytes to read a UInt32 at
    # the given position.  This does not change the internal position of the
    # internal cursor (`#pos`).
    @[AlwaysInline]
    def getUInt32(position : LibC::SizeT) : UInt32
      raise MmappedFileError.new("File is closed") if @addr.null?
      if position > @len - 4
        raise MmappedFileEofError.new("Bad position to get a UInt32 for a file of size #{@len} bytes")
      end
      (@addr + position).unsafe_as(Pointer(UInt32))[0]
    end

    # Returns a `UInt64` from the file at the given position.  Raises an
    # `MmappedFileEofError` if there are not enough bytes to read a UInt64 at
    # the given position.  This does not change the internal position of the
    # internal cursor (`#pos`).
    @[AlwaysInline]
    def getUInt64(position : LibC::SizeT) : UInt64
      raise MmappedFileError.new("File is closed") if @addr.null?
      if position > @len - 8
        raise MmappedFileEofError.new("Bad position to get a UInt64 for a file of size #{@len} bytes")
      end
      (@addr + position).unsafe_as(Pointer(UInt64))[0]
    end

    # Returns a `UInt128` from the file at the given position.  Raises an
    # `MmappedFileEofError` if there are not enough bytes to read a UInt128 at
    # the given position.  This does not change the internal position of the
    # internal cursor (`#pos`).
    @[AlwaysInline]
    def getUInt128(position : LibC::SizeT) : UInt128
      raise MmappedFileError.new("File is closed") if @addr.null?
      if position > @len - 16
        raise MmappedFileEofError.new("Bad position to get a UInt128 for a file of size #{@len} bytes")
      end
      (@addr + position).unsafe_as(Pointer(UInt128))[0]
    end

    # Gets *count* bytes from the given position, then returns a new `Bytes`
    # slice containing those bytes.  This does not change the internal position
    # of the internal cursor (`#pos`).
    def getBytes(count : Int32, position : LibC::SizeT) : Bytes
      raise MmappedFileError.new("File is closed") if @addr.null?
      if position > @len - count
        raise MmappedFileEofError.new("Cannot get #{count} bytes at #{position}: not enough data")
      end
      ret = Bytes.new(count)
      (@addr + position).copy_to(ret.to_unsafe, count)
      ret
    end

    # Gets a `String` of *count* bytes long at the given position, then returns
    # the new `String`.  This does not change the internal position of the
    # internal cursor (`#pos`).
    def getString(count : Int32, position : LibC::SizeT) : String
      String.new(getBytes(count, position))
    end

    # Returns an `Int8` from the file at the given position.  Raises an
    # `MmappedFileEofError` if there are not enough bytes to read an `Int8` at
    # the given position.  This does not change the internal position of the
    # internal cursor (`#pos`).
    def getInt8(position : LibC::SizeT) : Int8
      getUInt8(position).to_i8!
    end

    # Returns an `Int16` from the file at the given position.  Raises an
    # `MmappedFileEofError` if there are not enough bytes to read an `Int16` at
    # the given position.  This does not change the internal position of the
    # internal cursor (`#pos`).
    def getInt16(position : LibC::SizeT) : Int16
      getUInt16(position).to_i16!
    end

    # Gets three bytes from *position* and returns an `Int32`.
    # `MmappedFileEofError` if there are not enough bytes to read a 24-bit
    # integer at the given position.  This does not change the internal position
    # of the internal cursor (`#pos`).
    def getInt24(position : LibC::SizeT) : Int32
      ret = getUInt24(position)
      if bitflag?(ret, 0x800000)
        # Convert to negative
        -8388608_i32 + (ret & 0x7FFFFF).to_i32!
      else
        ret
      end
    end

    # Returns an `Int32` from the file at the given position.  Raises an
    # `MmappedFileEofError` if there are not enough bytes to read an `Int32` at
    # the given position.  This does not change the internal position of the
    # internal cursor (`#pos`).
    def getInt32(position : LibC::SizeT) : Int32
      getUInt32(position).to_i32!
    end

    # Returns an `Int64` from the file at the given position.  Raises an
    # `MmappedFileEofError` if there are not enough bytes to read an `Int64` at
    # the given position.  This does not change the internal position of the
    # internal cursor (`#pos`).
    def getInt64(position : LibC::SizeT) : Int64
      getUInt64(position).to_i64!
    end

    # Returns an `Int128` from the file at the given position.  Raises an
    # `MmappedFileEofError` if there are not enough bytes to read an `Int128` at
    # the given position.  This does not change the internal position of the
    # internal cursor (`#pos`).
    def getInt128(position : LibC::SizeT) : Int128
      getUInt128(position).to_i128!
    end

    ###
    ### "write" method variants.  These change @pos.
    ###
    ### These don't use the "put" variants internally so that there's no double
    ### checks on the position.
    ###

    # Writes a `UInt8` to the file at the current position.  If there is no
    # space to write the value, then this raises a `MmappedFileEofError`.
    @[AlwaysInline]
    def writeUInt8(val : UInt8) : Nil
      raise MmappedFileError.new("File is closed") if @addr.null?
      if @pos < @len
        @addr[@pos] = val
        @pos += 1
      else
        raise MmappedFileEofError.new("Cannot write 8-bit integer, not enough space left")
      end
    end

    # Writes a `UInt16` to the file at the current position.  If there is no
    # space to write the value, then this raises a `MmappedFileEofError`.
    @[AlwaysInline]
    def writeUInt16(val : UInt16) : Nil
      raise MmappedFileError.new("File is closed") if @addr.null?
      if @pos + 2 <= @len
        (@addr + @pos).unsafe_as(Pointer(UInt16)).value = val
        @pos += 2
      else
        raise MmappedFileEofError.new("Cannot write 16-bit integer, not enough space left")
      end
    end

    # Writes a 24-bit integer stored in a `UInt32` to the file at the current
    # position.  If there is no space to write the value, then this raises a
    # `MmappedFileEofError`.
    #
    # If *val* does not contain a 24-bit integer, this raises an
    # `ArgumentError`.
    @[AlwaysInline]
    def writeUInt24(val : UInt32, littleEndian? : Bool = true) : Nil
      raise MmappedFileError.new("File is closed") if @addr.null?
      if bitflag?(val, 0xFF000000)
        raise ArgumentError.new("Value does not contain a 24-bit integer")
      end

      if @pos + 3 <= @len
        if littleEndian?
          b1 = ((val & 0xFF0000) >> 16).to_u8!
          b2 = ((val & 0x00FF00) >> 8).to_u8!
          b3 = (val & 0x0000FF).to_u8!
        else
          b3 = ((val & 0xFF0000) >> 16).to_u8!
          b2 = ((val & 0x00FF00) >> 8).to_u8!
          b1 = (val & 0x0000FF).to_u8!
        end

        @addr[@pos    ] = b1
        @addr[@pos + 1] = b2
        @addr[@pos + 2] = b3
        @pos += 3
      else
        raise MmappedFileEofError.new("Cannot write 24-bit integer, not enough space left")
      end
    end

    # Writes a `UInt32` to the file at the current position.  If there is no
    # space to write the value, then this raises a `MmappedFileEofError`.
    @[AlwaysInline]
    def writeUInt32(val : UInt32) : Nil
      raise MmappedFileError.new("File is closed") if @addr.null?
      if @pos + 4 <= @len
        (@addr + @pos).unsafe_as(Pointer(UInt32)).value = val
        @pos += 4
      else
        raise MmappedFileEofError.new("Cannot write 32-bit integer, not enough space left")
      end
    end

    # Writes a `UInt64` to the file at the current position.  If there is no
    # space to write the value, then this raises a `MmappedFileEofError`.
    @[AlwaysInline]
    def writeUInt64(val : UInt64) : Nil
      raise MmappedFileError.new("File is closed") if @addr.null?
      if @pos + 8 <= @len
        (@addr + @pos).unsafe_as(Pointer(UInt64)).value = val
        @pos += 8
      else
        raise MmappedFileEofError.new("Cannot write 64-bit integer, not enough space left")
      end
    end

    # Writes a `UInt128` to the file at the current position.  If there is no
    # space to write the value, then this raises a `MmappedFileEofError`.
    @[AlwaysInline]
    def writeUInt128(val : UInt128) : Nil
      raise MmappedFileError.new("File is closed") if @addr.null?
      if @pos + 16 <= @len
        (@addr + @pos).unsafe_as(Pointer(UInt128)).value = val
        @pos += 16
      else
        raise MmappedFileEofError.new("Cannot write 128-bit integer, not enough space left")
      end
    end

    # Writes an `Int8` to the file at the current position.  If there is no
    # space to write the value, then this raises a `MmappedFileEofError`.
    def writeInt8(val : Int8) : Nil
      writeUInt8(val.to_u8!)
    end

    # Writes an `Int16` to the file at the current position.  If there is no
    # space to write the value, then this raises a `MmappedFileEofError`.
    def writeInt16(val : Int16) : Nil
      writeUInt16(val.to_u16!)
    end

    # Writes a 24-bit integer stored in an `Int32` to the file at the current
    # position.  If there is no space to write the value, then this raises a
    # `MmappedFileEofError`.
    def writeInt24(val : Int32) : Nil
      {% begin %}
        {% maxVal = (2 ** 23) - 1 %}
        {% minVal = -(2 ** 23) %}
        if val > {{maxVal}} || val < {{minVal}}
          raise ArgumentError.new("Value is a not a signed 24-bit integer.")
        end
      {% end %}
      writeUInt24(val.to_u32!)
    end

    # Writes an `Int32` to the file at the current position.  If there is no
    # space to write the value, then this raises a `MmappedFileEofError`.
    def writeInt32(val : Int32) : Nil
      writeUInt32(val.to_u32!)
    end

    # Writes an `Int64` to the file at the current position.  If there is no
    # space to write the value, then this raises a `MmappedFileEofError`.
    def writeInt64(val : Int64) : Nil
      writeUInt64(val.to_u64!)
    end

    # Writes an `Int128` to the file at the current position.  If there is no
    # space to write the value, then this raises a `MmappedFileEofError`.
    def writeInt128(val : Int128) : Nil
      writeUInt128(val.to_u128!)
    end

    # Writes a `Bytes` slice to the file at the current position.  If there is
    # not enough space to write the value, then this raises a
    # `MmappedFileEofError`.
    def writeBytes(data : Bytes) : Nil
      raise MmappedFileError.new("File is closed") if @addr.null?
      if @pos + data.size <= @len
        data.to_unsafe.copy_to(@addr + @pos, data.size)
        @pos += data.size
      else
        raise MmappedFileEofError.new("Cannot write #{data.size} bytes, not enough space left")
      end
    end

    # Writes a `String` as raw bytes to the file at the current position.  If
    # there is not enough space to write the value, then this raises a
    # `MmappedFileEofError`.
    def writeString(str : String) : Nil
      writeBytes(str.to_slice)
    end

    ###
    ### "put" method variants.  These take a position and don't change @pos.
    ###

    # Writes a `UInt8` to the file at the given position.  If there is no space
    # to write the value, then this raises a `MmappedFileEofError`.  This does
    # not change the internal position of the internal cursor (`#pos`).
    @[AlwaysInline]
    def putUInt8(val : UInt8, position : LibC::SizeT) : Nil
      raise MmappedFileError.new("File is closed") if @addr.null?
      if position < @len
        @addr[position] = val
      else
        raise MmappedFileEofError.new("Cannot write 8-bit integer at #{position}")
      end
    end

    # Writes a `UInt16` to the file at the given position.  If there is no space
    # to write the value, then this raises a `MmappedFileEofError`.  This does
    # not change the internal position of the internal cursor (`#pos`).
    @[AlwaysInline]
    def putUInt16(val : UInt16, position : LibC::SizeT) : Nil
      raise MmappedFileError.new("File is closed") if @addr.null?
      if position < @len - 2
        (@addr + position).unsafe_as(Pointer(UInt16)).value = val
      else
        raise MmappedFileEofError.new("Cannot write 16-bit integer at #{position}")
      end
    end

    # Writes a 24-bit integer stored in a `UInt32` to the file at the given
    # position.  If there is no space to write the value, then this raises a
    # `MmappedFileEofError`.  This does not change the internal position of the
    # internal cursor (`#pos`).
    #
    # If *val* does not contain a 24-bit integer, this raises an
    # `ArgumentError`.
    @[AlwaysInline]
    def putUInt24(val : UInt32, position : LibC::SizeT, littleEndian? : Bool = true) : Nil
      raise MmappedFileError.new("File is closed") if @addr.null?
      if bitflag?(val, 0xFF000000)
        raise ArgumentError.new("Value does not contain a 24-bit integer")
      end

      if position < @len - 3
        if littleEndian?
          b1 = ((val & 0xFF0000) >> 16).to_u8!
          b2 = ((val & 0x00FF00) >> 8).to_u8!
          b3 = (val & 0x0000FF).to_u8!
        else
          b3 = ((val & 0xFF0000) >> 16).to_u8!
          b2 = ((val & 0x00FF00) >> 8).to_u8!
          b1 = (val & 0x0000FF).to_u8!
        end

        @addr[@pos    ] = b1
        @addr[@pos + 1] = b2
        @addr[@pos + 2] = b3
        @pos += 3
      else
        raise MmappedFileEofError.new("Cannot write UInt16, not enough space left")
      end
    end

    # Writes a `UInt32` to the file at the given position.  If there is no space
    # to write the value, then this raises a `MmappedFileEofError`.  This does
    # not change the internal position of the internal cursor (`#pos`).
    @[AlwaysInline]
    def putUInt32(val : UInt32, position : LibC::SizeT) : Nil
      raise MmappedFileError.new("File is closed") if @addr.null?
      if position < @len - 4
        (@addr + position).unsafe_as(Pointer(UInt32)).value = val
      else
        raise MmappedFileEofError.new("Cannot write 32-bit integer at #{position}")
      end
    end

    # Writes a `UInt64` to the file at the given position.  If there is no space
    # to write the value, then this raises a `MmappedFileEofError.` This does
    # not change the internal position of the internal cursor (`#pos`).
    @[AlwaysInline]
    def putUInt64(val : UInt64, position : LibC::SizeT) : Nil
      raise MmappedFileError.new("File is closed") if @addr.null?
      if position < @len - 8
        (@addr + position).unsafe_as(Pointer(UInt64)).value = val
      else
        raise MmappedFileEofError.new("Cannot write 64-bit integer at #{position}")
      end
    end

    # Writes a `UInt128` to the file at the given position.  If there is no
    # space to write the value, then this raises a `MmappedFileEofError`.  This
    # does not change the internal position of the internal cursor (`#pos`).
    @[AlwaysInline]
    def putUInt128(val : UInt128, position : LibC::SizeT) : Nil
      raise MmappedFileError.new("File is closed") if @addr.null?
      if position < @len - 16
        (@addr +position).unsafe_as(Pointer(UInt128)).value = val
      else
        raise MmappedFileEofError.new("Cannot write 128-bit integer at #{position}")
      end
    end

    # Writes an `Int8` to the file at the given position.  If there is no space
    # to write the value, then this raises a `MmappedFileEofError`.  This does
    # not change the internal position of the internal cursor (`#pos`).
    def putInt8(val : Int8, position : LibC::SizeT) : Nil
      putUInt8(val.to_u8!, position)
    end

    # Writes an `Int16` to the file at the given position.  If there is no space
    # to write the value, then this raises a `MmappedFileEofError`.  This does
    # not change the internal position of the internal cursor (`#pos`).
    def putInt16(val : Int16, position : LibC::SizeT) : Nil
      putUInt16(val.to_u16!, position)
    end

    # Writes a 24-bit integer stored in a `UInt32` to the file at the given
    # position.  If there is no space to write the value, then this raises a
    # `MmappedFileEofError`.  This does not change the internal position of the
    # internal cursor (`#pos`).
    #
    # If *val* does not contain a 24-bit integer, this raises an
    # `ArgumentError`.
    def putInt24(val : Int32, position : LibC::SizeT) : Nil
      putUInt24(val.to_u32!, position)
    end

    # Writes an `Int32` to the file at the given position.  If there is no space
    # to write the value, then this raises a `MmappedFileEofError`.  This does
    # not change the internal position of the internal cursor (`#pos`).
    def putInt32(val : Int32, position : LibC::SizeT) : Nil
      putUInt32(val.to_u32!, position)
    end

    # Writes an `Int64` to the file at the given position.  If there is no space
    # to write the value, then this raises a `MmappedFileEofError`.  This does
    # not change the internal position of the internal cursor (`#pos`).
    def putInt64(val : Int64, position : LibC::SizeT) : Nil
      putUInt64(val.to_u64!, position)
    end

    # Writes an `Int128` to the file at the given position.  If there is no
    # space to write the value, then this raises a `MmappedFileEofError`.  This
    # does not change the internal position of the internal cursor (`#pos`).
    def putInt128(val : Int128, position : LibC::SizeT) : Nil
      putUInt128(val.to_u128!, position)
    end

    # Writes a `Bytes` slice to the file at the given position.  If there is not
    # enough space to write the value, then this raises a `MmappedFileEofError`.
    # This does not change the internal position of the internal cursor
    # (`#pos`).
    def putBytes(data : Bytes, position : LibC::SizeT) : Nil
      raise MmappedFileError.new("File is closed") if @addr.null?
      if position < @len - data.size
        data.to_unsafe.copy_to(@addr + position, data.size)
      else
        raise MmappedFileEofError.new("Cannot put #{data.size} bytes at position #{position}")
      end
    end

    # Writes a `String` as raw bytes to the file at the given position.  If
    # there is not enough space to write the value, then this raises a
    # `MmappedFileEofError`.  This does not change the internal position of the
    # internal cursor (`#pos`).
    def putString(str : String, position : LibC::SizeT) : Nil
      putBytes(str.to_slice, position)
    end
  end
end
