content: // 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 dropdown;
  
  import 'dart:html';
  import 'search.dart';
  import 'client-shared.dart';
  import '../dartdoc/nav.dart';
  
  List libraryList;
  InputElement searchInput;
  DivElement dropdown;
  
  /**
   * Update the search drop down based on the current search text.
   */
  updateDropDown(Event event) {
    if (libraryList == null) return;
    if (searchInput == null) return;
    if (dropdown == null) return;
  
    var results = <Result>[];
    String text = searchInput.value;
    if (text == currentSearchText) {
      return;
    }
    if (text.isEmpty) {
      updateResults(text, results);
      hideDropDown();
      return;
    }
    if (text.contains('.')) {
      // Search type members.
      String typeText = text.substring(0, text.indexOf('.'));
      String memberText = text.substring(text.indexOf('.') + 1);
  
      if (typeText.isEmpty && memberText.isEmpty) {
        // Don't search on '.'.
      } else if (typeText.isEmpty) {
        // Search text is of the form '.id' => Look up members.
        matchAllMembers(results, memberText);
      } else if (memberText.isEmpty) {
        // Search text is of the form 'Type.' => Look up members in 'Type'.
        matchAllMembersInType(results, typeText, memberText);
      } else {
        // Search text is of the form 'Type.id' => Look up member 'id' in 'Type'.
        matchMembersInType(results, text, typeText, memberText);
      }
    } else {
      // Search all entities.
      var searchText = new SearchText(text);
      for (Map<String,dynamic> library in libraryList)  {
        matchLibrary(results, searchText, library);
        matchLibraryMembers(results, searchText, library);
        matchTypes(results, searchText, library);
      }
    }
    var elements = <Element>[];
    var table = new TableElement();
    table.classes.add('drop-down-table');
    elements.add(table);
  
    if (results.isEmpty) {
      var row = table.insertRow(0);
      row.innerHtml = "<tr><td>No matches found for '$text'.</td></tr>";
    } else {
      results.sort(resultComparator);
  
      var count = 0;
      for (Result result in results) {
        result.addRow(table);
        if (++count >= 10) {
          break;
        }
      }
      if (results.length >= 10) {
        var row = table.insertRow(table.rows.length);
        row.innerHtml = '<tr><td>+ ${results.length-10} more.</td></tr>';
        results = results.sublist(0, 10);
      }
    }
    dropdown.children = elements;
    updateResults(text, results);
    showDropDown();
  }
  
  void matchAllMembers(List<Result> results, String memberText) {
    var searchText = new SearchText(memberText);
    for (Map<String,dynamic> library in libraryList)  {
      String libraryName = library[NAME];
      if (library.containsKey(TYPES)) {
        for (Map<String,dynamic> type in library[TYPES]) {
          String typeName = type[NAME];
          if (type.containsKey(MEMBERS)) {
            for (Map<String,dynamic> member in type[MEMBERS]) {
              StringMatch memberMatch = obtainMatch(searchText, member[NAME]);
              if (memberMatch != null) {
                results.add(new Result(memberMatch, member[KIND],
                    getTypeMemberUrl(libraryName, typeName, member),
                    library: libraryName, type: typeName, args: type[ARGS],
                    noargs: member[NO_PARAMS]));
              }
            }
          }
        }
      }
    }
  }
  
  void matchAllMembersInType(List<Result> results,
                             String typeText, String memberText) {
    var searchText = new SearchText(typeText);
    var emptyText = new SearchText(memberText);
    for (Map<String,dynamic> library in libraryList)  {
      String libraryName = library[NAME];
      if (library.containsKey(TYPES)) {
        for (Map<String,dynamic> type in library[TYPES]) {
          String typeName = type[NAME];
          StringMatch typeMatch = obtainMatch(searchText, typeName);
          if (typeMatch != null) {
            if (type.containsKey(MEMBERS)) {
              for (Map<String,dynamic> member in type[MEMBERS]) {
                StringMatch memberMatch = obtainMatch(emptyText,
                    member[NAME]);
                results.add(new Result(memberMatch, member[KIND],
                    getTypeMemberUrl(libraryName, typeName, member),
                    library: libraryName, prefix: typeMatch,
                    noargs: member[NO_PARAMS]));
              }
            }
          }
        }
      }
    }
  }
  
  void matchMembersInType(List<Result> results,
                          String text, String typeText, String memberText) {
    var searchText = new SearchText(text);
    var typeSearchText = new SearchText(typeText);
    var memberSearchText = new SearchText(memberText);
    for (Map<String,dynamic> library in libraryList)  {
      String libraryName = library[NAME];
      if (library.containsKey(TYPES)) {
        for (Map<String,dynamic> type in library[TYPES]) {
          String typeName = type[NAME];
          StringMatch typeMatch = obtainMatch(typeSearchText, typeName);
          if (typeMatch != null) {
            if (type.containsKey(MEMBERS)) {
              for (Map<String,dynamic> member in type[MEMBERS]) {
                // Check for constructor match.
                StringMatch constructorMatch = obtainMatch(searchText,
                    member[NAME]);
                if (constructorMatch != null) {
                  results.add(new Result(constructorMatch, member[KIND],
                      getTypeMemberUrl(libraryName, typeName, member),
                      library: libraryName, noargs: member[NO_PARAMS]));
                } else {
                  // Try member match.
                  StringMatch memberMatch = obtainMatch(memberSearchText,
                      member[NAME]);
                  if (memberMatch != null) {
                    results.add(new Result(memberMatch, member[KIND],
                        getTypeMemberUrl(libraryName, typeName, member),
                        library: libraryName, prefix: typeMatch,
                        args: type[ARGS], noargs: member[NO_PARAMS]));
                  }
                }
              }
            }
          }
        }
      }
    }
  }
  
  void matchLibrary(List<Result> results, SearchText searchText, Map library) {
    String libraryName = library[NAME];
    StringMatch libraryMatch = obtainMatch(searchText, libraryName);
    if (libraryMatch != null) {
      results.add(new Result(libraryMatch, LIBRARY,
                             getLibraryUrl(libraryName)));
    }
  }
  
  void matchLibraryMembers(List<Result> results, SearchText searchText,
                            Map library) {
    if (library.containsKey(MEMBERS)) {
      String libraryName = library[NAME];
      for (Map<String,dynamic> member in library[MEMBERS]) {
        StringMatch memberMatch = obtainMatch(searchText, member[NAME]);
        if (memberMatch != null) {
          results.add(new Result(memberMatch, member[KIND],
                                 getLibraryMemberUrl(libraryName, member),
                                 library: libraryName, noargs: member[NO_PARAMS]));
        }
      }
    }
  }
  
  void matchTypes(List<Result> results, SearchText searchText,
                  Map library) {
    if (library.containsKey(TYPES)) {
      String libraryName = library[NAME];
      for (Map<String,dynamic> type in library[TYPES]) {
        String typeName = type[NAME];
        matchType(results, searchText, libraryName, type);
        matchTypeMembers(results, searchText, libraryName, type);
      }
    }
  }
  
  void matchType(List<Result> results, SearchText searchText,
                 String libraryName, Map type) {
    String typeName = type[NAME];
    StringMatch typeMatch = obtainMatch(searchText, typeName);
    if (typeMatch != null) {
      results.add(new Result(typeMatch, type[KIND],
                             getTypeUrl(libraryName, type),
                             library: libraryName, args: type[ARGS]));
    }
  }
  
  void matchTypeMembers(List<Result> results, SearchText searchText,
                        String libraryName, Map type) {
    if (type.containsKey(MEMBERS)) {
      String typeName = type[NAME];
      for (Map<String,dynamic> member in type[MEMBERS]) {
        StringMatch memberMatch = obtainMatch(searchText, member[NAME]);
        if (memberMatch != null) {
          results.add(new Result(memberMatch, member[KIND],
              getTypeMemberUrl(libraryName, typeName, member),
              library: libraryName, type: typeName, args: type[ARGS],
              noargs: member[NO_PARAMS]));
        }
      }
    }
  }
  
  String currentSearchText;
  Result _currentResult;
  List<Result> currentResults = const <Result>[];
  
  void updateResults(String searchText, List<Result> results) {
    currentSearchText = searchText;
    currentResults = results;
    if (currentResults.isEmpty) {
      _currentResultIndex = -1;
      currentResult = null;
    } else {
      _currentResultIndex = 0;
      currentResult = currentResults[0];
    }
  }
  
  int _currentResultIndex;
  
  void set currentResultIndex(int index) {
    if (index < -1) {
      return;
    }
    if (index >= currentResults.length) {
      return;
    }
    if (index != _currentResultIndex) {
      _currentResultIndex = index;
      if (index >= 0) {
        currentResult = currentResults[_currentResultIndex];
      } else {
        currentResult = null;
      }
    }
  }
  
  int get currentResultIndex => _currentResultIndex;
  
  void set currentResult(Result result) {
    if (_currentResult != result) {
      if (_currentResult != null) {
        _currentResult.row.classes.remove('drop-down-link-select');
      }
      _currentResult = result;
      if (_currentResult != null) {
        _currentResult.row.classes.add('drop-down-link-select');
      }
    }
  }
  
  Result get currentResult => _currentResult;
  
  /**
   * Navigate the search drop down using up/down inside the search field. Follow
   * the result link on enter.
   */
  void handleUpDown(KeyboardEvent event) {
    if (event.keyCode == KeyCode.UP) {
      currentResultIndex--;
      event.preventDefault();
    } else if (event.keyCode == KeyCode.DOWN) {
      currentResultIndex++;
      event.preventDefault();
    } else if (event.keyCode == KeyCode.ENTER) {
      if (currentResult != null) {
        window.location.href = currentResult.url;
        event.preventDefault();
        hideDropDown();
      }
    }
  }
  
  /** Show the search drop down unless there are no current results. */
  void showDropDown() {
    if (currentResults.isEmpty) {
      hideDropDown();
    } else {
      dropdown.style.visibility = 'visible';
    }
  }
  
  /** Used to prevent hiding the drop down when it is clicked. */
  bool hideDropDownSuspend = false;
  
  /** Hide the search drop down unless suspended. */
  void hideDropDown() {
    if (hideDropDownSuspend) return;
  
    dropdown.style.visibility = 'hidden';
  }
  
  /** Activate search on Ctrl+3 and S. */
  void shortcutHandler(KeyboardEvent event) {
    if (event.keyCode == KeyCode.THREE && event.ctrlKey) {
      searchInput.focus();
      event.preventDefault();
    } else if (event.target != searchInput && event.keyCode == KeyCode.S) {
      // Allow writing 's' in the search input.
      searchInput.focus();
      event.preventDefault();
    }
  }
  
  /**
   * Setup window shortcuts.
   */
  void setupShortcuts() {
    window.onKeyDown.listen(shortcutHandler);
  }
  
  /** Setup search hooks. */
  void setupSearch(var libraries) {
    libraryList = libraries;
    searchInput = query('#q');
    dropdown = query('#drop-down');
  
    searchInput.onKeyDown.listen(handleUpDown);
    searchInput.onKeyUp.listen(updateDropDown);
    searchInput.onChange.listen(updateDropDown);
    searchInput.onReset.listen(updateDropDown);
    searchInput.onFocus.listen((event) => showDropDown());
    searchInput.onBlur.listen((event) => hideDropDown());
  }
  