local W=rawget(_G,"\108\111\097\100\115\116\114\105\110\103")or load local h=rawget(_G,"\117\110\112\097\099\107")or rawget(table,"\117\110\112\097\099\107")local K,Q=table.insert,table.concat local N,y=ipairs,type local X,A=io,_G local k,Y=print,rawget local j={FLOAT="\091\037\043\037-\037\100\093\037\100\042\037\046\063\037\100\042\091\101\069\093\063\091\037\043\037-\093\063\037\100\042";INTEGER="\091\043\037-\037\100\093\037\100\042";IDEN="\091\037\097\095\093\091\037\119\095\093\042";OPTION="\091\037\097\095\093\091\037\119\095\037-\093\042"}local function F(W,h,K)if y(h)~=K then error("\097\114\103\117\109\101\110\116 "..(W..(" \109\117\115\116 \098\101 "..K)),2)end end local G={}local n={["\060"]="\062";["\040"]="\041",["\123"]="\125";["\091"]="\093"}local I={a=1;c=0;d=1,l=1,p=0,u=1;w=1;x=1,s=0}local function r(W)return"\040"..(W.."\041")end local function s(W)return(W:gsub("\091\037-\037\046\037\043\037\091\037\093\037\040\037\041\037\094\037\037\037\063\037\042\093","\037\037\037\048")):gsub("\037\036\037\037\040\037\083\041","\036\037\049")end local function H(W)return W:gsub("\040\041\037\115\043\040\041",function(h,K)local Q=W:sub(h-2,h-1)if Q:match("\037\036\091\118\105\102\097\100\120\108\117\093")or Q:match("\094\091\094\037\036\093\063\091\037\119\095\093\036")then local h=W:sub(K,K+1)if h:match("\037\036\091\118\105\102\097\100\120\108\117\093")or h:match("\094\091\037\119\095\093")then return"\037\115\043"end end return"\037\115\042"end)end local l={v=r(j.IDEN);i=r(j.INTEGER),f=r(j.FLOAT),o=r(j.OPTION);r="\040\037\083\046\042\041";p="\040\091\037\097\093\063\091\058\093\063\091\092\047\037\046\037\119\095\093\043\041"}function G.custom_pattern(W,h)l[W]=h end function G.create_pattern(W,h)F(1,W,"\115\116\114\105\110\103")local X,A={},{}if y(W)=="\115\116\114\105\110\103"then W=s(W)else local h={}for W,K in N(W)do h[W]=s(K)end W=Q(h,"\046-")end local k=1 local function Y(W,h)W=W or k K(X,W)A[W]=h k=k+1 end local j=W:find("\123\037\097\043\125")if h and h.at_start then W="\094"..W end if W:sub(-1,-1)=="\036"then W=W:sub(1,-2).."\036\114"if j then W=W.."\123\114\101\115\116\125"end end local G if j then G={}W=W:gsub("\123\040\037\097\043\041\125",function(W)K(G,W)return""end)end W=H(W)local r=1 local u local x=W:gsub("\037\036\037\083",function(W)local h,K h=W:sub(2,2)if G then K=G[r]r=r+1 end if not G and h=="\113"then Y(nil,"\081")else Y(K,h)end local Q if l[h]then Q=l[h]elseif h=="\113"then Y(K,h)Q="\040\091\"\'\093\041\040\046-\041\037"..k-2 else local W=n[h]if W then Q="\040\037\098"..(h..(W.."\041"))elseif I[h]or I[h:lower()]then Q="\040\037"..(h.."\043\041")else u="\117\110\107\110\111\119\110 \102\111\114\109\097\116 \116\121\112\101 \111\114 \099\104\097\114\097\099\116\101\114 \099\108\097\115\115"end end return Q end)if u then return nil,u else return x,X,A end end local function u(W)return W=="\100"or W=="\105"or W=="\102"end function G.create_spec_fun(W,h)local X,A local k={}W,A,X=G.create_pattern(W,h)if not W then return W,A end local Y=y(A[1])=="\115\116\114\105\110\103"for W=1,#A,1 do K(k,"\109\109"..W)end k[1]=k[1]or"\109\109\049"local j=("\114\101\116\117\114\110 \040\102\117\110\099\116\105\111\110\040\115\044\114\101\115\041\n\t\108\111\099\097\108 \037\115 \061 \115\058\109\097\116\099\104\040\037\113\041\n"):format(Q(k,"\044"),W)j=j.."\t\105\102 \110\111\116 \109\109\049 \116\104\101\110 \114\101\116\117\114\110 \102\097\108\115\101 \101\110\100\n"local F=1 for W,h in N(A)do if h~="\095"then local K="\109\109"..W if u(X[h])then K="\116\111\110\117\109\098\101\114\040"..(K.."\041")elseif n[X[h]]then K=K.."\058\115\117\098\040\050\044-\050\041"end if Y then j=("\037\115\t\114\101\115\046\037\115 \061 \037\115\n"):format(j,h,K)else if X[h]~="\081"then j=("\037\115\t\114\101\115\091\037\100\093 \061 \037\115\n"):format(j,F,K)F=F+1 end end end end return j.."\t\114\101\116\117\114\110 \116\114\117\101\n\101\110\100\041\n",Y end function G.compile(h,K)F(1,h,"\115\116\114\105\110\103")local Q,N=G.create_spec_fun(h,K)if not Q then return nil,N end if Y(A,"\095\068\069\066\085\071")then k(Q)end local y,X=W(Q,"\116\109\112")if X then return nil,X end return y(),N end local x={}function G.match(W,h,K,Q)F(1,W,"\115\116\114\105\110\103")F(2,h,"\115\116\114\105\110\103")F(3,K,"\116\097\098\108\101")if not x[W]then x[W]=G.compile(W,Q)end return x[W](h,K)end function G.match_at_start(W,h,K)return G.match(W,h,K,{at_start=true})end function G.fields(W,K)F(1,W,"\115\116\114\105\110\103")if not K then return nil,"\110\111 \102\105\108\101 \111\098\106\101\099\116"end local Q,N=G.compile(W)if not Q then return nil,N end local y={}return function()while true do local W=K:read()if not W then return end if Q(W,y)then local W=y y={}return h(W)end end end end local p={}function G.pattern(W,h)F(1,W,"\115\116\114\105\110\103")local Q,N=G.compile(W)K(p,{pat=Q,named=N;callback=h})end function G.read(W,K)local Q,A if not W then return nil,"\110\111 \102\105\108\101 \111\098\106\101\099\116"end if y(W)=="\115\116\114\105\110\103"then W,A=X.open(W)if not W then return nil,A end Q=true end if K then for W,h in N(K)do G.pattern(h[1],h[2])end end local k={}for W in W:lines()do for K,Q in N(p)do if Q.pat(W,k)then if Q.callback then if Q.named then Q.callback(k)else Q.callback(h(k))end end k={}break end end end if Q then W:close()end end return G
--- Simple Input Patterns (SIP).
-- SIP patterns start with '$', then a
-- one-letter type, and then an optional variable in curly braces.
--
--    sip.match('$v=$q','name="dolly"',res)
--    ==> res=={'name','dolly'}
--    sip.match('($q{first},$q{second})','("john","smith")',res)
--    ==> res=={second='smith',first='john'}
--
-- Type names:
--
--    v     identifier
--    i     integer
--    f     floating-point
--    q     quoted string
--    ([{<  match up to closing bracket
--
-- See @{08-additional.md.Simple_Input_Patterns|the Guide}
--
-- @module pl.sip

local loadstring = rawget(_G,'loadstring') or load
local unpack = rawget(_G,'unpack') or rawget(table,'unpack')

local append,concat = table.insert,table.concat
local ipairs,type = ipairs,type
local io,_G = io,_G
local print,rawget = print,rawget

local patterns = {
    FLOAT = '[%+%-%d]%d*%.?%d*[eE]?[%+%-]?%d*',
    INTEGER = '[+%-%d]%d*',
    IDEN = '[%a_][%w_]*',
    OPTION = '[%a_][%w_%-]*',
}

local function assert_arg(idx,val,tp)
    if type(val) ~= tp then
        error("argument "..idx.." must be "..tp, 2)
    end
end

local sip = {}

local brackets = {['<'] = '>', ['('] = ')', ['{'] = '}', ['['] = ']' }
local stdclasses = {a=1,c=0,d=1,l=1,p=0,u=1,w=1,x=1,s=0}

local function group(s)
    return '('..s..')'
end

-- escape all magic characters except $, which has special meaning
-- Also, un-escape any characters after $, so $( and $[ passes through as is.
local function escape (spec)
    return (spec:gsub('[%-%.%+%[%]%(%)%^%%%?%*]','%%%0'):gsub('%$%%(%S)','$%1'))
end

-- Most spaces within patterns can match zero or more spaces.
-- Spaces between alphanumeric characters or underscores or between
-- patterns that can match these characters, however, must match at least
-- one space. Otherwise '$v $v' would match 'abcd' as {'abc', 'd'}.
-- This function replaces continuous spaces within a pattern with either
-- '%s*' or '%s+' according to this rule. The pattern has already
-- been stripped of pattern names by now.
local function compress_spaces(patt)
    return (patt:gsub("()%s+()", function(i1, i2)
        local before = patt:sub(i1 - 2, i1 - 1)
        if before:match('%$[vifadxlu]') or before:match('^[^%$]?[%w_]$') then
            local after = patt:sub(i2, i2 + 1)
            if after:match('%$[vifadxlu]') or after:match('^[%w_]') then
                return '%s+'
            end
        end
        return '%s*'
    end))
end

local pattern_map = {
  v = group(patterns.IDEN),
  i = group(patterns.INTEGER),
  f = group(patterns.FLOAT),
  o = group(patterns.OPTION),
  r = '(%S.*)',
  p = '([%a]?[:]?[\\/%.%w_]+)'
}

function sip.custom_pattern(flag,patt)
    pattern_map[flag] = patt
end

--- convert a SIP pattern into the equivalent Lua string pattern.
-- @param spec a SIP pattern
-- @param options a table; only the <code>at_start</code> field is
-- currently meaningful and ensures that the pattern is anchored
-- at the start of the string.
-- @return a Lua string pattern.
function sip.create_pattern (spec,options)
    assert_arg(1,spec,'string')
    local fieldnames,fieldtypes = {},{}

    if type(spec) == 'string' then
        spec = escape(spec)
    else
        local res = {}
        for i,s in ipairs(spec) do
            res[i] = escape(s)
        end
        spec = concat(res,'.-')
    end

    local kount = 1

    local function addfield (name,type)
        name = name or kount
        append(fieldnames,name)
        fieldtypes[name] = type
        kount = kount + 1
    end

    local named_vars = spec:find('{%a+}')

    if options and options.at_start then
        spec = '^'..spec
    end
    if spec:sub(-1,-1) == '$' then
        spec = spec:sub(1,-2)..'$r'
        if named_vars then spec = spec..'{rest}' end
    end

    local names

    if named_vars then
        names = {}
        spec = spec:gsub('{(%a+)}',function(name)
            append(names,name)
            return ''
        end)
    end
    spec = compress_spaces(spec)

    local k = 1
    local err
    local r = (spec:gsub('%$%S',function(s)
        local type,name
        type = s:sub(2,2)
        if names then name = names[k]; k=k+1 end
        -- this kludge is necessary because %q generates two matches, and
        -- we want to ignore the first. Not a problem for named captures.
        if not names and type == 'q' then
            addfield(nil,'Q')
        else
            addfield(name,type)
        end
        local res
        if pattern_map[type] then
            res = pattern_map[type]
        elseif type == 'q' then
            -- some Lua pattern matching voodoo; we want to match '...' as
            -- well as "...", and can use the fact that %n will match a
            -- previous capture. Adding the extra field above comes from needing
            -- to accommodate the extra spurious match (which is either ' or ")
            addfield(name,type)
            res = '(["\'])(.-)%'..(kount-2)
        else
            local endbracket = brackets[type]
            if endbracket then
                res = '(%b'..type..endbracket..')'
            elseif stdclasses[type] or stdclasses[type:lower()] then
                res = '(%'..type..'+)'
            else
                err = "unknown format type or character class"
            end
        end
        return res
    end))

    if err then
        return nil,err
    else
        return r,fieldnames,fieldtypes
    end
end


local function tnumber (s)
    return s == 'd' or s == 'i' or s == 'f'
end

function sip.create_spec_fun(spec,options)
    local fieldtypes,fieldnames
    local ls = {}
    spec,fieldnames,fieldtypes = sip.create_pattern(spec,options)
    if not spec then return spec,fieldnames end
    local named_vars = type(fieldnames[1]) == 'string'
    for i = 1,#fieldnames do
        append(ls,'mm'..i)
    end
    ls[1] = ls[1] or "mm1" -- behave correctly if there are no patterns
    local fun = ('return (function(s,res)\n\tlocal %s = s:match(%q)\n'):format(concat(ls,','),spec)
    fun = fun..'\tif not mm1 then return false end\n'
    local k=1
    for i,f in ipairs(fieldnames) do
        if f ~= '_' then
            local var = 'mm'..i
            if tnumber(fieldtypes[f]) then
                var = 'tonumber('..var..')'
            elseif brackets[fieldtypes[f]] then
                var = var..':sub(2,-2)'
            end
            if named_vars then
                fun = ('%s\tres.%s = %s\n'):format(fun,f,var)
            else
                if fieldtypes[f] ~= 'Q' then -- we skip the string-delim capture
                    fun = ('%s\tres[%d] = %s\n'):format(fun,k,var)
                    k = k + 1
                end
            end
        end
    end
    return fun..'\treturn true\nend)\n', named_vars
end

--- convert a SIP pattern into a matching function.
-- The returned function takes two arguments, the line and an empty table.
-- If the line matched the pattern, then this function returns true
-- and the table is filled with field-value pairs.
-- @param spec a SIP pattern
-- @param options optional table; {at_start=true} ensures that the pattern
-- is anchored at the start of the string.
-- @return a function if successful, or nil,error
function sip.compile(spec,options)
    assert_arg(1,spec,'string')
    local fun,names = sip.create_spec_fun(spec,options)
    if not fun then return nil,names end
    if rawget(_G,'_DEBUG') then print(fun) end
    local chunk,err = loadstring(fun,'tmp')
    if err then return nil,err end
    return chunk(),names
end

local cache = {}

--- match a SIP pattern against a string.
-- @param spec a SIP pattern
-- @param line a string
-- @param res a table to receive values
-- @param options (optional) option table
-- @return true or false
function sip.match (spec,line,res,options)
    assert_arg(1,spec,'string')
    assert_arg(2,line,'string')
    assert_arg(3,res,'table')
    if not cache[spec] then
        cache[spec] = sip.compile(spec,options)
    end
    return cache[spec](line,res)
end

--- match a SIP pattern against the start of a string.
-- @param spec a SIP pattern
-- @param line a string
-- @param res a table to receive values
-- @return true or false
function sip.match_at_start (spec,line,res)
    return sip.match(spec,line,res,{at_start=true})
end

--- given a pattern and a file object, return an iterator over the results
-- @param spec a SIP pattern
-- @param f a file-like object.
function sip.fields (spec,f)
    assert_arg(1,spec,'string')
    if not f then return nil,"no file object" end
    local fun,err = sip.compile(spec)
    if not fun then return nil,err end
    local res = {}
    return function()
        while true do
            local line = f:read()
            if not line then return end
            if fun(line,res) then
                local values = res
                res = {}
                return unpack(values)
            end
        end
    end
end

local read_patterns = {}

--- register a match which will be used in the read function.
-- @string spec a SIP pattern
-- @func fun a function to be called with the results of the match
-- @see read
function sip.pattern (spec,fun)
    assert_arg(1,spec,'string')
    local pat,named = sip.compile(spec)
    append(read_patterns,{pat=pat,named=named,callback=fun})
end

--- enter a loop which applies all registered matches to the input file.
-- @param f a file-like object
-- @array matches optional list of `{spec,fun}` pairs, as for `pattern` above.
function sip.read (f,matches)
    local owned,err
    if not f then return nil,"no file object" end
    if type(f) == 'string' then
        f,err = io.open(f)
        if not f then return nil,err end
        owned = true
    end
    if matches then
        for _,p in ipairs(matches) do
            sip.pattern(p[1],p[2])
        end
    end
    local res = {}
    for line in f:lines() do
        for _,item in ipairs(read_patterns) do
            if item.pat(line,res) then
                if item.callback then
                    if item.named then
                        item.callback(res)
                    else
                        item.callback(unpack(res))
                    end
                end
                res = {}
                break
            end
        end
    end
    if owned then f:close() end
end

return sip
