// Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

library classify;

import '../../compiler/implementation/scanner/scannerlib.dart';
// TODO(rnystrom): Use "package:" URL (#4968).
import 'markdown.dart' as md;

/**
 * Kinds of tokens that we care to highlight differently. The values of the
 * fields here will be used as CSS class names for the generated spans.
 */
class Classification {
  static const NONE = null;
  static const ERROR = "e";
  static const COMMENT = "c";
  static const IDENTIFIER = "i";
  static const KEYWORD = "k";
  static const OPERATOR = "o";
  static const STRING = "s";
  static const NUMBER = "n";
  static const PUNCTUATION = "p";

  // A few things that are nice to make different:
  static const TYPE_IDENTIFIER = "t";

  // Between a keyword and an identifier
  static const SPECIAL_IDENTIFIER = "r";

  static const ARROW_OPERATOR = "a";

  static const STRING_INTERPOLATION = 'si';
}

/// Returns a marked up HTML string. If the code does not appear to be valid
/// Dart code, returns the original [text].
String classifySource(String text) {
  try {
    var html = new StringBuffer();
    var tokenizer = new StringScanner.fromString(text, includeComments: true);

    var whitespaceOffset = 0;
    var token = tokenizer.tokenize();
    var inString = false;
    while (token.kind != EOF_TOKEN) {
      html.write(text.substring(whitespaceOffset, token.charOffset));
      whitespaceOffset = token.charOffset + token.slowCharCount;

      // Track whether or not we're in a string.
      switch (token.kind) {
        case STRING_TOKEN:
        case STRING_INTERPOLATION_TOKEN:
          inString = true;
          break;
      }

      final kind = classify(token);
      final escapedText = md.escapeHtml(token.value);
      if (kind != null) {
        // Add a secondary class to tokens appearing within a string so that
        // we can highlight tokens in an interpolation specially.
        var stringClass = inString ? Classification.STRING_INTERPOLATION : '';
        html.write('<span class="$kind $stringClass">$escapedText</span>');
      } else {
        html.write(escapedText);
      }

      // Track whether or not we're in a string.
      if (token.kind == STRING_TOKEN) {
        inString = false;
      }
      token = token.next;
    }
    return html.toString();
  } catch (e) {
    return text;
  }
}

bool _looksLikeType(String name) {
  // If the name looks like an UppercaseName, assume it's a type.
  return _looksLikePublicType(name) || _looksLikePrivateType(name);
}

bool _looksLikePublicType(String name) {
  // If the name looks like an UppercaseName, assume it's a type.
  return name.length >= 2 && isUpper(name[0]) && isLower(name[1]);
}

bool _looksLikePrivateType(String name) {
  // If the name looks like an _UppercaseName, assume it's a type.
  return (name.length >= 3 && name[0] == '_' && isUpper(name[1])
    && isLower(name[2]));
}

// These ensure that they don't return "true" if the string only has symbols.
bool isUpper(String s) => s.toLowerCase() != s;
bool isLower(String s) => s.toUpperCase() != s;

String classify(Token token) {
  switch (token.kind) {
    case IDENTIFIER_TOKEN:
      // Special case for names that look like types.
      final text = token.value;
      if (_looksLikeType(text)
          || text == 'num'
          || text == 'bool'
          || text == 'int'
          || text == 'double') {
        return Classification.TYPE_IDENTIFIER;
      }
      return Classification.IDENTIFIER;

    case STRING_TOKEN:
    case STRING_INTERPOLATION_TOKEN:
      return Classification.STRING;

    case INT_TOKEN:
    case HEXADECIMAL_TOKEN:
    case DOUBLE_TOKEN:
      return Classification.NUMBER;

    case COMMENT_TOKEN:
      return Classification.COMMENT;

    // => is so awesome it is in a class of its own.
    case FUNCTION_TOKEN:
      return Classification.ARROW_OPERATOR;

    case OPEN_PAREN_TOKEN:
    case CLOSE_PAREN_TOKEN:
    case OPEN_SQUARE_BRACKET_TOKEN:
    case CLOSE_SQUARE_BRACKET_TOKEN:
    case OPEN_CURLY_BRACKET_TOKEN:
    case CLOSE_CURLY_BRACKET_TOKEN:
    case COLON_TOKEN:
    case SEMICOLON_TOKEN:
    case COMMA_TOKEN:
    case PERIOD_TOKEN:
    case PERIOD_PERIOD_TOKEN:
      return Classification.PUNCTUATION;

    case PLUS_PLUS_TOKEN:
    case MINUS_MINUS_TOKEN:
    case TILDE_TOKEN:
    case BANG_TOKEN:
    case EQ_TOKEN:
    case BAR_EQ_TOKEN:
    case CARET_EQ_TOKEN:
    case AMPERSAND_EQ_TOKEN:
    case LT_LT_EQ_TOKEN:
    case GT_GT_EQ_TOKEN:
    case PLUS_EQ_TOKEN:
    case MINUS_EQ_TOKEN:
    case STAR_EQ_TOKEN:
    case SLASH_EQ_TOKEN:
    case TILDE_SLASH_EQ_TOKEN:
    case PERCENT_EQ_TOKEN:
    case QUESTION_TOKEN:
    case BAR_BAR_TOKEN:
    case AMPERSAND_AMPERSAND_TOKEN:
    case BAR_TOKEN:
    case CARET_TOKEN:
    case AMPERSAND_TOKEN:
    case LT_LT_TOKEN:
    case GT_GT_TOKEN:
    case PLUS_TOKEN:
    case MINUS_TOKEN:
    case STAR_TOKEN:
    case SLASH_TOKEN:
    case TILDE_SLASH_TOKEN:
    case PERCENT_TOKEN:
    case EQ_EQ_TOKEN:
    case BANG_EQ_TOKEN:
    case EQ_EQ_EQ_TOKEN:
    case BANG_EQ_EQ_TOKEN:
    case LT_TOKEN:
    case GT_TOKEN:
    case LT_EQ_TOKEN:
    case GT_EQ_TOKEN:
    case INDEX_TOKEN:
    case INDEX_EQ_TOKEN:
      return Classification.OPERATOR;

    // Color keyword token. Most are colored as keywords.
    case HASH_TOKEN:
    case KEYWORD_TOKEN:
      if (token.stringValue == 'void') {
        // Color "void" as a type.
        return Classification.TYPE_IDENTIFIER;
      }
      if (token.stringValue == 'this' || token.stringValue == 'super') {
        // Color "this" and "super" as identifiers.
        return Classification.SPECIAL_IDENTIFIER;
      }
      return Classification.KEYWORD;

    case EOF_TOKEN:
      return Classification.NONE;

    default:
      return Classification.NONE;
  }
}
