(function() {
  var Emitter, EmitterMixin, Grammar, Grim, Injections, OnigRegExp, Pattern, Rule, ScopeSelector, TokenizeLineResult, fs, path, _;

  path = require('path');

  _ = require('underscore-plus');

  fs = require('fs-plus');

  OnigRegExp = require('oniguruma').OnigRegExp;

  Emitter = require('event-kit').Emitter;

  Grim = require('grim');

  Injections = require('./injections');

  Pattern = require('./pattern');

  Rule = require('./rule');

  ScopeSelector = require('./scope-selector');

  module.exports = Grammar = (function() {
    Grammar.prototype.registration = null;

    function Grammar(registry, options) {
      var firstLineMatch, injectionSelector, injections, patterns, repository;
      this.registry = registry;
      if (options == null) {
        options = {};
      }
      this.name = options.name, this.fileTypes = options.fileTypes, this.scopeName = options.scopeName, this.foldingStopMarker = options.foldingStopMarker, this.maxTokensPerLine = options.maxTokensPerLine;
      injections = options.injections, injectionSelector = options.injectionSelector, patterns = options.patterns, repository = options.repository, firstLineMatch = options.firstLineMatch;
      this.emitter = new Emitter;
      this.repository = null;
      this.initialRule = null;
      this.rawPatterns = patterns;
      this.rawRepository = repository;
      if (injectionSelector != null) {
        this.injectionSelector = new ScopeSelector(injectionSelector);
      } else {
        this.injectionSelector = null;
      }
      if (firstLineMatch) {
        this.firstLineRegex = new OnigRegExp(firstLineMatch);
      } else {
        this.firstLineRegex = null;
      }
      if (this.fileTypes == null) {
        this.fileTypes = [];
      }
      this.includedGrammarScopes = [];
      this.injections = new Injections(this, injections);
    }


    /*
    Section: Event Subscription
     */

    Grammar.prototype.onDidUpdate = function(callback) {
      return this.emitter.on('did-update', callback);
    };


    /*
    Section: Tokenizing
     */

    Grammar.prototype.tokenizeLines = function(text) {
      var line, lineNumber, lines, ruleStack, scopes, tags, _i, _len, _ref, _results;
      lines = text.split('\n');
      ruleStack = null;
      scopes = [];
      _results = [];
      for (lineNumber = _i = 0, _len = lines.length; _i < _len; lineNumber = ++_i) {
        line = lines[lineNumber];
        _ref = this.tokenizeLine(line, ruleStack, lineNumber === 0), tags = _ref.tags, ruleStack = _ref.ruleStack;
        _results.push(this.registry.decodeTokens(line, tags, scopes));
      }
      return _results;
    };

    Grammar.prototype.tokenizeLine = function(line, ruleStack, firstLine, compatibilityMode) {
      var contentScopeName, initialRule, initialRuleStackLength, lastRule, lastSymbol, match, nextTags, openScopeTags, penultimateRule, popStack, position, previousPosition, previousRuleStackLength, rule, scopeName, tag, tags, tagsEnd, tagsStart, tokenCount, _i, _j, _k, _len, _len1, _len2, _ref, _ref1, _ref2, _ref3, _ref4, _ref5;
      if (firstLine == null) {
        firstLine = false;
      }
      if (compatibilityMode == null) {
        compatibilityMode = true;
      }
      tags = [];
      if (ruleStack != null) {
        ruleStack = ruleStack.slice();
        if (compatibilityMode) {
          openScopeTags = [];
          for (_i = 0, _len = ruleStack.length; _i < _len; _i++) {
            _ref = ruleStack[_i], scopeName = _ref.scopeName, contentScopeName = _ref.contentScopeName;
            if (scopeName) {
              openScopeTags.push(this.registry.startIdForScope(scopeName));
            }
            if (contentScopeName) {
              openScopeTags.push(this.registry.startIdForScope(contentScopeName));
            }
          }
        }
      } else {
        if (compatibilityMode) {
          openScopeTags = [];
        }
        initialRule = this.getInitialRule();
        scopeName = initialRule.scopeName, contentScopeName = initialRule.contentScopeName;
        ruleStack = [
          {
            rule: initialRule,
            scopeName: scopeName,
            contentScopeName: contentScopeName
          }
        ];
        if (scopeName) {
          tags.push(this.startIdForScope(initialRule.scopeName));
        }
        if (contentScopeName) {
          tags.push(this.startIdForScope(initialRule.contentScopeName));
        }
      }
      initialRuleStackLength = ruleStack.length;
      position = 0;
      tokenCount = 0;
      while (true) {
        previousRuleStackLength = ruleStack.length;
        previousPosition = position;
        if (position === line.length + 1) {
          break;
        }
        if (tokenCount >= this.getMaxTokensPerLine() - 1) {
          tags.push(line.length - position);
          while (ruleStack.length > initialRuleStackLength) {
            _ref1 = ruleStack.pop(), scopeName = _ref1.scopeName, contentScopeName = _ref1.contentScopeName;
            if (contentScopeName) {
              tags.push(this.endIdForScope(contentScopeName));
            }
            if (scopeName) {
              tags.push(this.endIdForScope(scopeName));
            }
          }
          break;
        }
        if (match = _.last(ruleStack).rule.getNextTags(ruleStack, line, position, firstLine)) {
          nextTags = match.nextTags, tagsStart = match.tagsStart, tagsEnd = match.tagsEnd;
          if (position < tagsStart) {
            tags.push(tagsStart - position);
            tokenCount++;
          }
          tags.push.apply(tags, nextTags);
          for (_j = 0, _len1 = nextTags.length; _j < _len1; _j++) {
            tag = nextTags[_j];
            if (tag >= 0) {
              tokenCount++;
            }
          }
          position = tagsEnd;
        } else {
          if (position < line.length || line.length === 0) {
            tags.push(line.length - position);
          }
          break;
        }
        if (position === previousPosition) {
          if (ruleStack.length === previousRuleStackLength) {
            console.error("Popping rule because it loops at column " + position + " of line '" + line + "'", _.clone(ruleStack));
            if (ruleStack.length > 1) {
              _ref2 = ruleStack.pop(), scopeName = _ref2.scopeName, contentScopeName = _ref2.contentScopeName;
              if (contentScopeName) {
                tags.push(this.endIdForScope(contentScopeName));
              }
              if (scopeName) {
                tags.push(this.endIdForScope(scopeName));
              }
            } else {
              if (position < line.length || (line.length === 0 && tags.length === 0)) {
                tags.push(line.length - position);
              }
              break;
            }
          } else if (ruleStack.length > previousRuleStackLength) {
            _ref3 = ruleStack.slice(-2), (_ref4 = _ref3[0], penultimateRule = _ref4.rule), (_ref5 = _ref3[1], lastRule = _ref5.rule);
            if ((lastRule != null) && lastRule === penultimateRule) {
              popStack = true;
            }
            if (((lastRule != null ? lastRule.scopeName : void 0) != null) && penultimateRule.scopeName === lastRule.scopeName) {
              popStack = true;
            }
            if (popStack) {
              ruleStack.pop();
              lastSymbol = _.last(tags);
              if (lastSymbol < 0 && lastSymbol === this.startIdForScope(lastRule.scopeName)) {
                tags.pop();
              }
              tags.push(line.length - position);
              break;
            }
          }
        }
      }
      for (_k = 0, _len2 = ruleStack.length; _k < _len2; _k++) {
        rule = ruleStack[_k].rule;
        rule.clearAnchorPosition();
      }
      if (compatibilityMode) {
        return new TokenizeLineResult(line, openScopeTags, tags, ruleStack, this.registry);
      } else {
        return {
          line: line,
          tags: tags,
          ruleStack: ruleStack
        };
      }
    };

    Grammar.prototype.activate = function() {
      return this.registration = this.registry.addGrammar(this);
    };

    Grammar.prototype.deactivate = function() {
      var _ref;
      this.emitter = new Emitter;
      if ((_ref = this.registration) != null) {
        _ref.dispose();
      }
      return this.registration = null;
    };

    Grammar.prototype.clearRules = function() {
      this.initialRule = null;
      return this.repository = null;
    };

    Grammar.prototype.getInitialRule = function() {
      return this.initialRule != null ? this.initialRule : this.initialRule = this.createRule({
        scopeName: this.scopeName,
        patterns: this.rawPatterns
      });
    };

    Grammar.prototype.getRepository = function() {
      return this.repository != null ? this.repository : this.repository = (function(_this) {
        return function() {
          var data, name, repository, _ref;
          repository = {};
          _ref = _this.rawRepository;
          for (name in _ref) {
            data = _ref[name];
            if ((data.begin != null) || (data.match != null)) {
              data = {
                patterns: [data],
                tempName: name
              };
            }
            repository[name] = _this.createRule(data);
          }
          return repository;
        };
      })(this)();
    };

    Grammar.prototype.addIncludedGrammarScope = function(scope) {
      if (!_.include(this.includedGrammarScopes, scope)) {
        return this.includedGrammarScopes.push(scope);
      }
    };

    Grammar.prototype.grammarUpdated = function(scopeName) {
      if (!_.include(this.includedGrammarScopes, scopeName)) {
        return false;
      }
      this.clearRules();
      this.registry.grammarUpdated(this.scopeName);
      if (Grim.includeDeprecatedAPIs) {
        this.emit('grammar-updated');
      }
      this.emitter.emit('did-update');
      return true;
    };

    Grammar.prototype.startIdForScope = function(scope) {
      return this.registry.startIdForScope(scope);
    };

    Grammar.prototype.endIdForScope = function(scope) {
      return this.registry.endIdForScope(scope);
    };

    Grammar.prototype.scopeForId = function(id) {
      return this.registry.scopeForId(id);
    };

    Grammar.prototype.createRule = function(options) {
      return new Rule(this, this.registry, options);
    };

    Grammar.prototype.createPattern = function(options) {
      return new Pattern(this, this.registry, options);
    };

    Grammar.prototype.getMaxTokensPerLine = function() {
      return this.maxTokensPerLine;
    };

    Grammar.prototype.scopesFromStack = function(stack, rule, endPatternMatch) {
      var contentScopeName, scopeName, scopes, _i, _len, _ref;
      scopes = [];
      for (_i = 0, _len = stack.length; _i < _len; _i++) {
        _ref = stack[_i], scopeName = _ref.scopeName, contentScopeName = _ref.contentScopeName;
        if (scopeName) {
          scopes.push(scopeName);
        }
        if (contentScopeName) {
          scopes.push(contentScopeName);
        }
      }
      if (endPatternMatch && (rule != null ? rule.contentScopeName : void 0) && rule === stack[stack.length - 1]) {
        scopes.pop();
      }
      return scopes;
    };

    return Grammar;

  })();

  if (Grim.includeDeprecatedAPIs) {
    EmitterMixin = require('emissary').Emitter;
    EmitterMixin.includeInto(Grammar);
    Grammar.prototype.on = function(eventName) {
      if (eventName === 'did-update') {
        Grim.deprecate("Call Grammar::onDidUpdate instead");
      } else {
        Grim.deprecate("Call explicit event subscription methods instead");
      }
      return EmitterMixin.prototype.on.apply(this, arguments);
    };
  }

  TokenizeLineResult = (function() {
    function TokenizeLineResult(line, openScopeTags, tags, ruleStack, registry) {
      this.line = line;
      this.openScopeTags = openScopeTags;
      this.tags = tags;
      this.ruleStack = ruleStack;
      this.registry = registry;
    }

    Object.defineProperty(TokenizeLineResult.prototype, 'tokens', {
      get: function() {
        return this.registry.decodeTokens(this.line, this.tags, this.openScopeTags);
      }
    });

    return TokenizeLineResult;

  })();

}).call(this);
