// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

import 'package:flutter/animation.dart';

import 'basic.dart';
import 'focus.dart';
import 'framework.dart';
import 'modal_barrier.dart';
import 'navigator.dart';
import 'overlay.dart';
import 'page_storage.dart';
import 'pages.dart';

const _kTransparent = const Color(0x00000000);

/// A route that displays widgets in the [Navigator]'s [Overlay].
abstract class OverlayRoute<T> extends Route<T> {
  /// Subclasses should override this getter to return the builders for the overlay.
  List<WidgetBuilder> get builders;

  /// The entries this route has placed in the overlay.
  List<OverlayEntry> get overlayEntries => _overlayEntries;
  final List<OverlayEntry> _overlayEntries = <OverlayEntry>[];

  void install(OverlayEntry insertionPoint) {
    assert(_overlayEntries.isEmpty);
    for (WidgetBuilder builder in builders)
      _overlayEntries.add(new OverlayEntry(builder: builder));
    navigator.overlay?.insertAll(_overlayEntries, above: insertionPoint);
  }

  /// A request was made to pop this route. If the route can handle it
  /// internally (e.g. because it has its own stack of internal state) then
  /// return false, otherwise return true. Returning false will prevent the
  /// default behavior of NavigatorState.pop().
  ///
  /// If this is called, the Navigator will not call dispose(). It is the
  /// responsibility of the Route to later call dispose().
  ///
  /// Subclasses shouldn't call this if they want to delay the finished() call.
  bool didPop(T result) {
    finished();
    return true;
  }

  /// Clears out the overlay entries.
  ///
  /// This method is intended to be used by subclasses who don't call
  /// super.didPop() because they want to have control over the timing of the
  /// overlay removal.
  ///
  /// Do not call this method outside of this context.
  void finished() {
    for (OverlayEntry entry in _overlayEntries)
      entry.remove();
    _overlayEntries.clear();
  }

  void dispose() {
    finished();
  }
}

abstract class TransitionRoute<T> extends OverlayRoute<T> {
  TransitionRoute({
    Completer<T> popCompleter,
    Completer<T> transitionCompleter
  }) : _popCompleter = popCompleter,
       _transitionCompleter = transitionCompleter;

  TransitionRoute.explicit(
    Completer<T> popCompleter,
    Completer<T> transitionCompleter
  ) : this(popCompleter: popCompleter, transitionCompleter: transitionCompleter);

  /// This future completes once the animation has been dismissed. For
  /// ModalRoutes, this will be after the completer that's passed in, since that
  /// one completes before the animation even starts, as soon as the route is
  /// popped.
  Future<T> get popped => _popCompleter?.future;
  final Completer<T> _popCompleter;

  /// This future completes only once the transition itself has finished, after
  /// the overlay entries have been removed from the navigator's overlay.
  Future<T> get completed => _transitionCompleter?.future;
  final Completer<T> _transitionCompleter;

  Duration get transitionDuration;
  bool get opaque;

  PerformanceView get performance => _performance;
  Performance _performanceController;
  PerformanceView _performance;

  /// Called to create the Performance object that will drive the transitions to
  /// this route from the previous one, and back to the previous route from this
  /// one.
  Performance createPerformanceController() {
    Duration duration = transitionDuration;
    assert(duration != null && duration >= Duration.ZERO);
    return new Performance(duration: duration, debugLabel: debugLabel);
  }

  /// Called to create the PerformanceView that exposes the current progress of
  /// the transition controlled by the Performance object created by
  /// [createPerformanceController()].
  PerformanceView createPerformance() {
    assert(_performanceController != null);
    return _performanceController.view;
  }

  T _result;

  void handleStatusChanged(PerformanceStatus status) {
    switch (status) {
      case PerformanceStatus.completed:
        if (overlayEntries.isNotEmpty)
          overlayEntries.first.opaque = opaque;
        break;
      case PerformanceStatus.forward:
      case PerformanceStatus.reverse:
        if (overlayEntries.isNotEmpty)
          overlayEntries.first.opaque = false;
        break;
      case PerformanceStatus.dismissed:
        assert(!overlayEntries.first.opaque);
        finished(); // clear the overlays
        assert(overlayEntries.isEmpty);
        break;
    }
  }

  PerformanceView get forwardPerformance => _forwardPerformance;
  final ProxyPerformance _forwardPerformance = new ProxyPerformance(alwaysDismissedPerformance);

  void install(OverlayEntry insertionPoint) {
    _performanceController = createPerformanceController();
    assert(_performanceController != null);
    _performance = createPerformance();
    assert(_performance != null);
    super.install(insertionPoint);
  }

  void didPush() {
    _performance.addStatusListener(handleStatusChanged);
    _performanceController.forward();
    super.didPush();
  }

  void didReplace(Route oldRoute) {
    if (oldRoute is TransitionRoute)
      _performanceController.progress = oldRoute._performanceController.progress;
    _performance.addStatusListener(handleStatusChanged);
    super.didReplace(oldRoute);
  }

  bool didPop(T result) {
    _result = result;
    _performanceController.reverse();
    _popCompleter?.complete(_result);
    return true;
  }

  void didPopNext(Route nextRoute) {
    _updateForwardPerformance(nextRoute);
    super.didPopNext(nextRoute);
  }

  void didChangeNext(Route nextRoute) {
    _updateForwardPerformance(nextRoute);
    super.didChangeNext(nextRoute);
  }

  void _updateForwardPerformance(Route nextRoute) {
    if (nextRoute is TransitionRoute && canTransitionTo(nextRoute) && nextRoute.canTransitionFrom(this)) {
      PerformanceView current = _forwardPerformance.masterPerformance;
      if (current != null) {
        if (current is TrainHoppingPerformance) {
          TrainHoppingPerformance newPerformance;
          newPerformance = new TrainHoppingPerformance(
            current.currentTrain,
            nextRoute.performance,
            onSwitchedTrain: () {
              assert(_forwardPerformance.masterPerformance == newPerformance);
              assert(newPerformance.currentTrain == nextRoute.performance);
              _forwardPerformance.masterPerformance = newPerformance.currentTrain;
              newPerformance.dispose();
            }
          );
          _forwardPerformance.masterPerformance = newPerformance;
          current.dispose();
        } else {
          _forwardPerformance.masterPerformance = new TrainHoppingPerformance(current, nextRoute.performance);
        }
      } else {
        _forwardPerformance.masterPerformance = nextRoute.performance;
      }
    } else {
      _forwardPerformance.masterPerformance = alwaysDismissedPerformance;
    }
  }

  bool canTransitionTo(TransitionRoute nextRoute) => true;
  bool canTransitionFrom(TransitionRoute nextRoute) => true;

  void finished() {
    super.finished();
    _transitionCompleter?.complete(_result);
  }

  void dispose() {
    _performanceController.stop();
    super.dispose();
  }

  String get debugLabel => '$runtimeType';
  String toString() => '$runtimeType(performance: $_performanceController)';
}

class LocalHistoryEntry {
  LocalHistoryEntry({ this.onRemove });
  final VoidCallback onRemove;
  LocalHistoryRoute _owner;
  void remove() {
    _owner.removeLocalHistoryEntry(this);
    assert(_owner == null);
  }
  void _notifyRemoved() {
    if (onRemove != null)
      onRemove();
  }
}

abstract class LocalHistoryRoute<T> extends Route<T> {
  List<LocalHistoryEntry> _localHistory;
  void addLocalHistoryEntry(LocalHistoryEntry entry) {
    assert(entry._owner == null);
    entry._owner = this;
    _localHistory ??= <LocalHistoryEntry>[];
    _localHistory.add(entry);
  }
  void removeLocalHistoryEntry(LocalHistoryEntry entry) {
    assert(entry != null);
    assert(entry._owner == this);
    assert(_localHistory.contains(entry));
    _localHistory.remove(entry);
    entry._owner = null;
    entry._notifyRemoved();
  }
  bool didPop(T result) {
    if (_localHistory != null && _localHistory.length > 0) {
      LocalHistoryEntry entry = _localHistory.removeLast();
      assert(entry._owner == this);
      entry._owner = null;
      entry._notifyRemoved();
      return false;
    }
    return super.didPop(result);
  }
  bool get willHandlePopInternally {
    return _localHistory != null && _localHistory.length > 0;
  }
}

class _ModalScopeStatus extends InheritedWidget {
  _ModalScopeStatus({
    Key key,
    this.isCurrent,
    this.route,
    Widget child
  }) : super(key: key, child: child) {
    assert(isCurrent != null);
    assert(route != null);
    assert(child != null);
  }

  final bool isCurrent;
  final Route route;

  bool updateShouldNotify(_ModalScopeStatus old) {
    return isCurrent != old.isCurrent ||
           route != old.route;
  }

  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('${isCurrent ? "active" : "inactive"}');
  }
}

class _ModalScope extends StatefulComponent {
  _ModalScope({
    Key key,
    this.route
  }) : super(key: key);

  final ModalRoute route;

  _ModalScopeState createState() => new _ModalScopeState();
}

class _ModalScopeState extends State<_ModalScope> {
  void initState() {
    super.initState();
    config.route.performance?.addStatusListener(_performanceStatusChanged);
    config.route.forwardPerformance?.addStatusListener(_performanceStatusChanged);
  }

  void didUpdateConfig(_ModalScope oldConfig) {
    assert(config.route == oldConfig.route);
  }

  void dispose() {
    config.route.performance?.removeStatusListener(_performanceStatusChanged);
    config.route.forwardPerformance?.removeStatusListener(_performanceStatusChanged);
    super.dispose();
  }

  void _performanceStatusChanged(PerformanceStatus status) {
    setState(() {
      // The performances' states are our build state, and they changed already.
    });
  }

  Widget build(BuildContext context) {
    Widget contents = new PageStorage(
      key: config.route._subtreeKey,
      bucket: config.route._storageBucket,
      child: new _ModalScopeStatus(
        route: config.route,
        isCurrent: config.route.isCurrent,
        child: config.route.buildPage(context, config.route.performance, config.route.forwardPerformance)
      )
    );
    if (config.route.offstage) {
      contents = new OffStage(child: contents);
    } else {
      contents = new Focus(
        key: new GlobalObjectKey(config.route),
        child: new IgnorePointer(
          ignoring: config.route.performance?.status == PerformanceStatus.reverse,
          child: config.route.buildTransitions(
            context,
            config.route.performance,
            config.route.forwardPerformance,
            contents
          )
        )
      );
    }
    contents = new RepaintBoundary(child: contents);
    ModalPosition position = config.route.getPosition(context);
    if (position == null)
      return contents;
    return new Positioned(
      top: position.top,
      right: position.right,
      bottom: position.bottom,
      left: position.left,
      child: contents
    );
  }
}

class ModalPosition {
  const ModalPosition({ this.top, this.right, this.bottom, this.left });
  final double top;
  final double right;
  final double bottom;
  final double left;
}

abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T> {
  ModalRoute({
    Completer<T> completer,
    this.settings: const RouteSettings()
  }) : super.explicit(completer, null);

  // The API for general users of this class

  final RouteSettings settings;

  /// Returns the modal route most closely associated with the given context.
  ///
  /// Returns null if the given context is not associated with a modal route.
  static ModalRoute of(BuildContext context) {
    _ModalScopeStatus widget = context.inheritFromWidgetOfExactType(_ModalScopeStatus);
    return widget?.route;
  }


  // The API for subclasses to override - used by _ModalScope

  ModalPosition getPosition(BuildContext context) => null;
  Widget buildPage(BuildContext context, PerformanceView performance, PerformanceView forwardPerformance);
  Widget buildTransitions(BuildContext context, PerformanceView performance, PerformanceView forwardPerformance, Widget child) {
    return child;
  }


  // The API for subclasses to override - used by this class

  /// Whether you can dismiss this route by tapping the modal barrier.
  bool get barrierDismissable;
  /// The color to use for the modal barrier. If this is null, the barrier will
  /// be transparent.
  Color get barrierColor;


  // The API for _ModalScope and HeroController

  bool get offstage => _offstage;
  bool _offstage = false;
  void set offstage (bool value) {
    if (_offstage == value)
      return;
    _offstage = value;
    _scopeKey.currentState?.setState(() {
      // _offstage is the value we're setting, but since there might not be a
      // state, we set it outside of this callback (which will only be called if
      // there's a state currently built).
      // _scopeKey is the key for the _ModalScope built in _buildModalScope().
      // When we mark that state dirty, it'll rebuild itself, and use our
      // offstage (via their config.route.offstage) when building.
    });
  }

  BuildContext get subtreeContext => _subtreeKey.currentContext;


  // Internals

  final GlobalKey<_ModalScopeState> _scopeKey = new GlobalKey<_ModalScopeState>();
  final GlobalKey _subtreeKey = new GlobalKey();
  final PageStorageBucket _storageBucket = new PageStorageBucket();

  Widget _buildModalBarrier(BuildContext context) {
    Widget barrier;
    if (barrierColor != null) {
      assert(barrierColor != _kTransparent);
      barrier = new AnimatedModalBarrier(
        color: new AnimatedColorValue(_kTransparent, end: barrierColor, curve: Curves.ease),
        performance: performance,
        dismissable: barrierDismissable
      );
    } else {
      barrier = new ModalBarrier(dismissable: barrierDismissable);
    }
    assert(performance.status != PerformanceStatus.dismissed);
    return new IgnorePointer(
      ignoring: performance.status == PerformanceStatus.reverse,
      child: barrier
    );
  }

  Widget _buildModalScope(BuildContext context) {
    return new _ModalScope(
      key: _scopeKey,
      route: this
      // calls buildTransitions() and buildPage(), defined above
    );
  }

  List<WidgetBuilder> get builders => <WidgetBuilder>[
    _buildModalBarrier,
    _buildModalScope
  ];

  String toString() => '$runtimeType($settings, performance: $_performance)';
}

/// A modal route that overlays a widget over the current route.
abstract class PopupRoute<T> extends ModalRoute<T> {
  PopupRoute({ Completer<T> completer }) : super(completer: completer);
  bool get opaque => false;
  void didChangeNext(Route nextRoute) {
    assert(nextRoute is! PageRoute);
    super.didChangeNext(nextRoute);
  }
}