%{
  (* CSS grammar based on:
   * - http://www.w3.org/TR/CSS2/grammar.html
   * - http://www.w3.org/TR/css3-mediaqueries/
   * - http://www.w3.org/TR/css3-fonts/
   * - http://www.w3.org/TR/css3-namespace/
   * - http://www.w3.org/TR/css3-animations/
   * - http://www.w3.org/TR/css3-conditional/
   *)
  open Lexing
  open Types
  open Util

  type term = Term of expr | Operator of string

  let concat_terms terms =
    let rec transform_ops = function
      | [] -> []
      | Term left :: Operator op :: Term right :: tl ->
        transform_ops (Term (Nary (op, [left; right])) :: tl)
      | Term hd :: tl -> hd :: transform_ops tl
      | Operator op :: _ -> raise (Syntax_error ("unexpected operator \"" ^ op ^ "\""))
    in
    let rec flatten_nary = function
      | [] -> []
      | Nary (op, Nary (op2, left) :: right) :: tl when op2 = op ->
        Nary (op, flatten_nary left @ flatten_nary right) :: flatten_nary tl
      | hd :: tl -> hd :: flatten_nary tl
    in
    match terms |> transform_ops |> flatten_nary with
    | [hd] -> hd
    | l -> Concat l

  (* TODO: move this to a normalization stage, because the syntax should be
   * preserved during parsing (e.g. for -echo command) *)
  let unary_number = function
    | Unary ("-", Number (n, u)) -> Number (-.n, u)
    | Unary ("+", (Number _ as n)) -> n
    | value -> value
%}

(* Tokens *)
%token S CDO CDC IMPORT_SYM PAGE_SYM MEDIA_SYM CHARSET_SYM FONT_FACE_SYM
%token NAMESPACE_SYM KEYFRAMES_SYM SUPPORTS_SYM IMPORTANT_SYM
%token <float> PERCENTAGE NUMBER
%token <float * string> UNIT_VALUE
%token <string> COMBINATOR RELATION STRING IDENT HASH URI FUNCTION
%token LPAREN RPAREN LBRACE RBRACE LBRACK RBRACK SEMICOL COLON COMMA DOT PLUS
%token MINUS SLASH STAR ONLY AND (*OR*) NOT FROM TO EOF
%token WS_AND WS_OR

(* Start symbol *)
%type <Types.stylesheet> stylesheet
%start stylesheet

%%

(* list with arbitrary whitespace between elements and separators *)
%inline ig2(a, b): a b {}
%inline ig3(a, b, c): a b c {}
%inline wslist(sep, x): S* l=separated_list(sep, terminated(x, S*))  { l }
%inline wspreceded(prefix, x): p=preceded(ig2(prefix, S*), x) { p }

%inline all_and: AND | WS_AND {}

cd: CDO S* | CDC S* {}

stylesheet:
  | charset    = charset? S* cd*
    imports    = terminated(import, cd*)*
    namespaces = terminated(namespace, cd*)*
    statements = terminated(nested_statement, cd*)*
                 EOF
  { let charset = match charset with None -> [] | Some c -> [c] in
    charset @ imports @ namespaces @ statements }

nested_statement:
  | s=ruleset | s=media | s=page | s=font_face_rule | s=keyframes_rule
  | s=supports_rule
  { s }

group_rule_body:
  | LBRACE S* statements=nested_statement* RBRACE S*
  { statements }

charset:
  | CHARSET_SYM name=STRING S* SEMICOL
  { Charset name }

import:
  | IMPORT_SYM S* tgt=string_or_uri media=media_query_list SEMICOL S*
  { Import (tgt, media) }
%inline string_or_uri:
  | str=STRING  { Strlit str }
  | uri=URI     { Uri uri }

namespace:
  | NAMESPACE_SYM S* prefix=terminated(namespace_prefix, S*)? ns=string_or_uri S* SEMICOL S*
  { Namespace (prefix, ns) }
%inline namespace_prefix:
  | prefix=IDENT
  { prefix }

media:
  | MEDIA_SYM queries=media_query_list rulesets=group_rule_body
  { Media (queries, rulesets) }
media_query_list:
  | S*
  { [] }
  | S* hd=media_query tl=wspreceded(COMMA, media_query)*
  { hd :: tl }
media_query:
  | prefix=only_or_not? typ=media_type S* feat=wspreceded(all_and, media_expr)*
  { (prefix, Some typ, feat) }
  | hd=media_expr tl=wspreceded(all_and, media_expr)*
  { (None, None, (hd :: tl)) }
%inline only_or_not:
  | ONLY S*   { "only" }
  | NOT S*    { "not" }
%inline media_type:
  | id=IDENT  { id }
media_expr:
  | LPAREN S* feature=media_feature S* value=wspreceded(COLON, expr)? RPAREN S*
  { (feature, value) }
%inline media_feature:
  | id=IDENT  { id }

page:
  | PAGE_SYM S* pseudo=pseudo_page? decls=decls_block
  { Page (pseudo, decls) }
pseudo_page:
  | COLON pseudo=IDENT S*
  { pseudo }

font_face_rule:
  | FONT_FACE_SYM S* LBRACE S* hd=descriptor_declaration?
    tl=wspreceded(SEMICOL, descriptor_declaration?)* RBRACE S*
  { Font_face (filter_none (hd :: tl)) }
descriptor_declaration:
  | name=property COLON S* value=expr
  { (name, value) }

keyframes_rule:
  | KEYFRAMES_SYM S* id=IDENT S* LBRACE S* rules=keyframe_ruleset* RBRACE S*
  { Keyframes (id, rules) }
keyframe_ruleset:
  | selector=keyframe_selector S* decls=decls_block
  { (selector, decls) }
keyframe_selector:
  | FROM          { Ident "from" }
  | TO            { Ident "to" }
  | n=PERCENTAGE  { Number (n, Some "%") }

supports_rule:
  | SUPPORTS_SYM S* cond=supports_condition S* body=group_rule_body
  { Supports (cond, body) }
supports_condition:
  | c=supports_negation
  | c=supports_conjunction
  | c=supports_disjunction
  | c=supports_condition_in_parens
  { c }
supports_condition_in_parens:
  | LPAREN S* c=supports_condition S* RPAREN
  | c=supports_declaration_condition
  (*XXX: | c=general_enclosed*)
  { c }
supports_negation:
  | NOT S+ c=supports_condition_in_parens
  { Not c }
supports_conjunction:
  | hd=supports_condition_in_parens tl=preceded(WS_AND, supports_condition_in_parens)+
  { And (hd :: tl) }
supports_disjunction:
  | hd=supports_condition_in_parens tl=preceded(WS_OR, supports_condition_in_parens)+
  { Or (hd :: tl) }
supports_declaration_condition:
  | LPAREN S* decl=supports_declaration RPAREN
  { Decl decl }
supports_declaration:
  | name=property S* COLON S* value=expr
  { (name, value) }
  (*XXX:
general_enclosed:
  | ( FUNCTION | LPAREN ) ( any | unused )* RPAREN
  { Enclosed expr }

any:
[ IDENT | NUMBER | PERCENTAGE | DIMENSION | STRING
              | DELIM | URI | HASH | UNICODE-RANGE | INCLUDES
              | DASHMATCH | ':' | FUNCTION S* [any|unused]* ')'
              | '(' S* [any|unused]* ')' | '[' S* [any|unused]* ']'
              ]
S*;
unused      : block | ATKEYWORD S* | ';' S* | CDO S* | CDC S*;
  *)

%inline decls_block:
  | LBRACE S* hd=declaration? tl=wspreceded(SEMICOL, declaration?)* RBRACE S*
  { filter_none (hd :: tl) }

ruleset:
  | selectors_hd = selector
    selectors_tl = wspreceded(COMMA, selector)*
    decls        = decls_block
  { Ruleset (selectors_hd :: selectors_tl, decls) }

selector:
  | simple=simple_selector S*
  { Simple simple }
  | left=simple_selector S+ right=selector
  { Combinator (Simple left, " ", right) }
  | left=simple_selector S* com=combinator right=selector
  { Combinator (Simple left, com, right) }
%inline combinator:
  | PLUS S*          { "+" }
  | c=COMBINATOR S*  { c }

simple_selector:
  | elem=element_name addons=element_addon*
  { elem ^ String.concat "" addons }
  | addons=element_addon+
  { String.concat "" addons }
%inline element_addon: a=HASH | a=cls | a=attrib | a=pseudo { a }
element_name:
  | tag=IDENT  { String.lowercase tag }
  | STAR       { "*" }
cls:
  | DOT name=IDENT
  { "." ^ name }
attrib:
  | LBRACK S* left=IDENT S* right=pair(RELATION, rel_value)? RBRACK
  { let right = match right with None -> "" | Some (op, term) -> op ^ term in
    "[" ^ String.lowercase left ^ right ^ "]" }
%inline rel_value:
  | S* id=IDENT S*  { id }
  | S* s=STRING S*  { "\"" ^ s ^ "\"" }
pseudo:
  | COLON id=IDENT
  { ":" ^ (String.lowercase id) }
  | COLON f=FUNCTION args=wslist(COMMA, simple_selector) RPAREN
  { ":" ^ String.lowercase f ^ "(" ^ String.concat "," args ^ ")" }

declaration:
  | name=property S* COLON S* value=expr important=boption(ig2(IMPORTANT_SYM, S*))
  { (String.lowercase name, value, important) }
%inline property:
  | name=IDENT       { name }
  | STAR name=IDENT  { "*" ^ name }  (* IE7 property name hack *)

expr:
  | l=exprl             { concat_terms l }
%inline exprl:
  | hd=term tl=opterm*  { Term hd :: List.concat tl }
%inline opterm:
  | t=term              { [Term t] }
  | op=operator t=term  { [Operator op; Term t] }
%inline operator:
  | SLASH S*            { "/" }
  | COMMA S*            { "," }

term:
  | op=unary_operator v=numval S*   { unary_number (Unary (op, v)) }
  | v=numval S*                     { v }
  | str=STRING S*                   { Strlit str }
  | id=IDENT S*                     { Ident (String.lowercase id) }
  | uri=URI S*                      { Uri uri }
  | fn=FUNCTION arg=expr RPAREN S*  { Function (String.lowercase fn, arg) }
  | hex=HASH S*
  { let h = "[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]" in
    if Str.string_match (Str.regexp ("^" ^ h ^ "\\(" ^ h ^ "\\)?$")) hex 0
      then Hexcolor (String.lowercase hex)
      else raise (Syntax_error ("invalid color #" ^ hex)) }
unary_operator:
  | MINUS  { "-" }
  | PLUS   { "+" }
%inline numval:
  | n=NUMBER      { Number (n, None) }
  | v=UNIT_VALUE  { let n, u = v in Number (n, Some (String.lowercase u)) }
  | n=PERCENTAGE  { Number (n, Some "%") }