/* description: Parses expressions. */

%options case-insensitive

/* lexical grammar */
%lex
%%
\s+                                                    /* skip whitespace */

(\-(webkit|moz)\-)?calc\b                             return 'CALC';

[a-z][a-z0-9-]*\s*\((?:(?:\"(?:\\.|[^\"\\])*\"|\'(?:\\.|[^\'\\])*\')|\([^)]*\)|[^\(\)]*)*\)  return 'FUNCTION';

"*"                                                   return 'MUL';
"/"                                                   return 'DIV';
"+"                                                   return 'ADD';
"-"                                                   return 'SUB';

([0-9]+("."[0-9]+)?|"."[0-9]+)em\b                    return 'LENGTH';     // em
([0-9]+("."[0-9]+)?|"."[0-9]+)ex\b                    return 'LENGTH';     // ex
([0-9]+("."[0-9]+)?|"."[0-9]+)ch\b                    return 'LENGTH';     // ch
([0-9]+("."[0-9]+)?|"."[0-9]+)rem\b                   return 'LENGTH';     // rem
([0-9]+("."[0-9]+)?|"."[0-9]+)vw\b                    return 'LENGTH';     // vw
([0-9]+("."[0-9]+)?|"."[0-9]+)vh\b                    return 'LENGTH';     // vh
([0-9]+("."[0-9]+)?|"."[0-9]+)vmin\b                  return 'LENGTH';     // vmin
([0-9]+("."[0-9]+)?|"."[0-9]+)vmax\b                  return 'LENGTH';     // vmax
([0-9]+("."[0-9]+)?|"."[0-9]+)vm\b                    return 'LENGTH';     // vm (non-standard name)
([0-9]+("."[0-9]+)?|"."[0-9]+)px\b                    return 'LENGTH';     // px
([0-9]+("."[0-9]+)?|"."[0-9]+)mm\b                    return 'LENGTH';     // mm
([0-9]+("."[0-9]+)?|"."[0-9]+)cm\b                    return 'LENGTH';     // cm
([0-9]+("."[0-9]+)?|"."[0-9]+)in\b                    return 'LENGTH';     // in
([0-9]+("."[0-9]+)?|"."[0-9]+)pt\b                    return 'LENGTH';     // pt
([0-9]+("."[0-9]+)?|"."[0-9]+)pc\b                    return 'LENGTH';     // pc
([0-9]+("."[0-9]+)?|"."[0-9]+)Q\b                     return 'LENGTH';     // Q
([0-9]+("."[0-9]+)?|"."[0-9]+)fr\b                    return 'LENGTH';     // fr
([0-9]+("."[0-9]+)?|"."[0-9]+)deg\b                   return 'ANGLE';      // deg
([0-9]+("."[0-9]+)?|"."[0-9]+)grad\b                  return 'ANGLE';      // grad
([0-9]+("."[0-9]+)?|"."[0-9]+)turn\b                  return 'ANGLE';      // turn
([0-9]+("."[0-9]+)?|"."[0-9]+)rad\b                   return 'ANGLE';      // rad
([0-9]+("."[0-9]+)?|"."[0-9]+)s\b                     return 'TIME';       // s
([0-9]+("."[0-9]+)?|"."[0-9]+)ms\b                    return 'TIME';       // ms
([0-9]+("."[0-9]+)?|"."[0-9]+)Hz\b                    return 'FREQ';       // Hz
([0-9]+("."[0-9]+)?|"."[0-9]+)kHz\b                   return 'FREQ';       // kHz
([0-9]+("."[0-9]+)?|"."[0-9]+)dpi\b                   return 'RES';        // dpi
([0-9]+("."[0-9]+)?|"."[0-9]+)dpcm\b                  return 'RES';        // dpcm
([0-9]+("."[0-9]+)?|"."[0-9]+)dppx\b                  return 'RES';        // dppm
([0-9]+("."[0-9]+)?|"."[0-9]+)\%                      return 'PERCENTAGE';
([0-9]+("."[0-9]+)?|"."[0-9]+)\b                      return 'NUMBER';

"("                                                   return 'LPAREN';
")"                                                   return 'RPAREN';

#\{([\s\S]*?)\}                                       return 'UNKNOWN'; // scss interpolation
@\{([\s\S]*?)\}                                       return 'UNKNOWN'; // less interpolation

\S[^\s()*/+-]*                                        return 'UNKNOWN';

<<EOF>>                                               return 'EOF';

/lex

%left ADD SUB
%left MUL DIV
%left UPREC


%start expression

%%

expression
  : math_expression EOF { return $1; }
  ;

  math_expression
    : CALC LPAREN math_expression RPAREN {
        $$ = $3;
        $$.source.start = { index: @1.range[0] };
        $$.source.end = { index: @4.range[1] };
      }
    | math_expression ADD math_expression {
        $$ = {
          type: 'MathExpression', operator: $2, left: $1, right: $3,
          source: {
            start: $1.source.start, end: $3.source.end,
            operator: { start: { index: @2.range[0] }, end: { index: @2.range[1] } }
          }
        };
      }
    | math_expression SUB math_expression {
        $$ = {
          type: 'MathExpression', operator: $2, left: $1, right: $3,
          source: {
            start: $1.source.start, end: $3.source.end,
            operator: { start: { index: @2.range[0] }, end: { index: @2.range[1] } }
          }
        };
      }
    | math_expression MUL math_expression {
        $$ = {
          type: 'MathExpression', operator: $2, left: $1, right: $3,
          source: {
            start: $1.source.start, end: $3.source.end,
            operator: { start: { index: @2.range[0] }, end: { index: @2.range[1] } }
          }
        };
      }
    | math_expression DIV math_expression {
        $$ = {
          type: 'MathExpression', operator: $2, left: $1, right: $3,
          source: {
            start: $1.source.start, end: $3.source.end,
            operator: { start: { index: @2.range[0] }, end: { index: @2.range[1] } }
          }
        };
      }
    | SUB math_expression %prec UPREC {
        if (@1.range[1] !== $2.source.start.index) {
          throw new Error('Unexpected spaces was found between sign and value');
        }
        if (typeof $2.value !== 'number') {
          throw new Error('Unexpected sign');
        }
        if ($2.sign) {
          throw new Error('Unexpected continuous sign');
        }
        $$ = $2;
        $$.sign = '-'
        $$.value = -$2.value;
        $$.source.start.index = @1.range[0];
      }
    | ADD math_expression %prec UPREC {
        if (@1.range[1] !== $2.source.start.index) {
          throw new Error('Unexpected spaces was found between sign and value');
        }
        if (typeof $2.value !== 'number') {
          throw new Error('Unexpected sign');
        }
        if ($2.sign) {
          throw new Error('Unexpected continuous sign');
        }
        $$ = $2;
        $$.sign = '+'
        $$.source.start.index = @1.range[0];
      }
    | LPAREN math_expression RPAREN {
        $$ = $2;
        $$.source.start = { index: @1.range[0] };
        $$.source.end = { index: @3.range[1] };
      }
    | function { $$ = $1; }
    | css_value { $$ = $1; }
    | value { $$ = $1; }
    ;

  value
    : NUMBER { $$ = { type: 'Value', value: parseFloat($1), source: { start: { index: @1.range[0] }, end: { index: @1.range[1] } } }; }
    ;

  function
    : FUNCTION { $$ = { type: 'Function', value: $1, source: { start: { index: @1.range[0] }, end: { index: @1.range[1] } } }; }
    ;

  css_value
    : LENGTH { $$ = { type: 'LengthValue', value: parseFloat($1), unit: /[a-z]+/i.exec($1)[0], source: { start: { index: @1.range[0] }, end: { index: @1.range[1] } } }; }
    | ANGLE { $$ = { type: 'AngleValue', value: parseFloat($1), unit: /[a-z]+/i.exec($1)[0], source: { start: { index: @1.range[0] }, end: { index: @1.range[1] } } }; }
    | TIME { $$ = { type: 'TimeValue', value: parseFloat($1), unit: /[a-z]+/i.exec($1)[0], source: { start: { index: @1.range[0] }, end: { index: @1.range[1] } } }; }
    | FREQ { $$ = { type: 'FrequencyValue', value: parseFloat($1), unit: /[a-z]+/i.exec($1)[0], source: { start: { index: @1.range[0] }, end: { index: @1.range[1] } } }; }
    | RES { $$ = { type: 'ResolutionValue', value: parseFloat($1), unit: /[a-z]+/i.exec($1)[0], source: { start: { index: @1.range[0] }, end: { index: @1.range[1] } } }; }
    | PERCENTAGE { $$ = { type: 'PercentageValue', value: parseFloat($1), unit: '%', source: { start: { index: @1.range[0] }, end: { index: @1.range[1] } } }; }
    | UNKNOWN { $$ = { type: 'UnknownValue', value: $1, unit: '', source: { start: { index: @1.range[0] }, end: { index: @1.range[1] } } }; }
    ;