// 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 within [delta]
/// of some [value].
///
/// In other words, this matches if the match argument is greater than
/// than or equal [value]-[delta] and less than or equal to [value]+[delta].
Matcher closeTo(num value, num delta) => _IsCloseTo(value, delta);

class _IsCloseTo extends FeatureMatcher<num> {
  final num _value, _delta;

  const _IsCloseTo(this._value, this._delta);

  @override
  bool typedMatches(num item, Map matchState) {
    var diff = item - _value;
    if (diff < 0) diff = -diff;
    return diff <= _delta;
  }

  @override
  Description describe(Description description) => description
      .add('a numeric value within ')
      .addDescriptionOf(_delta)
      .add(' of ')
      .addDescriptionOf(_value);

  @override
  Description describeTypedMismatch(
    num item,
    Description mismatchDescription,
    Map matchState,
    bool verbose,
  ) {
    var diff = item - _value;
    if (diff < 0) diff = -diff;
    return mismatchDescription.add(' differs by ').addDescriptionOf(diff);
  }
}

/// Returns a matcher which matches if the match argument is greater
/// than or equal to [low] and less than or equal to [high].
Matcher inInclusiveRange(num low, num high) => _InRange(low, high, true, true);

/// Returns a matcher which matches if the match argument is greater
/// than [low] and less than [high].
Matcher inExclusiveRange(num low, num high) =>
    _InRange(low, high, false, false);

/// Returns a matcher which matches if the match argument is greater
/// than [low] and less than or equal to [high].
Matcher inOpenClosedRange(num low, num high) =>
    _InRange(low, high, false, true);

/// Returns a matcher which matches if the match argument is greater
/// than or equal to a [low] and less than [high].
Matcher inClosedOpenRange(num low, num high) =>
    _InRange(low, high, true, false);

class _InRange extends FeatureMatcher<num> {
  final num _low, _high;
  final bool _lowMatchValue, _highMatchValue;

  const _InRange(
    this._low,
    this._high,
    this._lowMatchValue,
    this._highMatchValue,
  );

  @override
  bool typedMatches(num value, Map matchState) {
    if (value < _low || value > _high) {
      return false;
    }
    if (value == _low) {
      return _lowMatchValue;
    }
    if (value == _high) {
      return _highMatchValue;
    }
    // Value may still be outside if range if it can't be compared.
    return value > _low && value < _high;
  }

  @override
  Description describe(Description description) => description.add(
    'be in range from '
    "$_low (${_lowMatchValue ? 'inclusive' : 'exclusive'}) to "
    "$_high (${_highMatchValue ? 'inclusive' : 'exclusive'})",
  );
}
