// 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 search;

import 'dart:html';
import 'dropdown.dart';
import '../dartdoc/nav.dart';

/**
 * [SearchText] represent the search field text. The text is viewed in three
 * ways: [text] holds the original search text, used for performing
 * case-sensitive matches, [lowerCase] holds the lower-case search text, used
 * for performing case-insenstive matches, [camelCase] holds a camel-case
 * interpretation of the search text, used to order matches in camel-case.
 */
class SearchText {
  final String text;
  final String lowerCase;
  final String camelCase;

  SearchText(String searchText)
      : text = searchText,
        lowerCase = searchText.toLowerCase(),
        camelCase = searchText.isEmpty ? ''
            : '${searchText.substring(0, 1).toUpperCase()}'
              '${searchText.substring(1)}';

  int get length => text.length;

  bool get isEmpty => length == 0;
}

/**
 * [StringMatch] represents the case-insensitive matching of [searchText] as a
 * substring within a [text].
 */
class StringMatch {
  final SearchText searchText;
  final String text;
  final int matchOffset;
  final int matchEnd;

  StringMatch(this.searchText,
              this.text, this.matchOffset, this.matchEnd);

  /**
   * Returns the HTML representation of the match.
   */
  String toHtml() {
    return '${text.substring(0, matchOffset)}'
           '<span class="drop-down-link-highlight">$matchText</span>'
           '${text.substring(matchEnd)}';
  }

  String get matchText =>
      text.substring(matchOffset, matchEnd);

  /**
   * Is [:true:] iff [searchText] matches the full [text] case-sensitively.
   */
  bool get isFullMatch => text == searchText.text;

  /**
   * Is [:true:] iff [searchText] matches a substring of [text]
   * case-sensitively.
   */
  bool get isExactMatch => matchText == searchText.text;

  /**
   * Is [:true:] iff [searchText] matches a substring of [text] when
   * [searchText] is interpreted as camel case.
   */
  bool get isCamelCaseMatch => matchText == searchText.camelCase;
}

/**
 * [Result] represents a match of the search text on a library, type or member.
 */
class Result {
  final StringMatch prefix;
  final StringMatch match;

  final String library;
  final String type;
  final String args;
  final String kind;
  final String url;
  final bool noargs;

  TableRowElement row;

  Result(this.match, this.kind, this.url,
         {this.library: null, this.type: null, String args: null,
          this.prefix: null, this.noargs: false})
      : this.args = args != null ? '&lt;$args&gt;' : '';

  bool get isTopLevel => prefix == null && type == null;

  void addRow(TableElement table) {
    if (row != null) return;

    clickHandler(Event event) {
      window.location.href = url;
      hideDropDown();
    }

    row = table.insertRow(table.rows.length);
    row.classes.add('drop-down-link-tr');
    row.onMouseDown.listen((event) => hideDropDownSuspend = true);
    row.onClick.listen(clickHandler);
    row.onMouseUp.listen((event) => hideDropDownSuspend = false);
    var sb = new StringBuffer();
    sb.write('<td class="drop-down-link-td">');
    sb.write('<table class="drop-down-table"><tr><td colspan="2">');
    if (kind == GETTER) {
      sb.write('get ');
    } else if (kind == SETTER) {
      sb.write('set ');
    }
    sb.write(match.toHtml());
    if (kind == CLASS || kind == TYPEDEF) {
      sb.write(args);
    } else if (kind == CONSTRUCTOR || kind == METHOD) {
      if (noargs) {
        sb.write("()");
      } else {
        sb.write('(...)');
      }
    }
    sb.write('</td></tr><tr><td class="drop-down-link-kind">');
    sb.write(kindToString(kind));
    if (prefix != null) {
      sb.write(' in ');
      sb.write(prefix.toHtml());
      sb.write(args);
    } else if (type != null) {
      sb.write(' in ');
      sb.write(type);
      sb.write(args);
    }

    sb.write('</td><td class="drop-down-link-library">');
    if (library != null) {
      sb.write('library $library');
    }
    sb.write('</td></tr></table></td>');
    row.innerHtml = sb.toString();
  }
}

/**
 * Creates a [StringMatch] object for [text] if a substring matches
 * [searchText], or returns [: null :] if no match is found.
 */
StringMatch obtainMatch(SearchText searchText, String text) {
  if (searchText.isEmpty) {
    return new StringMatch(searchText, text, 0, 0);
  }
  int offset = text.toLowerCase().indexOf(searchText.lowerCase);
  if (offset != -1) {
    return new StringMatch(searchText, text,
                           offset, offset + searchText.length);
  }
  return null;
}

/**
 * Compares [a] and [b], regarding [:true:] smaller than [:false:].
 *
 * [:null:]-values are not handled.
 */
int compareBools(bool a, bool b) {
  if (a == b) return 0;
  return a ? -1 : 1;
}

/**
 * Used to sort the search results heuristically to show the more relevant match
 * in the top of the dropdown.
 */
int resultComparator(Result a, Result b) {
  // Favor top level entities.
  int result = compareBools(a.isTopLevel, b.isTopLevel);
  if (result != 0) return result;

  if (a.prefix != null && b.prefix != null) {
    // Favor full prefix matches.
    result = compareBools(a.prefix.isFullMatch, b.prefix.isFullMatch);
    if (result != 0) return result;
  }

  // Favor matches in the start.
  result = compareBools(a.match.matchOffset == 0,
                        b.match.matchOffset == 0);
  if (result != 0) return result;

  // Favor matches to the end. For example, prefer 'cancel' over 'cancelable'
  result = compareBools(a.match.matchEnd == a.match.text.length,
                        b.match.matchEnd == b.match.text.length);
  if (result != 0) return result;

  // Favor exact case-sensitive matches.
  result = compareBools(a.match.isExactMatch, b.match.isExactMatch);
  if (result != 0) return result;

  // Favor matches that do not break camel-case.
  result = compareBools(a.match.isCamelCaseMatch, b.match.isCamelCaseMatch);
  if (result != 0) return result;

  // Favor matches close to the begining.
  result = a.match.matchOffset.compareTo(b.match.matchOffset);
  if (result != 0) return result;

  if (a.type != null && b.type != null) {
    // Favor short type names over long.
    result = a.type.length.compareTo(b.type.length);
    if (result != 0) return result;

    // Sort type alphabetically.
    // TODO(4805): Use [:type.compareToIgnoreCase] when supported.
    result = a.type.toLowerCase().compareTo(b.type.toLowerCase());
    if (result != 0) return result;
  }

  // Sort match alphabetically.
  // TODO(4805): Use [:text.compareToIgnoreCase] when supported.
  return a.match.text.toLowerCase().compareTo(b.match.text.toLowerCase());
}
