module DDE
  extend FFI::Library  # todo: < Array ?
#  class Conv < FFI::Union
#    layout( :w, :ushort, # word should be 2 bytes, not 8
#            :d, :double, # it is 8 bytes
#            :b, [:char, 8]) # it is 8 bytes
#  end
#

  # XLTable class represents a single chunk of DDE data formatted as an Excel table
  class XlTable
    include Win::DDE

    # Received data types
    TDT_FLOAT = 1
    TDT_STRING = 2
    TDT_BOOL = 3
    TDT_ERROR = 4
    TDT_BLANK = 5
    TDT_INT = 6
    TDT_SKIP = 7
    TDT_TABLE = 16

    TDT_TYPES = {
            TDT_FLOAT => 'TDT_FLOAT',
            TDT_STRING =>'TDT_STRING',
            TDT_BOOL => 'TDT_BOOL',
            TDT_ERROR => 'TDT_ERROR',
            TDT_BLANK => 'TDT_BLANK',
            TDT_INT => 'TDT_INT',
            TDT_SKIP => 'TDT_SKIP',
            TDT_TABLE => 'TDT_TABLE'
    }

    attr_accessor :topic, # topic prefix
                  :time, # time spent parsing last transaction data
                  :total_time, # total time spent parsing data
                  :num_trans # number of data transactions

    def initialize
      @table = []    # Array contains Arrays of Strings
      @col = 0
      @row = 0
      @total_time = 0
      @total_records = 0
      # omitting separators for now
    end

    # tests if table data is empty or contains data in inconsistent state
    def empty?
      @table.empty? ||
              @row == 0 || @col == 0 ||
              @row != @table.size ||
              @col != @table.first.size  # assumes first element is also an Array
    end

    def data?;
      !empty?
    end

    def draw
      return false if empty?
      Encoding.default_external = 'cp866'
      # omitting separator gymnastics for now
      cout "-----\n"
      @table.each{|row| cout @topic; row.each {|col| cout " #{col}"}; cout "\n"}
    end

    def debug
      return false if empty?
      Encoding.default_external = 'cp866'
      # omitting separator gymnastics for now
      cout "-----\n"
      @table.each_with_index{|row, i| (cout @topic, i; p row) unless row == []}
      STDIN.gets
    end
    
    def receive(data_handle, mode = :collect)
      $mode = mode
      start = Time.now

      @offset = 0
      @pos = 0 #; @c=0; @r=0

      @data, total_size = dde_get_data(data_handle) #dde_access_data(dde_handle)
p @data.get_bytes(0, total_size) if $mode == :debug

      return nil unless @data &&  # DDE data is present at given dde_handle
      read_int == TDT_TABLE &&    # and, first data block is tdtTable
      read_int == 4               # and, its length is 4 bytes

      @row = read_int
      @col = read_int
      return nil unless @row != 0 && @col != 0  # Make sure nonzero row and col

p "data set size #{total_size}, row #{@row}, col #{@col}" if $mode == :debug
@strings = @floats = @flints = @ints = @blanks = @skips = @bools = @errors = 0

      @table = Array.new(@row){||Array.new}

      while @offset <= total_size-4   # Need at least 4 bytes ahead to read data type and size
        type = read_int         # Next data field(s) type
        size = read_int    # Next data field(s) length in bytes

p "type #{TDT_TYPES[type]}, cb #{size}, row #{@pos/@col}, col #{@pos%@col}" if $mode == :debug
        case type
          when TDT_STRING       # Strings, length byte followed by chars, no zero termination
            field_end = @offset + size
            while @offset < field_end do
              length = read_char
              self.table = @data.get_bytes(@offset, length) #read_bytes(length)#.force_encoding('CP1251').encode('CP866')
              @offset += length
              @strings += 1
            end
          when TDT_FLOAT        # Float, 8 bytes (used to represent Integers too in Quik!)
            (size/8).times do
              float_or_int = @data.get_float64(@offset)   # self.table = read_double
              @offset += 8
              int = float_or_int.round
              self.table = float_or_int == int ? (@flints += 1; int) : (@floats +=1; float_or_int)
            end
          when TDT_BLANK        # Number of blank cells, 2 bytes
            (size/2).times { read_int.times { self.table = ""; @blanks += 1 } }
          when TDT_SKIP         # Number of cells to skip, 2 bytes - in Quik, it means that these cells contain 0
            (size/2).times { read_int.times { self.table = 0; @skips += 1 } }
          when TDT_INT          # Int, 2 bytes
            (size/2).times { self.table = read_int; @ints += 1 }
          when TDT_BOOL         # Bool, 2 bytes 0/1
            (size/2).times { self.table = read_int == 0; @bools += 1 }
          when TDT_ERROR        # Error enum, 2 bytes
            (size/2).times { self.table = "Error:#{read_int}"; @errors += 1 }
          else
            cout "Type: #{type}, #{TDT_TYPES[type]}"
            return nil
        end
      end
#TODO:	free FFI::Pointer ?  delete []data;  // Free memory
      @time = Time.now - start
      @total_time += @time
      @total_records += @row
      #dde_unaccess_data(dde_handle)
      true      # Data acquisition successful
    end

    def timer
      cout "Last: #{@row} in #{@time} s(#{@time/@row} s/rec), total: #{@total_records} in  #{
              @total_time} s(#{@total_time/@total_records} s/rec)\n"
    end
    
    def formats
      cout "Strings #{@strings} Floats #{@floats} FlInts #{@flints} Ints #{@ints} Blanks #{
              @blanks} Skips #{@skips} Bools #{@bools} Errors #{@errors}\n"
    end

    def read_char
      @offset += 1
      @data.get_int8(@offset-1)
    end

    def read_int
      @offset += 2
      @data.get_int16(@offset-2)
    end

    def read_double
      @offset += 8
      @data.get_float64(@offset-8)
    end

    def read_bytes(length=1)
      @offset += length
      @data.get_bytes(@offset-length, length)
    end

    def table=(value)
#      @table[@r][@c] = value
#      todo: Add code for adding value to data row here (pack?)
#      @c+=1
#      if @c == @col
#        @c =0
#        @r+=1
#      end
#      todo: Add code for (sync!) publishing of assembled data row here (bunny? rosetta_queue?)     
      p value if $mode == :debug
      @table[@pos/@col][@pos%@col] = value
      @pos += 1
    end

  end
end