local process = require("process")
local shell = require("shell")
local text = require("text")
local tx = require("transforms")

local sh = {}
sh.internal = {}

function sh.internal.isWordOf(w, vs)
 return w and #w == 1 and not w[1].qr and tx.first(vs,{{w[1].txt}}) ~= nil
end

local isWordOf = sh.internal.isWordOf

-------------------------------------------------------------------------------

--SH API

sh.internal.ec = {}
sh.internal.ec.parseCommand = 127
sh.internal.ec.last = 0

function sh.getLastExitCode()
  return sh.internal.ec.last
end

function sh.internal.command_result_as_code(ec, reason)
  -- convert lua result to bash ec
  local code
  if ec == false then
    code = 1
  elseif ec == nil or ec == true then
    code = 0
  elseif type(ec) ~= "number" then
    code = 2 -- illegal number
  else
    code = ec
  end

  if reason and code ~= 0 then io.stderr:write(reason, "\n") end
  return code
end

function sh.internal.resolveActions(input, resolved)
  resolved = resolved or {}

  local processed = {}

  local prev_was_delim = true
  local words, reason = text.internal.tokenize(input)

  if not words then
    return nil, reason
  end

  while #words > 0 do
    local next = table.remove(words,1)
    if isWordOf(next, {";","&&","||","|"}) then
      prev_was_delim = true
      resolved = {}
    elseif prev_was_delim then
      prev_was_delim = false
      -- if current is actionable, resolve, else pop until delim
      if next and #next == 1 and not next[1].qr then
        local key = next[1].txt
        if key == "!" then
          prev_was_delim = true -- special redo
        elseif not resolved[key] then
          resolved[key] = shell.getAlias(key)
          local value = resolved[key]
          if value and key ~= value then
            local replacement_tokens, resolve_reason = sh.internal.resolveActions(value, resolved)
            if not replacement_tokens then
              return replacement_tokens, resolve_reason
            end
            words = tx.concat(replacement_tokens, words)
            next = table.remove(words,1)
          end
        end
      end
    end

    table.insert(processed, next)
  end

  return processed
end

-- returns true if key is a string that represents a valid command line identifier
function sh.internal.isIdentifier(key)
  if type(key) ~= "string" then
    return false
  end

  return key:match("^[%a_][%w_]*$") == key
end

-- expand (interpret) a single quoted area
-- examples: $foo or "$foo"
function sh.expand(value)
  local expanded = value
  :gsub("%$([_%w%?]+)", function(key)
    if key == "?" then
      return tostring(sh.getLastExitCode())
    end
    return os.getenv(key) or ''
  end)
  :gsub("%${(.*)}", function(key)
    if sh.internal.isIdentifier(key) then
      return os.getenv(key) or ''
    end
    io.stderr:write("${" .. key .. "}: bad substitution\n")
    os.exit(1)
  end)
  return expanded
end

function sh.internal.createThreads(commands, env, start_args)
  -- Piping data between programs works like so:
  -- program1 gets its output replaced with our custom stream.
  -- program2 gets its input replaced with our custom stream.
  -- repeat for all programs
  -- custom stream triggers execution of "next" program after write.
  -- custom stream triggers yield before read if buffer is empty.
  -- custom stream may have "redirect" entries for fallback/duplication.
  local threads = {}
  for i = 1, #commands do
    local command = commands[i]
    local program, args, redirects = table.unpack(command)
    local name = tostring(program)
    local thread_env = type(program) == "string" and env or nil
    local thread, reason = process.load(program or "/dev/null", thread_env, function(...)
      if redirects then
        sh.internal.openCommandRedirects(redirects)
      end

      args = tx.concat(args, start_args[i] or {}, table.pack(...))

      -- popen expects each process to first write an empty string
      -- this is required for proper thread order
      io.write("")
      return table.unpack(args, 1, args.n or #args)
    end, name)

    if not thread then
      for _,t in ipairs(threads) do
        process.internal.close(t)
      end
      return nil, reason
    end

    threads[i] = thread

  end

  if #threads > 1 then
    require("pipe").buildPipeChain(threads)
  end

  return threads
end

function sh.internal.executePipes(pipe_parts, eargs, env)
  local commands = {}
  for _,words in ipairs(pipe_parts) do
    local args = {}
    local reparse
    for _,word in ipairs(words) do
      local value = ""
      for _,part in ipairs(word) do
        reparse = reparse or part.qr or part.txt:find("[%$%*%?<>]")
        value = value .. part.txt
      end
      args[#args + 1] = value
    end

    local redirects
    if reparse then
      args, redirects = sh.internal.evaluate(words)
      if not args then
        return false, redirects -- in this failure case, redirects has the error message
      end
    end

    commands[#commands + 1] = table.pack(table.remove(args, 1), args, redirects)
  end

  local threads, reason = sh.internal.createThreads(commands, env, {[#commands]=eargs})
  if not threads then return false, reason end
  return process.internal.continue(threads[1])
end

function sh.execute(env, command, ...)
  checkArg(2, command, "string")
  if command:find("^%s*#") then return true, 0 end

  local words, reason = sh.internal.resolveActions(command)
  if type(words) ~= "table" then
    return words, reason
  elseif #words == 0 then
    return true
  end

  -- MUST be table.pack for non contiguous ...
  local eargs = table.pack(...)

  -- simple
  if not command:find("[;%$&|!<>]") then
    sh.internal.ec.last = sh.internal.command_result_as_code(sh.internal.executePipes({words}, eargs, env))
    return sh.internal.ec.last == 0
  end

  return sh.internal.execute_complex(words, eargs, env)
end

function sh.hintHandler(full_line, cursor)
  return sh.internal.hintHandlerImpl(full_line, cursor)
end

require("package").delay(sh, "/lib/core/full_sh.lua")

return sh
