# Copyright (c) 2014-2016 The Hybrid Group

class Serial
  def initialize(address, baude_rate=9600, data_bits=8, parity=:none, stop_bits=1)
    file_opts = RubySerial::Posix::O_RDWR | RubySerial::Posix::O_NOCTTY | RubySerial::Posix::O_NONBLOCK
    @fd = RubySerial::Posix.open(address, file_opts)

    if @fd == -1
      raise RubySerial::Error, RubySerial::Posix::ERROR_CODES[FFI.errno]
    else
      @open = true
    end

    fl = RubySerial::Posix.fcntl(@fd, RubySerial::Posix::F_GETFL, :int, 0)
    if fl == -1
      raise RubySerial::Error, RubySerial::Posix::ERROR_CODES[FFI.errno]
    end

    err = RubySerial::Posix.fcntl(@fd, RubySerial::Posix::F_SETFL, :int, ~RubySerial::Posix::O_NONBLOCK & fl)
    if err == -1
      raise RubySerial::Error, RubySerial::Posix::ERROR_CODES[FFI.errno]
    end

    @config = build_config(baude_rate, data_bits, parity, stop_bits)

    err = RubySerial::Posix.tcsetattr(@fd, RubySerial::Posix::TCSANOW, @config)
    if err == -1
      raise RubySerial::Error, RubySerial::Posix::ERROR_CODES[FFI.errno]
    end
  end

  def closed?
    !@open
  end

  def close
    err = RubySerial::Posix.close(@fd)
    if err == -1
      raise RubySerial::Error, RubySerial::Posix::ERROR_CODES[FFI.errno]
    else
      @open = false
    end
  end

  def write(data)
    data = data.to_s
    n =  0
    while data.size > n do
      buff = FFI::MemoryPointer.from_string(data[n..-1].to_s)
      i = RubySerial::Posix.write(@fd, buff, buff.size-1)
      if i == -1
        raise RubySerial::Error, RubySerial::Posix::ERROR_CODES[FFI.errno]
      else
        n = n+i
      end
    end

    # return number of bytes written
    n
  end

  def read(size)
    buff = FFI::MemoryPointer.new :char, size
    i = RubySerial::Posix.read(@fd, buff, size)
    if i == -1
      raise RubySerial::Error, RubySerial::Posix::ERROR_CODES[FFI.errno]
    end
    buff.get_bytes(0, i)
  end

  def getbyte
    buff = FFI::MemoryPointer.new :char, 1
    i = RubySerial::Posix.read(@fd, buff, 1)
    if i == -1
      raise RubySerial::Error, RubySerial::Posix::ERROR_CODES[FFI.errno]
    end

    if i == 0
      nil
    else
      buff.get_bytes(0,1).bytes.first
    end
  end

  def gets(sep=$/, limit=nil)
    if block_given?
      loop do
        yield(get_until_sep(sep, limit))
      end
    else
      get_until_sep(sep, limit)
    end
  end

  private

  def get_until_sep(sep, limit)
    sep = "\n\n" if sep == ''
    # This allows the method signature to be (sep) or (limit)
    (limit = sep; sep="\n") if sep.is_a? Integer
    bytes = []
    loop do
      current_byte = getbyte
      bytes << current_byte unless current_byte.nil?
      break if (bytes.last(sep.bytes.to_a.size) == sep.bytes.to_a) || ((bytes.size == limit) if limit)
    end

    bytes.map { |e| e.chr }.join
  end

  def build_config(baude_rate, data_bits, parity, stop_bits)
    config = RubySerial::Posix::Termios.new

    config[:c_iflag]  = RubySerial::Posix::IGNPAR
    config[:c_ispeed] = RubySerial::Posix::BAUDE_RATES[baude_rate]
    config[:c_ospeed] = RubySerial::Posix::BAUDE_RATES[baude_rate]
    config[:c_cflag]  = RubySerial::Posix::DATA_BITS[data_bits] |
      RubySerial::Posix::CREAD |
      RubySerial::Posix::CLOCAL |
      RubySerial::Posix::PARITY[parity] |
      RubySerial::Posix::STOPBITS[stop_bits]

    # Masking in baud rate on OS X would corrupt the settings.
    if RubySerial::ON_LINUX
      config[:c_cflag] = config[:c_cflag] | RubySerial::Posix::BAUDE_RATES[baude_rate]
    end

    config[:cc_c][RubySerial::Posix::VMIN] = 0

    config
  end
end
