require 'matrix'

module CompLearn
  class UNCD
    NCDCOMMAND="ncd -S"
    def self.cp()
      unless defined? @@cp
        begin
          pipe = IO.popen(NCDCOMMAND, "r+")
        rescue
          raise "Cannot execute #{NCDCOMMAND}, (please download from http://complearn.org/ ), exitting"
          exit 1
        end
        @@cp = CommunicationProtocol.new(pipe, pipe)
        @@cp.runcmd(:compressor_list)
      end
      @@cp
    end
  end
  class CommunicationProtocol
    class PositionIndicator
      def initialize(inppkt)
        @pkt = inppkt
        @offset = 0
      end
      def eatchar(howmany=1)
        result = @pkt[@offset, howmany]
        @offset += howmany
        raise "Advanced too far in packet." unless result.size == howmany
        result
      end
    end

    def decode_boolean(pi) ((decode_integer(pi) == 0)? false : true) end
    def decode_integer(pi) pi.eatchar(4).unpack("N")[0] end
    def decode_double(pi) decode_string(pi).to_f end
    def decode_vector(pi) len = decode_integer(pi)
      res = [ ]
      len.times { res << decode_double(pi) }
      Vector[*res]
    end
    def decode_string(pi) unwrap_packet(pi) end
    def decode_pointer(pi) decode_string(pi) end
    def encode_string(inpstr) wrap_packet(inpstr) end
    def encode_boolean(t) encode_integer(t ? 1 : 0) end
    def encode_integer(i) [i].pack("N") end
    def encode_double(i) encode_string(i.to_s) end
    def encode_pointer(i) encode_string(i.to_s) end

    def encode_cdc(cdc) encode_intarray(cdc) end
    def encode_cdcseq(cdcseq) res = [ ]
      res << encode_integer(cdcseq.size)
      cdcseq.each { |i| res << encode_cdc(i) }
      res.join('')
    end
    def encode_intarray(a)
      r = [ ]
      r << encode_integer(a.size)
      a.each { |j| r << encode_integer(j.to_i) }
      r.join('')
    end

    def encode_matspec(ums)
      ms = ums.to_s
      if ms =~ /full/i
        return encode_string('full')
      end
      if ms =~ /bigx/i
        return encode_string('bigx')
      end
      if ms =~ /bigy/i
        return encode_string('bigy')
      end
      raise "Illegal matspec #{ums.inspect}"
    end

    def decode_matrix(pi)
      size1 = decode_integer(pi)
      size2 = decode_integer(pi)
      m = [ ]
      size1.times { |i|
        m << [ ]
        size2.times { |j|
          m[-1] << decode_double(pi)
        }
      }
      Matrix[*m]
    end

    def initialize(inpstream, outstream)
      @inp = inpstream
      @out = outstream
    end

    def runcmd(cmd, params = '')
      raise "illegal command #{cmd}" unless respond_to? "do_response_#{cmd}"
      return nil if @out.closed?
      testpacket = encode_string(cmd.to_s)
      tosend = wrap_packet(testpacket + params)
#      STDERR.puts "Sending command packet: <#{tosend}>"
      begin
        @out.write(tosend)
      rescue Errno::EPIPE
        STDERR.puts "runcmd: compression server died, exitting"
        @out.close
        exit 1
      end
      result = send("do_response_#{cmd.to_s}",PositionIndicator.new(eat_packet))
#      puts "Got result for #{cmd}: #{result.inspect}"
      result
    end
    def wrap_packet(inpstr) [inpstr.size].pack("N") + inpstr end
    def eat_packet()
      pheader = @inp.read(4)
      unless pheader
        STDERR.puts "eat_packet: compression server died, exitting"
        @inp.close
        exit 1
      end
      sz = pheader.unpack("N")[0]
      raise "bad size" unless sz >= 0
      pkt = @inp.read(sz)
      pkt
    end
    def unwrap_packet(pi)
      sz = pi.eatchar(4).unpack("N")[0]
      raise "bad size" unless sz >= 0
      pi.eatchar(sz)
    end
  end
end
require "complearn/cmdresp"
module CompLearn
  class RealCompressor
    private
    def make_finalizer()
      proc { |id| @cp.runcmd(:free_compressor,@cp.encode_pointer(@objhandle)) }
    end
    def make_simplecmd(*args)
      r = [ ]
      sym = args.shift
      r << @cp.encode_pointer(@objhandle)
      args.each { |i|
        if (i.kind_of?(String))
          r << @cp.encode_string(i)
        else
          if (i.kind_of?(Integer) || i.kind_of?(Fixnum))
            r << @cp.encode_integer(i)
          else
            raise "Unkown object #{i.inspect}"
          end
        end
      }
#      puts "Running command: #{sym}(#{r.join(', ')})"
      @cp.runcmd(sym, r.join(''))
    end

    public
    def initialize(compname = nil, objhandle = nil)
      compname = "bzlib" if compname.nil?
   @cp = UNCD.cp
   @objhandle=objhandle||@cp.runcmd(:new_compressor,@cp.encode_string(compname))
      ObjectSpace.define_finalizer(self, make_finalizer)
    end
    def self.list() UNCD.cp.runcmd(:compressor_list) end
    def self.compressor_list() list() end
    def compress(str) make_simplecmd(:compress, str.to_s) end
    def decompress(str) make_simplecmd(:decompress, str.to_s) end
    def blurb() make_simplecmd(:blurb) end
    def canonical_extension() make_simplecmd(:canonical_extension) end
    def name() make_simplecmd(:name) end
    def compressor_version() make_simplecmd(:compressor_version) end
    def binding_version() make_simplecmd(:binding_version) end
    def is_threadsafe() make_simplecmd(:is_threadsafe) end
    def is_decompressible(str) make_simplecmd(:is_decompressible, str.to_s) end
    def is_just_size() make_simplecmd(:is_just_size) end
    def is_operational() make_simplecmd(:is_operational) end
    def is_hash_function() make_simplecmd(:is_hash_function) end
    def hash(str) make_simplecmd(:hash, str.to_s) end
    def is_hash_function() make_simplecmd(:is_hash_function) end
    def window_size() make_simplecmd(:window_size) end
    def compressed_size(str) make_simplecmd(:compressed_size, str.to_s) end
    def clone()
      oh = make_simplecmd(:clone, @objhandle.to_s)
      self.class.new(nil, oh)
    end
    def is_private_property(str)
      make_simplecmd(:is_private_property, str.to_s)
    end
    def driver()
      CompressorDriver.new(self)
    end
    def oh() @objhandle end
  end
  class CompressorDriver
    private
    def make_finalizer()
      proc { |id| @cp.runcmd(:free_compressordriver,@cp.encode_pointer(@objhandle)) }
    end
    public
    def initialize(rc)
      @cp = UNCD.cp
      @objhandle = @cp.runcmd(:new_compressordriver,@cp.encode_pointer(rc.oh))
      ObjectSpace.define_finalizer(self, make_finalizer)
    end
    def make_simplecmd(*args)
      r = [ ]
      sym = ("cd_" + args.shift.to_s).to_sym
      r << @cp.encode_pointer(@objhandle)
      args.each { |i|
        if (i.kind_of?(String))
          r << @cp.encode_string(i)
        else
          if (i.kind_of?(Integer) || i.kind_of?(Fixnum))
            r << @cp.encode_integer(i)
          else
            raise "Unkown object #{i.inspect}"
          end
        end
      }
#      puts "Running command: #{sym}(#{r.join(', ')})"
      @cp.runcmd(sym, r.join(''))
    end
    def store(str) make_simplecmd(:store, str.to_s) end
    def size() make_simplecmd(:size) end
    def compression_vector() make_simplecmd(:compression_vector) end
    def compress_single(i) make_simplecmd(:compress_single, i.to_i) end
    def compress_pair(i,j) make_simplecmd(:compress_pair, i.to_i,j.to_i) end
    def compression_matrix(dim1ind, dim2ind, matspec = :full)
      r = [ ]
      r << @cp.encode_pointer(@objhandle)
      r << @cp.encode_intarray(dim1ind)
      r << @cp.encode_intarray(dim2ind)
      r << @cp.encode_matspec(matspec)
      @cp.runcmd(:cd_compression_matrix, r.join(''))
    end
    def compression_sequence(compseq)
      r = [ ]
      r << @cp.encode_pointer(@objhandle)
      r << @cp.encode_cdcseq(compseq)
      @cp.runcmd(:cd_compression_sequence, r.join(''))
    end
  end
end
