// Copyright 2015 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import 'dart:async' show Completer, FutureOr;

import 'package:matcher/expect.dart' as m;
import 'package:stack_trace/stack_trace.dart' show Chain;

const defaultInterval = Duration(milliseconds: 500);
const defaultTimeout = Duration(seconds: 5);

const clock = Clock();

Future<T> waitFor<T>(FutureOr<T> Function() condition,
        {Object? matcher,
        Duration timeout = defaultTimeout,
        Duration interval = defaultInterval,
        String? reason}) =>
    clock.waitFor<T>(condition,
        matcher: matcher, timeout: timeout, interval: interval, reason: reason);

class Clock {
  const Clock();

  /// Sleep for the specified time.
  Future<void> sleep([Duration interval = defaultInterval]) =>
      Future.delayed(interval);

  /// The current time.
  DateTime get now => DateTime.now();

  /// Waits until [condition] evaluates to a value that matches [matcher] or
  /// until [timeout] time has passed. If [condition] returns a [Future], then
  /// uses the value of that [Future] rather than the value of [condition].
  ///
  /// If the wait is successful, then the matching return value of [condition]
  /// is returned. Otherwise, if [condition] throws, then that exception is
  /// rethrown. If [condition] doesn't throw then an [expect] exception is
  /// thrown.
  ///
  /// [reason] is optional and is typically not supplied, as a reason is
  /// generated from [matcher]; if [reason] is included it is appended to the
  /// reason generated by the matcher.
  Future<T> waitFor<T>(FutureOr<T> Function() condition,
      {Object? matcher,
      Duration timeout = defaultTimeout,
      Duration interval = defaultInterval,
      String? reason}) async {
    final mMatcher = matcher == null ? null : m.wrapMatcher(matcher);
    final endTime = now.add(timeout);
    while (true) {
      try {
        final value = await condition();
        if (mMatcher != null) {
          _matcherExpect(value, mMatcher, reason);
        }
        return value;
      } catch (e) {
        if (now.isAfter(endTime)) {
          rethrow;
        } else {
          await sleep(interval);
        }
      }
    }
  }
}

void _matcherExpect(Object? value, m.Matcher matcher, String? reason) {
  final matchState = {};
  if (matcher.matches(value, matchState)) {
    return;
  }
  final desc = m.StringDescription()
    ..add('Expected: ')
    ..addDescriptionOf(matcher)
    ..add('\n')
    ..add('  Actual: ')
    ..addDescriptionOf(value)
    ..add('\n');

  final mismatchDescription = m.StringDescription();
  matcher.describeMismatch(value, mismatchDescription, matchState, true);
  if (mismatchDescription.length > 0) {
    desc.add('   Which: $mismatchDescription\n');
  }

  if (reason != null) {
    desc.add('$reason\n');
  }

  m.fail(desc.toString());
}

class Lock {
  Completer<void>? _lock;
  Chain? _stack;

  final bool awaitChecking;

  Lock({this.awaitChecking = false});

  Future<void> acquire() {
    if (awaitChecking) {
      if (isHeld) {
        return Future.error(StateError(
            'Maybe you missed an await? Lock is already held by:\n$_stack'));
      } else {
        _stack = Chain.current().terse;
        _lock = Completer();
        return Future.value();
      }
    } else {
      return () async {
        while (isHeld) {
          await _lock!.future;
        }
        _lock = Completer();
      }();
    }
  }

  void release() {
    if (!isHeld) {
      throw StateError('No lock to release');
    }
    _lock!.complete();
    _lock = null;
    _stack = null;
  }

  bool get isHeld => _lock != null;
}
