// 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.

import 'feature_matcher.dart';
import 'interfaces.dart';

/// Returns a matcher which matches if the match argument is a string and
/// is equal to [value] when compared case-insensitively.
Matcher equalsIgnoringCase(String value) => _IsEqualIgnoringCase(value);

class _IsEqualIgnoringCase extends FeatureMatcher<String> {
  final String _value;
  final String _matchValue;

  _IsEqualIgnoringCase(String value)
    : _value = value,
      _matchValue = value.toLowerCase();

  @override
  bool typedMatches(String item, Map matchState) =>
      _matchValue == item.toLowerCase();

  @override
  Description describe(Description description) =>
      description.addDescriptionOf(_value).add(' ignoring case');
}

/// Returns a matcher which matches if the match argument is a string and
/// is equal to [value], ignoring whitespace.
///
/// In this matcher, "ignoring whitespace" means comparing with all runs of
/// whitespace collapsed to single space characters and leading and trailing
/// whitespace removed.
///
/// For example, the following will all match successfully:
///
///     expect("hello   world", equalsIgnoringWhitespace("hello world"));
///     expect("  hello world", equalsIgnoringWhitespace("hello world"));
///     expect("hello world  ", equalsIgnoringWhitespace("hello world"));
///
/// The following will not match:
///
///     expect("helloworld", equalsIgnoringWhitespace("hello world"));
///     expect("he llo world", equalsIgnoringWhitespace("hello world"));
Matcher equalsIgnoringWhitespace(String value) =>
    _IsEqualIgnoringWhitespace(value);

class _IsEqualIgnoringWhitespace extends FeatureMatcher<String> {
  final String _matchValue;

  _IsEqualIgnoringWhitespace(String value)
    : _matchValue = collapseWhitespace(value);

  @override
  bool typedMatches(String item, Map matchState) =>
      _matchValue == collapseWhitespace(item);

  @override
  Description describe(Description description) =>
      description.addDescriptionOf(_matchValue).add(' ignoring whitespace');

  @override
  Description describeTypedMismatch(
    String item,
    Description mismatchDescription,
    Map matchState,
    bool verbose,
  ) {
    return mismatchDescription
        .add('is ')
        .addDescriptionOf(collapseWhitespace(item))
        .add(' with whitespace compressed');
  }
}

/// Returns a matcher that matches if the match argument is a string and
/// starts with [prefixString].
Matcher startsWith(String prefixString) => _StringStartsWith(prefixString);

class _StringStartsWith extends FeatureMatcher<String> {
  final String _prefix;

  const _StringStartsWith(this._prefix);

  @override
  bool typedMatches(String item, Map matchState) => item.startsWith(_prefix);

  @override
  Description describe(Description description) =>
      description.add('a string starting with ').addDescriptionOf(_prefix);
}

/// Returns a matcher that matches if the match argument is a string and
/// ends with [suffixString].
Matcher endsWith(String suffixString) => _StringEndsWith(suffixString);

class _StringEndsWith extends FeatureMatcher<String> {
  final String _suffix;

  const _StringEndsWith(this._suffix);

  @override
  bool typedMatches(String item, Map matchState) => item.endsWith(_suffix);

  @override
  Description describe(Description description) =>
      description.add('a string ending with ').addDescriptionOf(_suffix);
}

/// Returns a matcher that matches if the match argument is a string and
/// contains a given list of [substrings] in relative order.
///
/// For example, `stringContainsInOrder(["a", "e", "i", "o", "u"])` will match
/// "abcdefghijklmnopqrstuvwxyz".

Matcher stringContainsInOrder(List<String> substrings) =>
    _StringContainsInOrder(substrings);

class _StringContainsInOrder extends FeatureMatcher<String> {
  final List<String> _substrings;

  const _StringContainsInOrder(this._substrings);

  @override
  bool typedMatches(String item, Map matchState) {
    var fromIndex = 0;
    for (var s in _substrings) {
      var index = item.indexOf(s, fromIndex);
      if (index < 0) return false;
      fromIndex = index + s.length;
    }
    return true;
  }

  @override
  Description describe(Description description) => description.addAll(
    'a string containing ',
    ', ',
    ' in order',
    _substrings,
  );
}

/// Returns a matcher that matches if the match argument is a string and
/// matches the regular expression given by [re].
///
/// [re] can be a [RegExp] instance or a [String]; in the latter case it will be
/// used to create a RegExp instance.
Matcher matches(Pattern re) => _MatchesRegExp(re);

class _MatchesRegExp extends FeatureMatcher<String> {
  final RegExp _regexp;

  _MatchesRegExp(Pattern re)
    : _regexp =
          (re is String)
              ? RegExp(re)
              : (re is RegExp)
              ? re
              : throw ArgumentError('matches requires a regexp or string');

  @override
  bool typedMatches(String item, Map matchState) => _regexp.hasMatch(item);

  @override
  Description describe(Description description) =>
      description.add("match '${_regexp.pattern}'");
}

/// Utility function to collapse whitespace runs to single spaces
/// and strip leading/trailing whitespace.
String collapseWhitespace(String string) {
  var result = StringBuffer();
  var skipSpace = true;
  for (var i = 0; i < string.length; i++) {
    var character = string[i];
    if (_isWhitespace(character)) {
      if (!skipSpace) {
        result.write(' ');
        skipSpace = true;
      }
    } else {
      result.write(character);
      skipSpace = false;
    }
  }
  return result.toString().trim();
}

bool _isWhitespace(String ch) =>
    ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t';
