/*
 *
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 */

/**
 * This is a tree grammar for advanced CSS in Flex. It walks the AST generated 
 * by the CSS parser and builds CSS DOM objects.
 */
tree grammar CSSTree;

options 
{
    language = Java;
    tokenVocab = CSS;
    ASTLabelType = CommonTree;
}

@header 
{
/*
 *
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 */
	
package org.apache.royale.compiler.internal.css;

import java.util.Map;
import java.util.HashMap;
import org.apache.royale.compiler.css.*;
import org.apache.royale.compiler.problems.CSSParserProblem;

}

@members 
{

/**
 * CSS DOM object.
 */
protected CSSDocument model;

/**
 * Every definition object needs the token stream to compute source location.
 */
private final TokenStream tokenStream = getTreeNodeStream().getTokenStream();

/**
 * Tree walker problems.
 */
protected List<CSSParserProblem> problems = new ArrayList<CSSParserProblem>();

/**
 * Used for building up attribute selector strings until we implement a data
 * structure for it.
 */
protected String curAttribute;


/**
 * Collect problems.
 */
@Override
public void displayRecognitionError(String[] tokenNames, RecognitionException e)
{
    problems.add(CSSParserProblem.create(this, tokenNames, e));
}
}

stylesheet
scope 
{
    // namespace declarations are buffered in this map
    List<CSSNamespaceDefinition> namespaces;
    // ruleset definitions are buffered in this list
    List<CSSRule> rules;
    // font-face declarations are buffered in this list
    List<CSSFontFace> fontFaces;
    // keyframe declarations are buffered in this list
    List<CSSKeyFrames> keyFrames;
}
@init 
{
    $stylesheet::rules = new ArrayList<CSSRule>();
    $stylesheet::namespaces = new ArrayList<CSSNamespaceDefinition>();
    $stylesheet::fontFaces = new ArrayList<CSSFontFace>();
    $stylesheet::keyFrames = new ArrayList<CSSKeyFrames>();
}
@after 
{
    model = new CSSDocument($stylesheet::rules, 
                            $stylesheet::namespaces, 
                            $stylesheet::fontFaces,
                            $start,
                            tokenStream);
}
    :   ( namespaceStatement | fontFace | keyframes | mediaQuery | ruleset )*
    ;

namespaceStatement
@after        
{ 
    final CSSNamespaceDefinition ns = new CSSNamespaceDefinition(
            $id.text, $uri.text, $start, tokenStream);
    $stylesheet::namespaces.add(ns); 
}
    :   ^(AT_NAMESPACE id=ID? uri=STRING)
    ;
  
keyframes
    :   ^(AT_KEYFRAMES id=ID ruleset*)
         { $stylesheet::keyFrames.add(new CSSKeyFrames($id.text, CSSModelTreeType.KEYFRAMES, $start, tokenStream)); }
    |   ^(AT_WEBKIT_KEYFRAMES id=ID ruleset*)
         { $stylesheet::keyFrames.add(new CSSKeyFrames($id.text, CSSModelTreeType.KEYFRAMES_WEBKIT, $start, tokenStream)); }
    ;
  

mediaQuery
scope 
{ 
    // media query condition clauses are buffered in this list
    List<CSSMediaQueryCondition> conditions 
}
@init 
{ 
    $mediaQuery::conditions = new ArrayList<CSSMediaQueryCondition>(); 
}
    :   ^(AT_MEDIA medium ruleset*)
    ;
  
medium 
    :   ^(I_MEDIUM_CONDITIONS mediumCondition*)
    ;
  
mediumCondition
    :   ID 
    { 
        $mediaQuery::conditions.add(new CSSMediaQueryCondition($start, tokenStream)); 
    } 
    | ONLY id=ID 
    { 
        $mediaQuery::conditions.add(new CSSMediaQueryCondition($start, tokenStream)); 
        $mediaQuery::conditions.add(new CSSMediaQueryCondition($id, tokenStream)); 
    } 
    | ARGUMENTS
    { 
        $mediaQuery::conditions.add(new CSSMediaQueryCondition($start, tokenStream)); 
    } 
    | COMMA
    { 
        $mediaQuery::conditions.add(new CSSMediaQueryCondition($start, tokenStream)); 
    } 
    ;
    
fontFace
@after
{
    final CSSFontFace fontFace = new CSSFontFace($d.properties, $start, tokenStream);
    $stylesheet::fontFaces.add(fontFace);
}
    :   ^(AT_FONT_FACE d=declarationsBlock)
    ;
  
ruleset
scope 
{
    // list of subject selectors
    List<CSSSelector> subjects
}
@init 
{
    $ruleset::subjects = new ArrayList<CSSSelector>();
}
@after 
{
    final List<CSSMediaQueryCondition> mediaQueryConditions;
    if ($mediaQuery.isEmpty())
        mediaQueryConditions = null;
    else
        mediaQueryConditions = $mediaQuery::conditions;
    
    final CSSRule cssRule = new CSSRule(
            mediaQueryConditions,
            $ruleset::subjects,
            $d.properties, 
            $start, 
            tokenStream);
    $stylesheet::rules.add(cssRule);
}
    :   ^(I_RULE selectorGroup d=declarationsBlock)
    ;

selectorGroup
    :  ^(I_SELECTOR_GROUP compoundSelector+)
    ;    

compoundSelector
@init
{
    final Stack<CSSSelector> simpleSelectorStack = new Stack<CSSSelector>();
}
@after
{
    $ruleset::subjects.add(simpleSelectorStack.peek());
}
    :   ^(I_SELECTOR firstSelector[simpleSelectorStack] moreSelectors[simpleSelectorStack]*)   
    ;
    
moreSelectors [Stack<CSSSelector> simpleSelectorStack]
    :   ^(I_CHILD_SELECTOR simpleSelector[simpleSelectorStack, CombinatorType.CHILD])
    |   ^(I_PRECEDED_SELECTOR simpleSelector[simpleSelectorStack, CombinatorType.PRECEDED])
    |   ^(I_SIBLING_SELECTOR simpleSelector[simpleSelectorStack, CombinatorType.SIBLING])
    |   ^(I_SIMPLE_SELECTOR simpleSelector[simpleSelectorStack, CombinatorType.DESCENDANT]) 
    ;

firstSelector [Stack<CSSSelector> simpleSelectorStack]
    :   ^(I_SIMPLE_SELECTOR simpleSelector[simpleSelectorStack, CombinatorType.DESCENDANT])
    ;

simpleSelector [Stack<CSSSelector> simpleSelectorStack, CombinatorType combinatorType]
scope
{
    String namespace;
    String element;
    List<CSSSelectorCondition> conditions;
}
@init
{
    $simpleSelector::conditions = new ArrayList<CSSSelectorCondition>();
    final CSSCombinator combinator ;
    if (simpleSelectorStack.isEmpty())
        combinator = null;
    else                    
        combinator = new CSSCombinator(simpleSelectorStack.peek(), combinatorType, $start, tokenStream);
}
@after
{
    final CSSSelector simpleSelector = new CSSSelector(
        combinator,
        $simpleSelector::element,
        $simpleSelector::namespace,
        $simpleSelector::conditions, 
        $start, 
        tokenStream);
    simpleSelectorStack.push(simpleSelector);
}
    :   simpleSelectorFraction+
    ;    
   
    
simpleSelectorFraction
    :   elementSelector
    |   conditionSelector 
    ;
   
conditionSelector
@init
{
    ConditionType type = null;
    String name = null;
}
@after
{
    $simpleSelector::conditions.add(
        new CSSSelectorCondition(name, type, $start, tokenStream));
}
    :   ^(DOT c=ID)   { type = ConditionType.CLASS; name = $c.text; }  
    |   HASH_WORD   { type = ConditionType.ID; name = $HASH_WORD.text.substring(1); }
    |   ^(COLON NOT arg=ARGUMENTS) { type = ConditionType.NOT; name = $arg.text; }
    |   ^(COLON s=ID) { type = ConditionType.PSEUDO; name = $s.text; } 
    |   ^(DOUBLE_COLON dc=ID) { type = ConditionType.PSEUDO; name = $dc.text; } 
    |   attributeSelector { type = ConditionType.ATTRIBUTE; name = curAttribute.substring(1); }
    ;
  
elementSelector
    :   ^(PIPE ns=ID e1=ID)  
        { $simpleSelector::element = $e1.text; 
          $simpleSelector::namespace = $ns.text; }
    |   e2=ID             
        { $simpleSelector::element = $e2.text; }
    |   np=NUMBER_WITH_PERCENT             
        { $simpleSelector::element = $np.text; }
    |   STAR           
        { $simpleSelector::element = $STAR.text; }
    ;
    
attributeSelector
    :   open = SQUARE_OPEN attributeName attributeOperator* attributeValue* close = SQUARE_END
	{ curAttribute = $open.text + curAttribute + $close.text; }
    ;
    
attributeName
    :    n1 = ID
         { curAttribute = $n1.text; }
    ;
    
attributeOperator
    :    o1 = BEGINS_WITH
         { curAttribute += $o1.text; }
    |    o2 = ENDS_WITH
         { curAttribute += $o2.text; }
    |    o3 = CONTAINS
         { curAttribute += $o3.text; }
    |    o4 = LIST_MATCH
         { curAttribute += $o4.text; }
    |    o5 = HREFLANG_MATCH
         { curAttribute += $o5.text; }
    |    o6 = EQUALS
         { curAttribute += $o6.text; }
    ;
    
attributeValue
    :    s = STRING
         { curAttribute += $s.text; }
    ;
    	

declarationsBlock returns [List<CSSProperty> properties]
@init 
{
    $properties = new ArrayList<CSSProperty>();
}
    :   ^(I_DECL (declaration 
         { 
             if ($declaration.property != null)
                 $properties.add($declaration.property); 
         }
         )*)
    ;

declaration returns [CSSProperty property]
@after
{
    if ($id.text != null && $v.propertyValue != null)
        $property = new CSSProperty($id.text, $v.propertyValue, $start, tokenStream);  
}
    :   ^(COLON id=ID v=value)
    ;
    
value returns [CSSPropertyValue propertyValue]
    :   ^( I_ARRAY 
                              { final List<CSSPropertyValue> array = new ArrayList<CSSPropertyValue>(); }
           ( s1=multiValue    { array.add($s1.propertyValue); } )+
        )                     { $propertyValue = new CSSArrayPropertyValue(array, $start, tokenStream); }
    |   s2=multiValue         { $propertyValue = $s2.propertyValue; }
    ;    

multiValue returns [CSSPropertyValue propertyValue]
    :   ^( I_MULTIVALUE 
                              { final List<CSSPropertyValue> array = new ArrayList<CSSPropertyValue>(); }
           ( s1=singleValue   { array.add($s1.propertyValue); } )+
        )                     { $propertyValue = new CSSMultiValuePropertyValue(array, $start, tokenStream); }
    |   s2=singleValue        { $propertyValue = $s2.propertyValue; }
    ;
  
singleValue returns [CSSPropertyValue propertyValue]
    :   NUMBER_WITH_PERCENT         
		{ $propertyValue = new CSSNumberPropertyValue($NUMBER_WITH_PERCENT.text, $start, tokenStream); }
    |   NUMBER_WITH_UNIT         
		{ $propertyValue = new CSSNumberPropertyValue($NUMBER_WITH_UNIT.text, $start, tokenStream); }
    |   HASH_WORD         
        { $propertyValue = new CSSColorPropertyValue($start, tokenStream); }
    |   ALPHA_VALUE
        { $propertyValue = CSSKeywordPropertyValue.create($start, tokenStream); }
    |   RECT_VALUE
        { $propertyValue = CSSKeywordPropertyValue.create($start, tokenStream); }
    |   ROTATE_VALUE
        { $propertyValue = CSSKeywordPropertyValue.create($start, tokenStream); }
    |   SCALE_VALUE
        { $propertyValue = CSSKeywordPropertyValue.create($start, tokenStream); }
    |   TRANSLATE3D_VALUE
        { $propertyValue = CSSKeywordPropertyValue.create($start, tokenStream); }
    |   RGB
    	{ $propertyValue = new CSSRgbColorPropertyValue($RGB.text, $start, tokenStream); }
    |   RGBA
    	{ $propertyValue = new CSSRgbaColorPropertyValue($RGBA.text, $start, tokenStream); }
    |   ^(CLASS_REFERENCE cr=ARGUMENTS)
        { $propertyValue = new CSSFunctionCallPropertyValue($CLASS_REFERENCE.text, $cr.text, $start, tokenStream); }
    |   ^(PROPERTY_REFERENCE pr=ARGUMENTS)
        { $propertyValue = new CSSFunctionCallPropertyValue($PROPERTY_REFERENCE.text, $pr.text, $start, tokenStream); }
    |   ^(EMBED es=ARGUMENTS)
        { $propertyValue = new CSSFunctionCallPropertyValue($EMBED.text, $es.text, $start, tokenStream); }
    |   ^(URL url=ARGUMENTS format=formatOption*)
        { $propertyValue = new CSSURLAndFormatPropertyValue($URL.text, $url.text, $format.text, $start, tokenStream); }
    |   ^(LOCAL l=ARGUMENTS)
        { $propertyValue = new CSSFunctionCallPropertyValue($LOCAL.text, $l.text, $start, tokenStream); }
    |   ^(CALC l=ARGUMENTS)
        { $propertyValue = new CSSFunctionCallPropertyValue($CALC.text, $l.text, $start, tokenStream); }
    |   ^(FUNCTIONS l=ARGUMENTS)
        { $propertyValue = new CSSFunctionCallPropertyValue($FUNCTIONS.text, $l.text, $start, tokenStream); }
    |   s=STRING   
        { $propertyValue = new CSSStringPropertyValue($s.text, $start, tokenStream); }                   
    |   ID
        { $propertyValue = CSSKeywordPropertyValue.create($start, tokenStream); } 
    |   OPERATOR
        { $propertyValue = CSSKeywordPropertyValue.create($start, tokenStream); } 
    |   IMPORTANT
        { $propertyValue = CSSKeywordPropertyValue.create($start, tokenStream); } 
    ;
    
formatOption returns [CSSPropertyValue propertyValue]
    :   ^(FORMAT format=ARGUMENTS)
        { $propertyValue = new CSSFunctionCallPropertyValue($FORMAT.text, $format.text, $start, tokenStream); } 
    ;

argumentList returns [List<String> labels, List<String> values]
@init 
{
    $labels = new ArrayList<String>(3);
    $values = new ArrayList<String>(3);
}
    :   argument[$labels, $values]+
    ;
    
argument [List<String> labels, List<String> values]
@after
{
    // Use null for argument without label.
    $labels.add($l.text);
    $values.add($v.text); 
}
    :   ^(EQUALS l=ID? v=STRING)
    ;
