// 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());
}
