# olewriter.rb
#
# This class should never be instantiated directly. The entire class, and all
# its methods should be considered private.

class MaxSizeError < StandardError; end

class OLEWriter < IO

   # Not meant for public consumption
   MaxSize    = 7087104
   BlockSize  = 4096
   BlockDiv   = 512
   ListBlocks = 127

   attr_reader :biff_size, :book_size, :big_blocks, :list_blocks
   attr_reader :root_start, :size_allowed

   # Accept an IO object, a fileno, or a String
   def initialize(arg, &block)
      if arg.kind_of?(String)
         super(File.open(arg, "w+").fileno, "w+", &block)
      elsif arg.respond_to?(:fileno)
         super(arg.fileno, "w+", &block)
      else
         super(arg, "w+", &block)
      end
      binmode

      @biff_only     = false
      @size_allowed  = true
      @biff_size     = 0
      @book_size     = 0
      @big_blocks    = 0
      @list_blocks   = 0
      @root_start    = 0
      @block_count   = 4
   end

   # Set the size of the data to be written to the OLE stream
   #
   # @big_blocks = (109 depot block x (128 -1 marker word)
   # - (1 x end words)) = 13842
   #
   # MaxSize = @big_blocks * 512 bytes = 7087104
   def set_size(size = BlockSize)
      raise MaxSizeError if size > MaxSize

      @biff_size = size

      if biff_size > BlockSize
         @book_size = size
      else
         @book_size = BlockSize
      end

      @size_allowed = true
   end

   # Calculate various sizes needed for the OLE stream
   def calculate_sizes
      @big_blocks  = (@book_size.to_f/BlockDiv.to_f).ceil
      @list_blocks = (@big_blocks / ListBlocks) + 1
      @root_start  = @big_blocks
   end

   # Write root entry, big block list and close the filehandle.
   def close
      if @size_allowed == true
         write_padding
         write_property_storage
         write_big_block_depot
      end
      super
   end 

   # Write the OLE header block
   def write_header
      return if @biff_only == true
      calculate_sizes
      root_start = @root_start

      write([0xD0CF11E0, 0xA1B11AE1].pack("NN"))
      write([0x00, 0x00, 0x00, 0x00].pack("VVVV"))
      write([0x3E, 0x03, -2, 0x09].pack("vvvv"))
      write([0x06, 0x00, 0x00].pack("VVV"))
      write([@list_blocks, root_start].pack("VV"))
      write([0x00, 0x1000,-2].pack("VVV"))
      write([0x00, -2 ,0x00].pack("VVV"))

      unused = [-1].pack("V")

      1.upto(@list_blocks){
         root_start += 1
         write([root_start].pack("V"))
      }

      @list_blocks.upto(108){
         write(unused)
      }
   end

   # Write a big block depot
   def write_big_block_depot
      total_blocks = @list_blocks * 128
      used_blocks  = @big_blocks + @list_blocks + 2
      
      marker = [-3].pack("V")
      eoc    = [-2].pack("V")
      unused = [-1].pack("V")

      num_blocks = @big_blocks - 1

      1.upto(num_blocks){|n|
         write([n].pack("V"))
      }

      write eoc
      write eoc

      1.upto(@list_blocks){ write(marker) }

      used_blocks.upto(total_blocks){ write(unused) }
      
   end

   # Write property storage
   def write_property_storage
      write_pps('Root Entry', 0x05,  1,   -2, 0x00)
      write_pps('Book',       0x02, -1, 0x00, @book_size)
      write_pps("",           0x00, -1, 0x00, 0x0000)
      write_pps("",           0x00, -1, 0x00, 0x0000)
   end

   # Write property sheet in property storage
   def write_pps(name, type, dir, start, size)
      length = 0
      ord_name = []
      unless name.empty? 
         name += "\0"
         ord_name = name.unpack("c*")
         length = name.length * 2
      end

      zero = [0].pack("C")
      unknown = [0].pack("V")
      
      write(ord_name.pack("v*"))

      for n in 1..64-length
         write(zero)
      end

      write([length,type,-1,-1,dir].pack("vvVVV"))

      for n in 1..5
         write(unknown)
      end
      
      for n in 1..4
         write([0].pack("V"))
      end

      write([start,size].pack("VV"))
      write(unknown)
   end

   # Pad the end of the file
   def write_padding
      min_size = 512
      min_size = BlockSize if @biff_size < BlockSize

      if @biff_size % min_size != 0
         padding = min_size - (@biff_size % min_size)
         write("\0" * padding)
      end
   end
end
