--
-- Thanks to http://lua-users.org/wiki/PeterOdding
-- examples from http://lua-users.org/wiki/LpegRecipes
--
-- author: Giacomo Mantani

myname = ...            -- Trick to differentiate ------------------------------
if myname == nil then   -- interactive vs batch/testing mode
  myname = "testing"
end
module(myname, package.seeall) -------------------------------------------------

local lpeg = require "lpeg"
-- Basic Patterns
local P, S, V = lpeg.P, lpeg.S, lpeg.V;
-- Captures
local C, Cb, Cc, Cg, Cs, Ct, Cmt =
                  lpeg.C, lpeg.Cb, lpeg.Cc, lpeg.Cg, lpeg.Cs, lpeg.Ct, lpeg.Cmt;

---
-- From lpeg-utility but verbose
--
-- Returns a pattern which matches the literal string caselessly.
--
-- @param literal A literal string to match case-insensitively.
-- @return An LPeg pattern.
local function caseless (literal)
 	function bothcase (a) return lpeg.S(a:lower()..a:upper()) end
  function ffold (a, b) return a * b end
  -- for every char in match's arg call 'bothcase' and fold all results 'ffold'
  local caseless = lpeg.Cf((P(1) / bothcase )^1, ffold )

  return assert(caseless:match(literal))
end

-- wrap lpeg syntax with a more friendly method call
local function tag(open, close)
  return open * C((P(1) - close)^0) * close / 1
end

-- create a pattern which captures the lua value [id] and the input matching
-- [patt] in a table
local function token(id, patt)
  return Ct(Cc(id) * C(patt))
end

local comment = token("comment", tag(P"<!--", P"-->"))
local jscomment_multi = token("jscomment_multi", tag(P"/*", P"*/"))
local title   = token("title", tag(P"<" * caseless("title") * P">", P"</" * caseless("title") * P">"))
local whitespace = token('whitespace', S('\r\n\f\t ')^1)

-- we only need to synchronize the line-counter for these token types
local multiline_tokens = { comment = true, jscomment_multi = true, whitespace = true }

-- ordered choice of all tokens and last-resort error which consumes one character
local all_tokens = whitespace + comment + title + jscomment_multi +
                   token('error', 1)

local table_of_tokens = Ct(all_tokens ^ 0)

-- if you really want to try it out before writing any code :P
local function printt(tokens)
  local printt, format = _G.print, _G.string.format
  for _, token in pairs(tokens) do
    if token[1] ~= "error" and token[1] ~= "whitespace" then
      printt(format('line %i, %s: `%s`', token[3], token[1], token[2]))
    end
  end
  printt(format('total of %i tokens, %i lines', #tokens, tokens[#tokens][3]))
end

-- increment [line] by the number of line-ends in [text]
local function sync(line, text)
  local index, limit = 1, #text
  while index <= limit do
    local start, stop = text:find('\r\n', index, true)
    if not start then
      start, stop = text:find('[\r\n\f]', index)
      if not start then break end
    end
    index = stop + 1
    line = line + 1
  end
  return line
end

-- public interface
if myname ~= "testing" then
  print"[Testing mode]"
  local filename = arg[1]
  local fh = assert(io.open(filename))
  -- [1] Read test file
  local input = fh:read'*a'
  fh:close()
  assert(type(input) == 'string', 'bad argument #1 (expected string)')
  -- Scan all content line by line
  local line = 1
  local tokens = lpeg.match(table_of_tokens, input)
  for i, token in pairs(tokens) do
    token[3] = line
    if multiline_tokens[token[1]] then
      line = sync(line, token[2])
    end
  end
  printt(tokens)
end
