text: // block; "begin", "case", "fun", "if", "receive", "try": closed by "end"
  // block internal; "after", "catch", "of"
  // guard; "when", closed by "->"
  // "->" opens a clause, closed by ";" or "."
  // "<<" opens a binary, closed by ">>"
  // "," appears in arglists, lists, tuples and terminates lines of code
  // "." resets indentation to 0
  // obsolete; "cond", "let", "query"
  
  CodeMirror.defineMIME("text/x-erlang", "erlang");
  
  CodeMirror.defineMode("erlang", function(cmCfg, modeCfg) {
  
    function rval(state,stream,type) {
      // distinguish between "." as terminator and record field operator
      if (type == "record") {
        state.context = "record";
      }else{
        state.context = false;
      }
  
      // remember last significant bit on last line for indenting
      if (type != "whitespace" && type != "comment") {
        state.lastToken = stream.current();
      }
      //     erlang             -> CodeMirror tag
      switch (type) {
        case "atom":        return "atom";
        case "attribute":   return "attribute";
        case "builtin":     return "builtin";
        case "comment":     return "comment";
        case "fun":         return "meta";
        case "function":    return "tag";
        case "guard":       return "property";
        case "keyword":     return "keyword";
        case "macro":       return "variable-2";
        case "number":      return "number";
        case "operator":    return "operator";
        case "record":      return "bracket";
        case "string":      return "string";
        case "type":        return "def";
        case "variable":    return "variable";
        case "error":       return "error";
        case "separator":   return null;
        case "open_paren":  return null;
        case "close_paren": return null;
        default:            return null;
      }
    }
  
    var typeWords = [
      "-type", "-spec", "-export_type", "-opaque"];
  
    var keywordWords = [
      "after","begin","catch","case","cond","end","fun","if",
      "let","of","query","receive","try","when"];
  
    var separatorWords = [
      "->",";",":",".",","];
  
    var operatorWords = [
      "and","andalso","band","bnot","bor","bsl","bsr","bxor",
      "div","not","or","orelse","rem","xor"];
  
    var symbolWords = [
      "+","-","*","/",">",">=","<","=<","=:=","==","=/=","/=","||","<-"];
  
    var openParenWords = [
      "<<","(","[","{"];
  
    var closeParenWords = [
      "}","]",")",">>"];
  
    var guardWords = [
      "is_atom","is_binary","is_bitstring","is_boolean","is_float",
      "is_function","is_integer","is_list","is_number","is_pid",
      "is_port","is_record","is_reference","is_tuple",
      "atom","binary","bitstring","boolean","function","integer","list",
      "number","pid","port","record","reference","tuple"];
  
    var bifWords = [
      "abs","adler32","adler32_combine","alive","apply","atom_to_binary",
      "atom_to_list","binary_to_atom","binary_to_existing_atom",
      "binary_to_list","binary_to_term","bit_size","bitstring_to_list",
      "byte_size","check_process_code","contact_binary","crc32",
      "crc32_combine","date","decode_packet","delete_module",
      "disconnect_node","element","erase","exit","float","float_to_list",
      "garbage_collect","get","get_keys","group_leader","halt","hd",
      "integer_to_list","internal_bif","iolist_size","iolist_to_binary",
      "is_alive","is_atom","is_binary","is_bitstring","is_boolean",
      "is_float","is_function","is_integer","is_list","is_number","is_pid",
      "is_port","is_process_alive","is_record","is_reference","is_tuple",
      "length","link","list_to_atom","list_to_binary","list_to_bitstring",
      "list_to_existing_atom","list_to_float","list_to_integer",
      "list_to_pid","list_to_tuple","load_module","make_ref","module_loaded",
      "monitor_node","node","node_link","node_unlink","nodes","notalive",
      "now","open_port","pid_to_list","port_close","port_command",
      "port_connect","port_control","pre_loaded","process_flag",
      "process_info","processes","purge_module","put","register",
      "registered","round","self","setelement","size","spawn","spawn_link",
      "spawn_monitor","spawn_opt","split_binary","statistics",
      "term_to_binary","time","throw","tl","trunc","tuple_size",
      "tuple_to_list","unlink","unregister","whereis"];
  
    // ignored for indenting purposes
    var ignoreWords = [
      ",", ":", "catch", "after", "of", "cond", "let", "query"];
  
  
    var smallRE      = /[a-z_]/;
    var largeRE      = /[A-Z_]/;
    var digitRE      = /[0-9]/;
    var octitRE      = /[0-7]/;
    var anumRE       = /[a-z_A-Z0-9]/;
    var symbolRE     = /[\+\-\*\/<>=\|:]/;
    var openParenRE  = /[<\(\[\{]/;
    var closeParenRE = /[>\)\]\}]/;
    var sepRE        = /[\->\.,:;]/;
  
    function isMember(element,list) {
      return (-1 < list.indexOf(element));
    }
  
    function isPrev(stream,string) {
      var start = stream.start;
      var len = string.length;
      if (len <= start) {
        var word = stream.string.slice(start-len,start);
        return word == string;
      }else{
        return false;
      }
    }
  
    function tokenize(stream, state) {
      if (stream.eatSpace()) {
        return rval(state,stream,"whitespace");
      }
  
      // attributes and type specs
      if ((peekToken(state).token == "" || peekToken(state).token == ".") &&
          stream.peek() == '-') {
        stream.next();
        if (stream.eat(smallRE) && stream.eatWhile(anumRE)) {
          if (isMember(stream.current(),typeWords)) {
            return rval(state,stream,"type");
          }else{
            return rval(state,stream,"attribute");
          }
        }
        stream.backUp(1);
      }
  
      var ch = stream.next();
  
      // comment
      if (ch == '%') {
        stream.skipToEnd();
        return rval(state,stream,"comment");
      }
  
      // macro
      if (ch == '?') {
        stream.eatWhile(anumRE);
        return rval(state,stream,"macro");
      }
  
      // record
      if ( ch == "#") {
        stream.eatWhile(anumRE);
        return rval(state,stream,"record");
      }
  
      // char
      if ( ch == "$") {
        if (stream.next() == "\\") {
          if (!stream.eatWhile(octitRE)) {
            stream.next();
          }
        }
        return rval(state,stream,"string");
      }
  
      // quoted atom
      if (ch == '\'') {
        if (singleQuote(stream)) {
          return rval(state,stream,"atom");
        }else{
          return rval(state,stream,"error");
        }
      }
  
      // string
      if (ch == '"') {
        if (doubleQuote(stream)) {
          return rval(state,stream,"string");
        }else{
          return rval(state,stream,"error");
        }
      }
  
      // variable
      if (largeRE.test(ch)) {
        stream.eatWhile(anumRE);
        return rval(state,stream,"variable");
      }
  
      // atom/keyword/BIF/function
      if (smallRE.test(ch)) {
        stream.eatWhile(anumRE);
  
        if (stream.peek() == "/") {
          stream.next();
          if (stream.eatWhile(digitRE)) {
            return rval(state,stream,"fun");      // f/0 style fun
          }else{
            stream.backUp(1);
            return rval(state,stream,"atom");
          }
        }
  
        var w = stream.current();
  
        if (isMember(w,keywordWords)) {
          pushToken(state,stream);
          return rval(state,stream,"keyword");
        }
        if (stream.peek() == "(") {
          // 'put' and 'erlang:put' are bifs, 'foo:put' is not
          if (isMember(w,bifWords) &&
              (!isPrev(stream,":") || isPrev(stream,"erlang:"))) {
            return rval(state,stream,"builtin");
          }else{
            return rval(state,stream,"function");
          }
        }
        if (isMember(w,guardWords)) {
          return rval(state,stream,"guard");
        }
        if (isMember(w,operatorWords)) {
          return rval(state,stream,"operator");
        }
        if (stream.peek() == ":") {
          if (w == "erlang") {
            return rval(state,stream,"builtin");
          } else {
            return rval(state,stream,"function");
          }
        }
        return rval(state,stream,"atom");               
      }
  
      // number
      if (digitRE.test(ch)) {
        stream.eatWhile(digitRE);
        if (stream.eat('#')) {
          stream.eatWhile(digitRE);    // 16#10  style integer
        } else {
          if (stream.eat('.')) {       // float
            stream.eatWhile(digitRE);
          }
          if (stream.eat(/[eE]/)) {
            stream.eat(/[-+]/);        // float with exponent
            stream.eatWhile(digitRE);
          }
        }
        return rval(state,stream,"number");   // normal integer
      }
  
      // open parens
      if (nongreedy(stream,openParenRE,openParenWords)) {
        pushToken(state,stream);
        return rval(state,stream,"open_paren");
      }
  
      // close parens
      if (nongreedy(stream,closeParenRE,closeParenWords)) {
        pushToken(state,stream);
        return rval(state,stream,"close_paren");
      }
  
      // separators
      if (greedy(stream,sepRE,separatorWords)) {
        // distinguish between "." as terminator and record field operator
        if (state.context == false) {
          pushToken(state,stream);
        }
        return rval(state,stream,"separator");
      }
  
      // operators
      if (greedy(stream,symbolRE,symbolWords)) {
        return rval(state,stream,"operator");
      }
  
      return rval(state,stream,null);
    }
  
    function nongreedy(stream,re,words) {
      if (stream.current().length == 1 && re.test(stream.current())) {
        stream.backUp(1);
        while (re.test(stream.peek())) {
          stream.next();
          if (isMember(stream.current(),words)) {
            return true;
          }
        }
        stream.backUp(stream.current().length-1);
      }
      return false;
    }
  
    function greedy(stream,re,words) {
      if (stream.current().length == 1 && re.test(stream.current())) {
        while (re.test(stream.peek())) {
          stream.next();
        }
        while (0 < stream.current().length) {
          if (isMember(stream.current(),words)) {
            return true;
          }else{
            stream.backUp(1);
          }
        }
        stream.next();
      }
      return false;
    }
  
    function doubleQuote(stream) {
      return quote(stream, '"', '\\');
    }
  
    function singleQuote(stream) {
      return quote(stream,'\'','\\');
    }
  
    function quote(stream,quoteChar,escapeChar) {
      while (!stream.eol()) {
        var ch = stream.next();
        if (ch == quoteChar) {
          return true;
        }else if (ch == escapeChar) {
          stream.next();
        }
      }
      return false;
    }
  
    function Token(stream) {
      this.token  = stream ? stream.current() : "";
      this.column = stream ? stream.column() : 0;
      this.indent = stream ? stream.indentation() : 0;
    }
  
    function myIndent(state,textAfter) {
      var indent = cmCfg.indentUnit;
      var outdentWords = ["after","catch"];
      var token = (peekToken(state)).token;
      var wordAfter = takewhile(textAfter,/[^a-z]/);
  
      if (isMember(token,openParenWords)) {
        return (peekToken(state)).column+token.length;
      }else if (token == "." || token == ""){
        return 0;
      }else if (token == "->") {
        if (wordAfter == "end") {
          return peekToken(state,2).column;
        }else if (peekToken(state,2).token == "fun") {
          return peekToken(state,2).column+indent;
        }else{
          return (peekToken(state)).indent+indent;
        }
      }else if (isMember(wordAfter,outdentWords)) {
        return (peekToken(state)).indent;
      }else{
        return (peekToken(state)).column+indent;
      }
    }
  
    function takewhile(str,re) {
      var m = str.match(re);
      return m ? str.slice(0,m.index) : str;
    }
  
    function popToken(state) {
      return state.tokenStack.pop();
    }
  
    function peekToken(state,depth) {
      var len = state.tokenStack.length;
      var dep = (depth ? depth : 1);
      if (len < dep) {
        return new Token;
      }else{
        return state.tokenStack[len-dep];
      }
    }
  
    function pushToken(state,stream) {
      var token = stream.current();
      var prev_token = peekToken(state).token;
      if (isMember(token,ignoreWords)) {
        return false;
      }else if (drop_both(prev_token,token)) {
        popToken(state);
        return false;
      }else if (drop_first(prev_token,token)) {
        popToken(state);
        return pushToken(state,stream);
      }else{
        state.tokenStack.push(new Token(stream));
        return true;
      }
    }
  
    function drop_first(open, close) {
      switch (open+" "+close) {
        case "when ->":       return true;
        case "-> end":        return true;
        case "-> .":          return true;
        case ". .":           return true;
        default:              return false;
      }
    }
  
    function drop_both(open, close) {
      switch (open+" "+close) {
        case "( )":         return true;
        case "[ ]":         return true;
        case "{ }":         return true;
        case "<< >>":       return true;
        case "begin end":   return true;
        case "case end":    return true;
        case "fun end":     return true;
        case "if end":      return true;
        case "receive end": return true;
        case "try end":     return true;
        case "-> ;":        return true;
        default:            return false;
      }
    }
  
    return {
      startState:
        function() {
          return {tokenStack: [],
                  context: false,
                  lastToken: null};
        },
  
      token:
        function(stream, state) {
          return tokenize(stream, state);
        },
  
      indent:
        function(state, textAfter) {
  //        console.log(state.tokenStack);
          return myIndent(state,textAfter);
        }
    };
  });
  