{
  require 'coffee-script'
  @_ = require './utilities'
  assert = require 'assert'

  @_.extend(@, require("./index"))

  @pushScope = (scope = new @Scope(@scope)) =>
    assert(scope instanceof @Scope, "Malformed arguments to pushScope")
    assert(scope.parent == @scope, "Next scope must be child of current scope")
    @scope = scope

  @popScope = (statements) =>
    scopeOld = @scope
    scopeNew = @scope.parent
    @scope = scopeNew
    return scopeOld

  @rehead = (head, body) => [head].concat(@_.pluck(body, 1))
}

identifier = a:[A-Za-z]b:[a-zA-Z0-9-_]* { a + b.join("") }
namespacedIdentifier = head:identifier tail:( "::" identifier )* { @rehead(head, tail).join("") }

comment = "//" [^\n\r]* { null }

whitespaceWeak = " " / "\t" { null }
whitespaceStrong = comment? "\n" / "\r" { null }
whitespace = whitespaceStrong / whitespaceWeak { null }

expressionSeperator = ((whitespace* "," whitespace*) / whitespace+) { null }
statementSeperator = whitespaceWeak* (";" / whitespaceStrong) whitespace* { null }

////////////////////////////////////////////////////////////////////////////////
//                                                                            //
//                                  Scope                                     //
//                                                                            //
////////////////////////////////////////////////////////////////////////////////

global = ({ @globalScope = @scope = @Scope.createGlobal() }) whitespace* statements:scopeBody whitespace* { @globalScope.addStatements(statements); @globalScope }

scope = ({ @pushScope() }) "{" whitespace* body:scopeBody whitespace* "}" { @scope.addStatements(body); @popScope() }

scopeBody = statements:(head:statement tail:(statementSeperator statement)* statementSeperator? whitespace* { @rehead(head, tail) })? comment? whitespace* { @_.compact(statements) || [] }

////////////////////////////////////////////////////////////////////////////////
//                                                                            //
//                                 Arguments                                  //
//                                                                            //
////////////////////////////////////////////////////////////////////////////////

arguments =
  head:argumentsItem tail:(expressionSeperator argumentsItem)* { new @ExpressionSet(@rehead(head, tail) ) }

argumentsItem =
  name:(identifier / integer) whitespace* ":" whitespace* expression:expression { name: name, expression: expression } /
  expression:expression { { expression: expression } }

argumentsDefinition =
  "(" whitespace* "*" whitespace* ")" { @ArgumentsDefinition.WILDCARD } /
  "(" whitespace* head:argumentsDefinitionItem tail:(expressionSeperator argumentsDefinitionItem)* whitespace* ")" { new @ArgumentsDefinition(@rehead(head, tail), @scope) } /
  "" { @ArgumentsDefinition.ZERO }

argumentsDefinitionItem =
  name:identifier whitespace* "=" whitespace* expression:expression { name: name, expression: expression } /
  name:identifier { name: name }

////////////////////////////////////////////////////////////////////////////////
//                                                                            //
//                                 Statement                                  //
//                                                                            //
////////////////////////////////////////////////////////////////////////////////

statement =
  comment /
  macroDefinitionStatement /
  macroReferenceStatement /
  layerStatement /
  classStatement /
  loopStatement /
  conditionalStatement /
  propertyStatement /
  javascriptStatement

propertyStatement =
  name:identifier whitespace* ":" whitespace* expression:expression { new @PropertyStatement(name, expression) }

macroDefinitionStatement =
  name:namespacedIdentifier whitespace* args:argumentsDefinition whitespace* "=" whitespace* body:expression { new @MacroDefinitionStatement(name, args, body) }

macroReferenceStatement =
  name:namespacedIdentifier whitespace* "(" whitespace* expressions:arguments whitespace* ")" { new @MacroReferenceStatement(name, expressions) } /
  name:namespacedIdentifier whitespace* "(" whitespace* ")" { new @MacroReferenceStatement(name, @ExpressionSet.ZERO) }

loopStatement =
  "for" whitespace+ keyIdentifier:identifier expressionSeperator valueIdentifier:identifier whitespace+ "in" whitespace+ collection:expression whitespace* scope:scope { new @LoopStatement(scope, valueIdentifier, keyIdentifier, collection) } /
  "for" whitespace+ valueIdentifier:identifier whitespace+ "in" whitespace+ collection:expression whitespace* scope:scope { new @LoopStatement(scope, valueIdentifier, null, collection) }

conditionalStatement =
  ifItem:conditionalStatementIf elseIfItems:( whitespace* conditionalStatementElseIf  )* whitespace* elseItem:conditionalStatementElse? { new @ConditionalStatement(@_.compact(@rehead(ifItem, elseIfItems).concat([elseItem]))) }

conditionalStatementIf =
  "if" whitespace+ condition:expression whitespace* scope:scope { {condition: condition, scope: scope} }

conditionalStatementElseIf =
  "else if" whitespace+ condition:expression whitespace* scope:scope { {condition: condition, scope: scope} }

conditionalStatementElse =
  "else" whitespace* scope:scope { {condition: new @LiteralExpression(true), scope: scope} }

layerStatement =
  "#" name:identifier? whitespace* body:scope { new @LayerStatement(name, body) }

classStatement =
  "." name:identifier whitespace* body:scope { new @ClassStatement(name, body) }

javascriptStatement =
  "`" body:[^\`]* "`" { new @JavascriptStatement(body.join("")) }

////////////////////////////////////////////////////////////////////////////////
//                                                                            //
//                                 Expression                                 //
//                                                                            //
////////////////////////////////////////////////////////////////////////////////

expression =
  filterExpression /
  arithmeticExpression

/////////////////////////////////////////////
//           Filter Expression             //
/////////////////////////////////////////////

filterExpression =
  notOperatorExpression /
  booleanLogicExpression /
  filterStrongExpression

booleanLogicExpression =
  left:filterStrongExpression whitespace* operator:("||" / "&&") whitespace* right:expression { new @BooleanLogicExpression(operator, [left, right]) }

notOperatorExpression =
  "!" whitespace* expression:expression { new @NotOperatorExpression(expression) }

/////////////////////////////////////////////
//         Filter Weak Expression          //
/////////////////////////////////////////////

filterStrongExpression =
  groupExpression /
  setOperatorExpression /
  comparisonOperatorExpression /
  typeCheckOperatorExpression

groupExpression =
  whitespace* "(" expression:expression ")" { expression }

typeCheckOperatorExpression =
  whitespace* "is" whitespace* type:arithmeticExpression { new @TypeCheckOperatorExpression(type) }

comparisonOperatorExpression =
  whitespace* left:arithmeticExpression whitespace* operator:("==" / ">=" / "<=" / "<" / ">" / "!=") whitespace* right:arithmeticExpression { new @ComparisonOperatorExpression(left, operator, right) }

setOperatorExpression =
  whitespace* left:arithmeticExpression whitespace+ operator:("in" / "!in") whitespace+ right:arithmeticExpression { new @SetOperatorExpression(left, operator, right) }

/////////////////////////////////////////////
//     Arithmetic Operator Expression      //
/////////////////////////////////////////////

arithmeticExpression =
  left:arithmeticStrongExpression whitespaceWeak* operator:("+" / "-") whitespaceWeak* right:arithmeticExpression { new @ArithmeticOperatorExpression(left, operator, right) } /
  arithmeticStrongExpression

arithmeticStrongExpression =
  left:propertyAccessExpression whitespaceWeak* operator:("/" / "*") whitespaceWeak* right:arithmeticStrongExpression { new @ArithmeticOperatorExpression(left, operator, right) } /
  "(" whitespace* expression:arithmeticExpression whitespace* ")" { expression } /
  conditionalExpression

/////////////////////////////////////////////
//            Conditional Expression       //
/////////////////////////////////////////////

conditionalExpression =
  head:propertyAccessExpression whitespaceWeak* '??' whitespaceWeak* tail:conditionalExpression { new @NullCoalescingExpression(head, tail) } /
  condition:propertyAccessExpression whitespaceWeak* '?' whitespaceWeak* trueExpression:conditionalExpression whitespaceWeak* ':' whitespaceWeak* falseExpression:conditionalExpression { new @TernaryExpression(condition, trueExpression, falseExpression) } /
  propertyAccessExpression

/////////////////////////////////////////////
//            Value Expression             //
/////////////////////////////////////////////

propertyAccessExpression =
  head:atomicExpression accesses:(
    "." (integer / identifier) /
    "[" expression "]"
  )* {
    output = head

    for access in accesses
      if access[0] == "."
        output = new @PropertyAccessExpression(output, new @LiteralExpression(access[1]))
      else if access[0] == "["
        output = new @PropertyAccessExpression(output, access[1])

    return output
  }

/////////////////////////////////////////////
//           Weak Value Expression         //
/////////////////////////////////////////////

atomicExpression =
  javascriptExpression /
  literalExpression /
  macroReferenceExpression /
  stringExpression /
  scopeExpression /
  arrayExpression

literalExpression =
  value:value { new @LiteralExpression(value) }

macroReferenceExpression =
  name:namespacedIdentifier whitespace* "(" whitespace* expressions:arguments whitespace* ")" { new @MacroReferenceExpression(name, expressions) } /
  name:namespacedIdentifier (whitespace* "(" whitespace* ")")? { new @MacroReferenceExpression(name, @ExpressionSet.ZERO) }

stringExpression =
  "\"" body:[^\"]* "\"" { new @StringExpression(body.join("")) } /
  "'" body:[^\']* "'" { new @StringExpression(body.join("")) }

arrayExpression =
  "[" whitespace* head:expression tail:(expressionSeperator expression)* whitespace* "]" { new @ArrayExpression(@_.map(@rehead(head, tail))) }

scopeExpression =
  body:scope { new @ScopeExpression(body) }

javascriptExpression =
  "`" body:[^\`]* "`" { new @JavascriptExpression(body.join("")) }

////////////////////////////////////////////////////////////////////////////////
//                                                                            //
//                                  Value                                     //
//                                                                            //
////////////////////////////////////////////////////////////////////////////////

value =
  null /
  boolean /
  number /
  attributeReferenceValue /
  colorValue

boolean =
  "true" { true } /
  "false" { false }

null =
  "null" { null }

number =
  before:integer? "." after:([0-9]+) { (before + parseFloat("." + after.join(""))) } /
  number:integer { number }

integer =
  head:[1-9-] tail:[0-9]* { parseInt(head + tail.join("")) } /
  "0" { 0 }

attributeReferenceValue =
  "@" name:identifier { new @AttributeReferenceValue(name) }

colorValue =
  "#" color:[0-9a-fA-F]+ { @ColorValue.hex(color.join("")) }
