control_port = arg[1] or CONTROL_PORT or '\\\\.\\CNCB1'
data_port    = arg[2] or DATA_PORT or '\\\\.\\CNCB0'

rs232  = require "rs232.core"
out    = io.stderr
ztimer = require "lzmq.timer"

started = ztimer.monotonic():start()

monotonic = ztimer.monotonic()

local function is_timed_out(elapsed, timeout)
  if elapsed >= timeout then return true end
  if (timeout - elapsed) < 100 then return true end
  return false
end

local function open_port(name)
  local e, p = rs232.open(name)
  if e ~= rs232.RS232_ERR_NOERROR then
    -- handle error
    out:write(string.format("can't open serial port '%s', error: '%s'\n",
        name, rs232.error_tostring(e)))
    os.exit(-1)
  end

  -- set port settings
  assert(p:set_baud_rate(rs232.RS232_BAUD_115200) == rs232.RS232_ERR_NOERROR)
  assert(p:set_data_bits(rs232.RS232_DATA_8)      == rs232.RS232_ERR_NOERROR)
  assert(p:set_parity(rs232.RS232_PARITY_NONE)    == rs232.RS232_ERR_NOERROR)
  assert(p:set_stop_bits(rs232.RS232_STOP_1)      == rs232.RS232_ERR_NOERROR)
  assert(p:set_flow_control(rs232.RS232_FLOW_OFF) == rs232.RS232_ERR_NOERROR)
  -- assert(p:set_rts(rs232.RS232_RTS_ON)            == rs232.RS232_ERR_NOERROR)
  print("SET RTS", p:set_rts(rs232.RS232_RTS_ON))

  out:write(string.format("OK, port open with values '%s'\n", tostring(p)))
  return p
end

local sep = '\n'

function printf(...)
  io.stderr:write(string.format(...))
end

function pass(...)
  printf(...) 
  io.stderr:write("\n")
end

function fail(...)
  io.stderr:write("ERROR: ")
  printf(...)
  io.stderr:write("!\n")
  os.exit(-1)
end

function warn(...)
  local s = string.format(...)
  io.stderr:write("WARNING: ", s, "\n")
end

function test(test)
  io.stderr:write(
      "----------------------------------------------\n",
      "testing: ", test, "\n",
      "----------------------------------------------\n"
  )
end

function remote(...)
  local s = string.format(...)
  s = string.gsub(s, "\n", ";")
  s = string.gsub(s, "%s+", " ")
  s = string.gsub(s, "^%s*", "")
  control:write(s .. sep)
  local e, d = control:read(1, 30000)
  assert(e == rs232.RS232_ERR_NOERROR, rs232.error_tostring(e))
  assert(d == sep)
end

function reconnect()
  remote[[
    data:in_queue_clear()
    while true do e, d = data:read(1024, 100)
      if e ~= rs232.RS232_ERR_NOERROR then break end
      if not d or #d == 0 then break end
    end
  ]]
  data:in_queue_clear()
  while true do e, d = data:read(1024, 100)
    if e ~= rs232.RS232_ERR_NOERROR then break end
    if not d or #d == 0 then break end
  end
end

control = open_port(control_port)
data    = open_port(data_port)

local function test_echo(len)
  reconnect()
  printf("%d bytes: ", len)

  local s = ('a'):rep(len)

  remote([[
    e, s = data:read(%d, 5000, 1)
    if s then data:write(s) end
  ]], len)

  e, written = data:write(s)
  if e ~= rs232.RS232_ERR_NOERROR then fail(rs232.error_tostring(e)) end
  if written ~= len then warn("written to low data %d/%d", len, written) end

  e, s1 = data:read(len, 5000, true)
  if e ~= rs232.RS232_ERR_NOERROR then fail(rs232.error_tostring(e)) end

  if s1 ~= s then fail('Data not matched: %d/%s', len, #s1) end

  pass('ok')
end

local function test_read_timeout_forced(len, tm, sl)
  reconnect()

  printf("%d bytes, %dms total timeout, %dms pause: ", len, tm, sl)

  remote(string.format ([[
      str = string.rep('a', %d)
      data:write(str)
      print('server: sleeping for %dms')
      ztimer.sleep(%d)
      print('server: woke up')
      data:write(str)
  ]], len, sl, sl))

  monotonic:start()

  e, d = data:read(2*len, tm, true)

  elapsed = monotonic:stop()

  alldone = d and (#d == 2 * len)

  if e == rs232.RS232_ERR_TIMEOUT then
    if is_timed_out(elapsed, tm) then
      pass('ok')
    else
      fail("should have timed out")
    end
  elseif e == rs232.RS232_ERR_NOERROR then
    if alldone then
      pass("proper timeout")
    elseif is_timed_out(elapsed, tm) then
      pass("ok")
    else
      fail("should have timed out")
    end
  else
    if alldone then
      fail("unexpected error '%s'", rs232.error_tostring(e))
    else
      fail(rs232.error_tostring(e))
    end
  end
end

local function test_read_some(len, tm, sl)
  reconnect()

  printf("%d bytes, %dms total timeout, %dms pause: ", len, tm, sl)

  remote(string.format ([[
      str = string.rep('a', %d)
      print('server: sleeping for %dms')
      ztimer.sleep(%d)
      print('server: woke up')
      data:write(str)
  ]], len, sl, sl))

  monotonic:start()

  e, d = data:read(len, tm)

  elapsed = monotonic:stop()

  if e == rs232.RS232_ERR_TIMEOUT then
    if is_timed_out(elapsed, tm) or (d and #d > 0) then
      pass('ok')
    else
      fail("should have timed out")
    end
  elseif e == rs232.RS232_ERR_NOERROR then
    if (not d) or (#d == 0) then
      fail("should have timed out")
    elseif d or is_timed_out(elapsed, tm) then
      pass("ok")
    else
      fail("should have timed out 1")
    end
  else
    if alldone then
      fail("unexpected error '%s'", rs232.error_tostring(e))
    else
      fail(rs232.error_tostring(e))
    end
  end

end

local function test_read_all(len, sl)
  reconnect()

  printf("%d bytes, %dms pause: ", len, sl)

  remote(string.format ([[
      str = string.rep('a', %d)
      print('server: sleeping for %dms')
      ztimer.sleep(%d)
      data:write(str)
      print('server: sleeping for %dms')
      ztimer.sleep(%d)
      print('server: woke up')
      data:write(str)
  ]], len, sl, sl, sl, sl))

  monotonic:start()

  e, d = data:read(len * 2)

  elapsed = monotonic:stop()

  if e ~= rs232.RS232_ERR_NOERROR then fail(rs232.error_tostring(e)) end
  if not d or #d == 0  then fail("no data")
  elseif #d > len then warning("wait too long %d, readed %d", elapsed, #d)
  else pass('ok') end

end

local function test_queue_in(len)
  reconnect()
  printf("%d bytes: ", len)

  e, l = data:in_queue()
  if e ~= rs232.RS232_ERR_NOERROR then fail(rs232.error_tostring(e)) end
  if l ~= 0 then fail('should be emty') end

  s = string.rep('a', len)

  remote([[
    s = ('a'):rep(%d)
    data:write(s)
  ]], len)

  ztimer.sleep(2000)

  e, l = data:in_queue()
  if e ~= rs232.RS232_ERR_NOERROR then fail(rs232.error_tostring(e)) end
  if l ~= len then fail('expected %d but got %d', len, l) end

  e = data:in_queue_clear()
  if e ~= rs232.RS232_ERR_NOERROR then fail('clear fail %s', rs232.error_tostring(e)) end

  e, l = data:in_queue()
  if e ~= rs232.RS232_ERR_NOERROR then fail(rs232.error_tostring(e)) end
  if l ~= 0 then warn('clear fail %d', l)
  else pass('ok') end
end


test"echo"
test_echo(128)
test_echo(256)
test_echo(1024)

test"input queue"
test_queue_in(16)
test_queue_in(128)
test_queue_in(256)
test_queue_in(1024)

test"read timeout forced"
test_read_timeout_forced(1024, 2000, 3000)
test_read_timeout_forced(1024, 3000, 2000)
test_read_timeout_forced(2048, 2000, 3000)
test_read_timeout_forced(2048, 3000, 2000)

test"read some"
test_read_some(1024, 2000, 3000)
test_read_some(1024, 3000, 2000)

test"read all"
test_read_all(64, 2000)
test_read_all(128, 2000)
test_read_all(512, 2000)
test_read_all(1024, 2000)

test("shutting server down")
reconnect()
remote("os.exit()")
pass('ok')

test(string.format("done in %.2fs", started:stop()/1000))
