-----------------------------------------------------------------------------
-- LTN12 - Filters, sources, sinks and pumps.
-- LuaSocket toolkit.
-- Author: Diego Nehab
-----------------------------------------------------------------------------

-----------------------------------------------------------------------------
-- Declare module
-----------------------------------------------------------------------------
local string = require("string")
local table = require("table")
local base = _G
local _M = {}
if module then -- heuristic for exporting a global package table
  --ltn12 = _M
end
local filter,source,sink,pump = {},{},{},{}

_M.filter = filter
_M.source = source
_M.sink = sink
_M.pump = pump

-- 2048 seems to be better in windows...
_M.BLOCKSIZE = 2048
_M._VERSION = "LTN12 1.0.3"

-----------------------------------------------------------------------------
-- Filter stuff
-----------------------------------------------------------------------------
-- returns a high level filter that cycles a low-level filter
function filter.cycle(low, ctx, extra)
  base.assert(low)
  return function(chunk)
    local ret
    ret, ctx = low(ctx, chunk, extra)
    return ret
  end
end

-- chains a bunch of filters together
-- (thanks to Wim Couwenberg)
function filter.chain(...)
  local arg = {...}
  local n = select('#',...)
  local top, index = 1, 1
  local retry = ""
  return function(chunk)
    retry = chunk and retry
    while true do
      if index == top then
        chunk = arg[index](chunk)
        if chunk == "" or top == n then return chunk
         elseif chunk then index = index + 1
         else
          top = top+1
          index = top
        end
       else
        chunk = arg[index](chunk or "")
        if chunk == "" then
          index = index - 1
          chunk = retry
         elseif chunk then
          if index == n then return chunk
           else index = index + 1 end
         else base.error("filter returned inappropriate nil") end
      end
    end
  end
end

-----------------------------------------------------------------------------
-- Source stuff
-----------------------------------------------------------------------------
-- create an empty source
local function empty()
  return nil
end

function source.empty()
  return empty
end

-- returns a source that just outputs an error
function source.error(err)
  return function()
    return nil, err
  end
end

-- creates a file source
function source.file(handle, io_err)
  if handle then
    return function()
      local chunk = handle:read(_M.BLOCKSIZE)
      if not chunk then handle:close() end
      return chunk
    end
   else return source.error(io_err or "unable to open file") end
end

-- turns a fancy source into a simple source
function source.simplify(src)
  base.assert(src)
  return function()
    local chunk, err_or_new = src()
    src = err_or_new or src
    if not chunk then return nil, err_or_new
     else return chunk end
  end
end

-- creates string source
function source.string(s)
  if s then
    local i = 1
    return function()
      local chunk = string.sub(s, i, i+_M.BLOCKSIZE-1)
      i = i + _M.BLOCKSIZE
      if chunk ~= "" then return chunk
       else return nil end
    end
   else return source.empty() end
end

-- creates rewindable source
function source.rewind(src)
  base.assert(src)
  local t = {}
  return function(chunk)
    if not chunk then
      chunk = table.remove(t)
      if not chunk then return src()
       else return chunk end
     else
      table.insert(t, chunk)
    end
  end
end

-- chains a source with one or several filter(s)
function source.chain(src, f, ...)
  if ... then f=filter.chain(f, ...) end
  base.assert(src and f)
  local last_in, last_out = "", ""
  local state = "feeding"
  local err
  return function()
    if not last_out then
      base.error('source is empty!', 2)
    end
    while true do
      if state == "feeding" then
        last_in, err = src()
        if err then return nil, err end
        last_out = f(last_in)
        if not last_out then
          if last_in then
            base.error('filter returned inappropriate nil')
           else
            return nil
          end
         elseif last_out ~= "" then
          state = "eating"
          if last_in then last_in = "" end
          return last_out
        end
       else
        last_out = f(last_in)
        if last_out == "" then
          if last_in == "" then
            state = "feeding"
           else
            base.error('filter returned ""')
          end
         elseif not last_out then
          if last_in then
            base.error('filter returned inappropriate nil')
           else
            return nil
          end
         else
          return last_out
        end
      end
    end
  end
end

-- creates a source that produces contents of several sources, one after the
-- other, as if they were concatenated
-- (thanks to Wim Couwenberg)
function source.cat(...)
  local arg = {...}
  local src = table.remove(arg, 1)
  return function()
    while src do
      local chunk, err = src()
      if chunk then return chunk end
      if err then return nil, err end
      src = table.remove(arg, 1)
    end
  end
end

-----------------------------------------------------------------------------
-- Sink stuff
-----------------------------------------------------------------------------
-- creates a sink that stores into a table
function sink.table(t)
  t = t or {}
  local f = function(chunk, err)
    if chunk then table.insert(t, chunk) end
    return 1
  end
  return f, t
end

-- turns a fancy sink into a simple sink
function sink.simplify(snk)
  base.assert(snk)
  return function(chunk, err)
    local ret, err_or_new = snk(chunk, err)
    if not ret then return nil, err_or_new end
    snk = err_or_new or snk
    return 1
  end
end

-- creates a file sink
function sink.file(handle, io_err)
  if handle then
    return function(chunk, err)
      if not chunk then
        handle:close()
        return 1
       else return handle:write(chunk) end
    end
   else return sink.error(io_err or "unable to open file") end
end

-- creates a sink that discards data
local function null()
  return 1
end

function sink.null()
  return null
end

-- creates a sink that just returns an error
function sink.error(err)
  return function()
    return nil, err
  end
end

-- chains a sink with one or several filter(s)
function sink.chain(f, snk, ...)
  if ... then
    local args = { f, snk, ... }
    snk = table.remove(args, #args)
    f = filter.chain(unpack(args))
  end
  base.assert(f and snk)
  return function(chunk, err)
    if chunk ~= "" then
      local filtered = f(chunk)
      local done = chunk and ""
      while true do
        local ret, snkerr = snk(filtered, err)
        if not ret then return nil, snkerr end
        if filtered == done then return 1 end
        filtered = f(done)
      end
     else return 1 end
  end
end

-----------------------------------------------------------------------------
-- Pump stuff
-----------------------------------------------------------------------------
-- pumps one chunk from the source to the sink
function pump.step(src, snk)
  local chunk, src_err = src()
  local ret, snk_err = snk(chunk, src_err)
  if chunk and ret then return 1
   else return nil, src_err or snk_err end
end

-- pumps all data from a source to a sink, using a step function
function pump.all(src, snk, step)
  base.assert(src and snk)
  step = step or pump.step
  while true do
    local ret, err = step(src, snk)
    if not ret then
      if err then return nil, err
       else return 1 end
    end
  end
end

return _M
