--
-- Each entry of "syntax" describe a node of the AST tree.
-- The "properties" field gives the specification for the properties
-- of each node.
--
-- Each "properties" entry is of the form:
--
-- <name> = <ast_element_type>
--
-- where <ast_element_type> is a recursive type defined as follow:
-- it can be:
--
-- "Expression",
-- "Statement",
-- ...
-- to indicate a specific kind of "node". Alternatively a node can be
-- specified as;
--
-- { type = "node", kind = "Statement" }
--
-- In addition an <ast_element_type> can be also:
--
-- { type = "literal", value = "string" }
--
-- { type = "enum", values = {"a", "b", "c"} }
--
-- { type = "list", kind = <ast_element_type> }
--
-- { type = "choice", values = {<ast_element_type>, <ast_element_type>, ...} }
--
-- The latter two are defined recursively. A "list" is Lua table of element of a
-- given type. The "choice" allow an element to be either of one type or another.
--

local syntax = {
   Node = {
      kind = "Node",
      abstract = true
   },
   Expression = {
      kind = "Expression",
      base = "Node",
      abstract = true,
   },
   Statement = {
      kind = "Statement",
      base = "Node",
      abstract = true,
   },
   Chunk = {
      kind = "Chunk",
      base = "Node",
      properties = {
         body = {
            type = "list",
            kind = "Statement"
         },
         chunkname = { type = "literal", value = "string" },
      }
   },
   Identifier = {
      kind = "Identifier",
      base = "Expression",
      properties = {
         name = { type = "literal", value = "string" },
      }
   },
   Vararg = {
      kind = "Vararg",
      base = "Identifier",
      properties = { }
   },
   BinaryExpression = {
      kind = "BinaryExpression",
      base = "Expression",
      properties = {
         operator = {
            type   = "enum",
            values = {
               "+", "-", "*", "/", "^", "%",
               "==", "~=", ">=", ">", "<=", "<",
            }
         },
         left = "Expression",
         right = "Expression",
      }
   },
   ConcatenateExpression = {
      kind = "ConcatenateExpression",
      base = "Expression",
      properties = {
         terms = {
            type = "list",
            kind = "Expression",
         }
      }
   },
   UnaryExpression = {
      kind = "UnaryExpression",
      base = "Expression",
      properties = {
         operator = {
            type   = "enum",
            values = { "not", "-", "#" },
         },
         argument = "Expression",
      }
   },
   ExpressionValue = {
      kind = "ExpressionValue",
      base = "Expression",
      properties = {
         value = "Expression",
      }
   },
   AssignmentExpression = {
      kind = "AssignmentExpression",
      base = "Statement",
      properties = {
         left = {
            type = "list",
            kind = { type = "choice", values = { "MemberExpression", "Identifier" } },
         },
         right = {
            type = "list",
            kind = "Expression",
         }
      }
   },
   LogicalExpression = {
      kind = "LogicalExpression",
      base = "Expression",
      properties = {
         operator = {
            type = "enum",
            values = { "and", "or" }
         },
         left  = "Expression",
         right = "Expression",
      }
   },
   MemberExpression = {
      kind = "MemberExpression",
      base = "Expression",
      properties = {
         object = "Expression",
         property = "Expression",
         computed = {
            type = "literal",
            value = "boolean",
            default = false
         },
      }
   },
   CallExpression = {
      kind = "CallExpression",
      base = "Expression",
      properties = {
         callee = "Expression",
         arguments = { type = "list", kind = "Expression" },
      }
   },
   SendExpression = {
      kind = "SendExpression",
      base = "Expression",
      properties = {
         receiver = "Expression",
         method = "Identifier",
         arguments = {
            type = "list",
            kind = "Expression"
         }
      }
    },
    Literal = {
      kind = "Literal",
      base = "Expression",
      properties = {
         value = {
            type = "choice",
            values = {
               { type = "literal", value = "string" },
               { type = "literal", value = "number" },
               { type = "literal", value = "nil" },
               { type = "literal", value = "boolean" },
               { type = "literal", value = "cdata" },
            }
         },
      }
   },
   Table = {
      kind = "Table",
      base = "Expression",
      properties = {
         array_entries = {
            type = "list",
            kind = "Expression",
         },
         hash_keys = {
            type = "list",
            kind = "Expression",
         },
         hash_values = {
            type = "list",
            kind = "Expression",
         },
      }
   },
   ExpressionStatement = {
      kind = "ExpressionStatement",
      base = "Statement",
      properties = {
         expression = {
            type = "choice",
            values = { "Statement", "Expression" },
         }
      }
   },
   StatementsGroup = {
       kind = "StatementsGroup",
       base = "Statement",
       properties = {
           statements = {
               type = "list",
               kind = "Statement",
           }
       }
   },
   EmptyStatement = {
      kind = "EmptyStatement",
      base = "Statement",
      properties = { },
   },
   DoStatement = {
      kind = "DoStatement",
      base = "Statement",
      properties = {
         body = {
            type = "list",
            kind = "Statement",
         }
      }
   },
   IfStatement = {
      kind = "IfStatement",
      base = "Statement",
      properties = {
         tests = {
            type = "list",
            kind = "Expression",
         },
         cons = {
            type = "list",
            kind = { type = "list", kind = "Statement" },
         },
         alternate = {
            type = "list",
            kind = "Statement",
            optional = true,
         }
      }
   },
   LabelStatement = {
      kind = "LabelStatement",
      base = "Statement",
      properties = {
         label = { type = "literal", value = "string" },
      }
   },
   GotoStatement = {
      kind = "GotoStatement",
      base = "Statement",
      properties = {
         label = { type = "literal", value = "string" }
      }
   },
   BreakStatement = {
      kind = "BreakStatement",
      base = "Statement",
      properties = { },
   },
   ReturnStatement = {
      kind = "ReturnStatement",
      base = "Statement",
      properties = {
         arguments = {
            type = "list",
            kind = "Expression"
         }
      }
   },
   WhileStatement = {
      kind = "WhileStatement",
      base = "Statement",
      properties = {
         test = "Expression",
         body = {
            type = "list",
            kind = "Statement"
         }
      }
   },
   RepeatStatement = {
      kind = "RepeatStatement",
      base = "Statement",
      properties = {
         test = "Expression",
         body = {
            type = "list",
            kind = "Statement",
         },
      }
   },
   ForInit = {
      kind = "ForInit",
      base = "Expression",
      properties = {
         id = "Identifier",
         value = "Expression",
      }
   },
   ForStatement = {
      kind = "ForStatement",
      base = "Statement",
      properties = {
         init = "ForInit",
         last = "Expression",
         step = {
            type = "node",
            kind = "Expression",
            optional = true,
         },
         body = {
            type = "list",
            kind = "Statement",
         },
      }
   },
   ForNames = {
      kind = "ForNames",
      base = "Expression",
      properties = {
         names = {
            type = "list",
            kind = "Identifier",
         }
      }
   },
   ForInStatement = {
      kind = "ForInStatement",
      base = "Statement",
      properties = {
         namelist = "ForNames",
         explist = {
            type = "list",
            kind = "Expression"
         },
         body = {
            type = "list",
            kind = "Statement",
         },
      }
   },
   LocalDeclaration = {
      kind = "LocalDeclaration",
      base = "Statement",
      properties = {
         names = {
            type = "list",
            kind = "Identifier"
         },
         expressions = {
            type = "list",
            kind = "Expression"
         }
      }
   },
   FunctionDeclaration = {
      kind = "FunctionDeclaration",
      base = "Statement",
      properties = {
         id = {
            type = "choice",
            values = { "MemberExpression", "Identifier" },
         },
         body = {
            type = "list",
            kind = "Statement",
         },
         params = {
            type = "list",
            kind = "Identifier",
         },
         vararg = {
            type = "literal",
            value = "boolean",
            default = false
         },
         locald = {
            type = "literal",
            value = "boolean",
            default = false
         }
      }
   },
   FunctionExpression = {
      kind = "FunctionExpression",
      base = "Expression",
      properties = {
         body = {
            type = "list",
            kind = "Statement",
         },
         params = {
            type = "list",
            kind = "Identifier",
         },
         vararg = {
            type = "literal",
            value = "boolean",
            default = false
         }
      }
   }
}

local check

local function iskind(prop, tag)
   if type(prop) ~= "table" then
      return false
   end
   local meta = syntax[prop.kind]
   while meta do
      if meta.kind == tag then
         return true
      end
      meta = syntax[meta.base]
   end
   return false
end

local function isnode(prop)
   return iskind(prop, "Node")
end

local function kind2str(spec)
   if type(spec) == "string" then
      return spec
   elseif spec.type == "node" then
      return spec.kind
   elseif spec.type == "list" then
      local etype = kind2str(spec.kind)
      return "list of " .. etype
   elseif spec.type == "enum" then
      local ls = {}
      for i = 1, #spec.values do ls[i] = spec.values[i] end
      return table.concat(ls, ", ")
   elseif spec.type == "literal" then
      return "literal " .. spec.value
   elseif spec.type == "choice" then
      local ls = {}
      for i = 1, #spec.values do ls[i] = kind2str(spec.values[i]) end
      return table.concat(ls, "|")
   else
      error("internal error: invalid spec type")
   end
end

local function check_node(tag, prop)
   if not isnode(prop) then
      return false, "expected Node"
   end
   if not iskind(prop, tag) then
      return false, "expected " .. tag
   end
   return true
end

local function check_list(spec, prop)
   if type(prop) ~= "table" then
      return false, "expected list of "..kind2str(spec.kind).." (got "..type(prop)..")"
   end
   if isnode(prop) then
      return false, "expected list of "..kind2str(spec.kind).." (got node)"
   end
   for i=1, #prop do
      local ok, err = check(spec.kind, prop[i])
      if not ok then
         return false, err.." (got "..prop[i].kind..")"
      end
   end
   return true
end

local function check_enum(spec, prop)
   for i=1, #spec.values do
      if prop == spec.values[i] then return true end
   end
   return false, "expected one of "..kind2str(spec).." (got '"..tostring(prop).."')"
end

local function check_literal(spec, prop)
   assert(type(spec.value) == "string")
   if type(prop) ~= spec.value then
      return false, "expected "..spec.value.." (got "..type(prop)..")"
   end
   return true
end

local function check_choice(spec, prop)
   for i = 1, #spec.values do
      if check(spec.values[i], prop) then
         return true
      end
   end
   return false, "expected one of "..kind2str(spec).." (got '"..tostring(prop).."')"
end

function check(spec, prop)
   if type(spec) == "string" then
      return check_node(spec, prop)
   elseif spec.type == "node" then
      return check_node(spec.kind, prop)
   elseif spec.type == "list" then
      return check_list(spec, prop)
   elseif spec.type == "enum" then
      return check_enum(spec, prop)
   elseif spec.type == "literal" then
      return check_literal(spec, prop)
   elseif spec.type == "choice" then
      return check_choice(spec, prop)
   else
      error("internal error: invalid spec type")
   end
end

local function validate(meta, node)
   if meta == nil then
      error("unknown node kind: "..node.kind)
   end
   for name, spec in pairs(meta.properties) do
      if node[name] == nil and type(spec.default) ~= 'nil' then
         node[name] = spec.default
      end
      local prop = node[name]
      if prop ~= nil or not spec.optional then
         local ok, er = check(spec, prop)
         if not ok then
            error(er.." for "..(node.kind or "?").."."..name)
         end
      end
   end
   return node
end

local function build(kind, props)
   local meta = syntax[kind]
   props.kind = kind
   return validate(meta, props)
end

return {
   syntax = syntax,
   build  = build,
}
