
#====================================================================#
# r2e.rb
#====================================================================#

#====================================================================#
# R2E Module
module R2E

  #==================================================================#
  # Archive Class
  class Archive

    #================================================================#
    # Initialize Method

    def initialize
      @kcode  = 'none'
      @script = []
      @dll    = []
    end

    #================================================================#
    # Instance Methods

    def set_kcode(kcode)
      @kcode = kcode
    end

    def add_script(name, filepath)
      @script << Script.new(name, filepath)
    end

    def add_dll(name, filepath)
      @dll << DLL.new(name, filepath)
    end

    def pack
      name_table   = NameTable.new
      script_table = ScriptTable.new
      dll_table    = DLLTable.new

      @script.each { |script|
        script_table.add(name_table.add(script.name), script.pack)
      }

      @dll.each { |dll|
        dll_table.add(name_table.add(dll.name), dll.pack)
      }

      packed_name_table   = name_table.pack
      packed_script_table = script_table.pack
      packed_dll_table    = dll_table.pack

      archive_header = ArchiveHeader.new
      archive_header.signature        = 'RUBY'
      archive_header.time_date_stamp  = Time.now.to_i
      archive_header.offset_to_name   = ArchiveHeader.size
      archive_header.number_of_name   = name_table.size
      archive_header.offset_to_script = ArchiveHeader.size + packed_name_table.size
      archive_header.number_of_script = script_table.size
      archive_header.offset_to_dll    = ArchiveHeader.size + packed_name_table.size + packed_script_table.size
      archive_header.number_of_dll    = dll_table.size

      archive_header.options = case @kcode
                               when 'none' then ArchiveHeader::OPTIONS_KCODE_NONE
                               when 'euc'  then ArchiveHeader::OPTIONS_KCODE_EUC
                               when 'sjis' then ArchiveHeader::OPTIONS_KCODE_SJIS
                               when 'utf8' then ArchiveHeader::OPTIONS_KCODE_UTF8
                               else raise 'invalid char-set'
                               end

      return archive_header.pack + packed_name_table + packed_script_table + packed_dll_table
    end

    def output(io)
      io.print(self.pack)
    end

    def output_to_file(filepath)
      File.open(filepath, 'wb') { |file|
        self.output(file)
      }
    end

    #================================================================#
    # Script Class
    class Script

      #==============================================================#
      # Initialize Method

      def initialize(name, filepath)
        @name     = name
        @filepath = filepath
      end

      #==============================================================#
      # Accessor

      attr_reader :name, :filepath

      #==============================================================#
      # Instance Methods

      def pack
        return File.open(filepath, 'rb') { |file| file.read }
      end

    end # Script

    #================================================================#
    # DLL Class
    class DLL

      #==============================================================#
      # Initialize Method

      def initialize(name, filepath)
        @name     = name
        @filepath = filepath
      end

      #==============================================================#
      # Accessor

      attr_reader :name, :filepath

      #==============================================================#
      # Instance Methods

      def pack
        return File.open(filepath, 'rb') { |file| file.read }
      end

    end # DLL

  end # Archive

  #==================================================================#
  # ArchiveHeader Class
  class ArchiveHeader

    OPTIONS_KCODE_NONE = 0
    OPTIONS_KCODE_EUC  = 1
    OPTIONS_KCODE_SJIS = 2
    OPTIONS_KCODE_UTF8 = 3

    #================================================================#
    # Initialize Method

    def initialize
      @signature        = nil
      @time_date_stamp  = nil
      @offset_to_name   = nil
      @number_of_name   = nil
      @offset_to_script = nil
      @number_of_script = nil
      @offset_to_dll    = nil
      @number_of_dll    = nil
      @options          = nil
    end

    #================================================================#
    # Accessor

    attr_accessor :signature, :time_date_stamp, :offset_to_name, :number_of_name,
                  :offset_to_script, :number_of_script, :offset_to_dll, :number_of_dll, :options

    #================================================================#
    # Class Methods

    def self.size
      return 4 * 9
    end

    #================================================================#
    # Instance Methods

    def pack
      return [@signature, @time_date_stamp, @offset_to_name, @number_of_name, @offset_to_script, @number_of_script, @offset_to_dll, @number_of_dll, @options].pack('Z4L8')
    end

  end # ArchiveHeader

  #==================================================================#
  # NameHeader Class
  class NameHeader

    #================================================================#
    # Initialize Method

    def initialize
      @id     = nil
      @offset = nil
      @size   = nil
    end

    #================================================================#
    # Accessor

    attr_accessor :id, :offset, :size

    #================================================================#
    # Class Methods

    def self.size
      return 4 * 3
    end

    #================================================================#
    # Instance Methods

    def pack
      return [@id, @offset, @size].pack('L3')
    end

  end # NameHeader

  #==================================================================#
  # ScriptHeader Class
  class ScriptHeader

    #================================================================#
    # Initialize Method

    def initialize
      @name_id = nil
      @offset  = nil
      @size    = nil
    end

    #================================================================#
    # Accessor

    attr_accessor :name_id, :offset, :size

    #================================================================#
    # Class Methods

    def self.size
      return 4 * 3
    end

    #================================================================#
    # Instance Methods

    def pack
      return [@name_id, @offset, @size].pack('L3')
    end

  end # ScriptHeader

  #==================================================================#
  # DLLHeader Class
  class DLLHeader

    #================================================================#
    # Initialize Method

    def initialize
      @name_id = nil
      @offset  = nil
      @size    = nil
    end

    #================================================================#
    # Accessor

    attr_accessor :name_id, :offset, :size

    #================================================================#
    # Class Methods

    def self.size
      return 4 * 3
    end

    #================================================================#
    # Instance Methods

    def pack
      return [@name_id, @offset, @size].pack('L3')
    end

  end # DLLHeader

  #==================================================================#
  # NameTable Class
  class NameTable

    #================================================================#
    # Initialize Method

    def initialize
      @name = []
    end

    #================================================================#
    # Instance Methods

    def add(name)
      @name << Item.new(@name.size + 1, name)
      return @name.size
    end

    def size
      return @name.size
    end

    def pack
      header = ''
      table  = ''
      header_size = @name.size * NameHeader.size

      @name.each { |name|
        name_header = NameHeader.new
        name_header.id     = name.id
        name_header.offset = header_size + table.size
        name_header.size   = name.size

        header << name_header.pack
        table  << name.pack
      }

      return header + table
    end

    #================================================================#
    # Item Class
    class Item

      #==============================================================#
      # Initialize Method

      def initialize(id, name)
        @id   = id
        @name = name
      end

      #==============================================================#
      # Accessor

      attr_reader :id, :name

      #==============================================================#
      # Instance Methods

      def size
        return @name.size
      end

      def pack
        return @name + "\0"
      end

    end # Item

  end # NameTable

  #==================================================================#
  # ScriptTable Class
  class ScriptTable

    #================================================================#
    # Initialize Method

    def initialize
      @script = []
    end

    #================================================================#
    # Instance Methods

    def add(name_id, data)
      @script << Item.new(name_id, data)
    end

    def size
      return @script.size
    end

    def pack
      header_size = ScriptHeader.size * @script.size
      header = ''
      table  = ''

      @script.each { |script|
        script_header = ScriptHeader.new
        script_header.name_id = script.name_id
        script_header.offset  = header_size + table.size
        script_header.size    = script.size

        header << script_header.pack
        table  << script.pack
      }

      return header + table
    end

    #================================================================#
    # Item Class
    class Item

      #==============================================================#
      # Initialize Method

      def initialize(name_id, data)
        @name_id = name_id
        @data    = data
      end

      #==============================================================#
      # Accessor

      attr_reader :name_id, :data

      #==============================================================#
      # Instance Method

      def size
        return @data.size
      end

      def pack
        return @data + "\0"
      end

    end # Item

  end # ScriptTable

  #==================================================================#
  # DLLTable Class
  class DLLTable

    #================================================================#
    # Initialize Method

    def initialize
      @dll = []
    end

    #================================================================#
    # Instance Methods

    def add(name_id, data)
      @dll << Item.new(name_id, data)
    end

    def size
      return @dll.size
    end

    def pack
      header_size = DLLHeader.size * @dll.size
      header = ''
      table  = ''

      @dll.each { |dll|
        dll_header = DLLHeader.new
        dll_header.name_id = dll.name_id
        dll_header.offset  = header_size + table.size
        dll_header.size    = dll.size

        header << dll_header.pack
        table  << dll.pack
      }

      return header + table
    end

    #================================================================#
    # Item Class
    class Item

      #==============================================================#
      # Initialize Method

      def initialize(name_id, data)
        @name_id = name_id
        @data    = data
      end

      #==============================================================#
      # Accessor

      attr_reader :name_id, :data

      #==============================================================#
      # Instance Methods

      def size
        return @data.size
      end

      def pack
        return @data + "\0"
      end

    end # Item

  end # DLLTable

  #==================================================================#
  # Binary Module
  module Binary

    #================================================================#
    # Archive Class
    class Archive

      #==============================================================#
      # Initialize Method

      def initialize(binary)
        @binary = binary
      end

      #==============================================================#
      # Class Methods

      def self.new_from_file(filepath)
        return self.new(File.open(filepath, 'rb') { |file| file.read })
      end

      #==============================================================#
      # Instance Methods

      def size
        return @binary.size
      end

      def selfcheck
        unless @binary[0, 4] == 'RUBY'
          raise 'アーカイブのシグネチャが不正です。'
        end
      end

      def output(io)
        io.print(@binary)
      end

    end # Archive

    #================================================================#
    # Core Class
    class Core

      #==============================================================#
      # Class Constants

      ARCHIVE_OFFSET = 0x34
      ARCHIVE_SIZE   = 0x38

      #==============================================================#
      # Initialize Method

      def initialize(binary)
        @binary = binary
      end

      #==============================================================#
      # Class Methods

      def self.new_from_file(filepath)
        return self.new(File.open(filepath, 'rb') { |file| file.read })
      end

      #==============================================================#
      # Instance Method

      def size
        return @binary.size
      end

      def archive_offset
        return @binary[ARCHIVE_OFFSET, 4].unpack('I')[0]
      end

      def archive_offset=(value)
        @binary[ARCHIVE_OFFSET, 4] = [value].pack('I')
      end

      def archive_size
        return @binary[ARCHIVE_SIZE, 4].unpack('I')[0]
      end

      def archive_size=(value)
        @binary[ARCHIVE_SIZE, 4] = [value].pack('I')
      end

      def selfcheck
        unless @binary[0, 2] == 'MZ'
          raise 'コアのシグネチャが不正です。'
        end

        unless self.archive_offset == 0 && self.archive_size == 0
          raise 'コアのヘッダが不正です。既にアーカイブが埋め込まれています。'
        end
      end

      def output(io)
        io.print(@binary)
      end

    end # Core

    #================================================================#
    # Executable Class
    class Executable

      #==============================================================#
      # Initialize Method

      def initialize(archive, core)
        @archive = archive
        @core    = core
      end

      #==============================================================#
      # Instance Method

      def selfcheck
        @archive.selfcheck
        @core.selfcheck
      end

      def output(io)
        @core.archive_offset = @core.size
        @core.archive_size   = @archive.size
        @core.output(io)
        @archive.output(io)
      end

      def output_to_file(filepath)
        File.open(filepath, 'wb') { |file|
          self.output(file)
        }
      end

    end # Executable

  end # Binary

end # R2E

#====================================================================#
# End of source.
#====================================================================#
