// 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:collection';

import 'basic.dart';
import 'binding.dart';
import 'framework.dart';
import 'navigator.dart';
import 'overlay.dart';
import 'pages.dart';
import 'transitions.dart';

// Heroes are the parts of an application's screen-to-screen transitions where a
// widget from one screen shifts to a position on the other. For example,
// album art from a list of albums growing to become the centerpiece of the
// album's details view. In this context, a screen is a navigator ModalRoute.

// To get this effect, all you have to do is wrap each hero on each route with a
// Hero widget, and give each hero a tag. Tag must either be unique within the
// current route's widget subtree, or all the Heroes with that tag on a
// particular route must have a key. When the app transitions from one route to
// another, each tag present is animated. When there's exactly one hero with
// that tag, that hero will be animated for that tag. When there are multiple
// heroes in a route with the same tag, then whichever hero has a key that
// matches one of the keys in the "most important key" list given to the
// navigator when the route was pushed will be animated. If a hero is only
// present on one of the routes and not the other, then it will be made to
// appear or disappear as needed.

// TODO(ianh): Make the appear/disappear animations pretty. Right now they're
// pretty crude (just rotate and shrink the constraints). They should probably
// involve actually scaling and fading, at a minimum.

// Heroes and the Navigator's Stack must be axis-aligned for all this to work.
// The top left and bottom right coordinates of each animated Hero will be
// converted to global coordinates and then from there converted to the
// Navigator Stack's coordinate space, and the entire Hero subtree will, for the
// duration of the animation, be lifted out of its original place, and
// positioned on that stack. If the Hero isn't axis aligned, this is going to
// fail in a rather ugly fashion. Don't rotate your heroes!

// To make the animations look good, it's critical that the widget tree for the
// hero in both locations be essentially identical. The widget of the target is
// used to do the transition: when going from route A to route B, route B's
// hero's widget is placed over route A's hero's widget, and route A's hero is
// hidden. Then the widget is animated to route B's hero's position, and then
// the widget is inserted into route B. When going back from B to A, route A's
// hero's widget is placed over where route B's hero's widget was, and then the
// animation goes the other way.

// TODO(ianh): If the widgets use Inherited properties, they are taken from the
// Navigator's position in the widget hierarchy, not the source or target. We
// should interpolate the inherited properties from their value at the source to
// their value at the target. See: https://github.com/flutter/flutter/issues/213

class _HeroManifest {
  const _HeroManifest({
    this.key,
    this.config,
    this.sourceStates,
    this.currentRect,
    this.currentTurns
  });
  final GlobalKey key;
  final Widget config;
  final Set<HeroState> sourceStates;
  final RelativeRect currentRect;
  final double currentTurns;
}

abstract class HeroHandle {
  bool get alwaysAnimate;
  _HeroManifest _takeChild(Rect animationArea, Animation<double> currentAnimation);
}

class Hero extends StatefulWidget {
  Hero({
    Key key,
    this.tag,
    this.child,
    this.turns: 1,
    this.alwaysAnimate: false
  }) : super(key: key) {
    assert(tag != null);
  }

  final Object tag;

  /// The widget below this widget in the tree.
  final Widget child;

  final int turns;

  /// If true, the hero will always animate, even if it has no matching hero to
  /// animate to or from. (This only applies if the hero is relevant; if there
  /// are multiple heroes with the same tag, only the one whose key matches the
  /// "most valuable keys" will be used.)
  final bool alwaysAnimate;

  static Map<Object, HeroHandle> of(BuildContext context, Set<Key> mostValuableKeys) {
    mostValuableKeys ??= new HashSet<Key>();
    assert(!mostValuableKeys.contains(null));
    // first we collect ALL the heroes, sorted by their tags
    Map<Object, Map<Key, HeroState>> heroes = <Object, Map<Key, HeroState>>{};
    void visitor(Element element) {
      if (element.widget is Hero) {
        StatefulElement hero = element;
        Hero heroWidget = element.widget;
        Object tag = heroWidget.tag;
        assert(tag != null);
        Key key = heroWidget.key;
        final Map<Key, HeroState> tagHeroes = heroes.putIfAbsent(tag, () => <Key, HeroState>{});
        assert(() {
          if (tagHeroes.containsKey(key)) {
            new FlutterError(
              'There are multiple heroes that share the same key within the same subtree.\n'
              'Within each subtree for which heroes are to be animated (typically a PageRoute subtree), '
              'either each Hero must have a unique tag, or, all the heroes with a particular tag must '
              'have different keys.\n'
              'In this case, the tag "$tag" had multiple heroes with the key "$key".'
            );
          }
          return true;
        });
        tagHeroes[key] = hero.state;
      }
      element.visitChildren(visitor);
    }
    context.visitChildElements(visitor);
    // next, for each tag, we're going to decide on the one hero we care about for that tag
    Map<Object, HeroHandle> result = <Object, HeroHandle>{};
    for (Object tag in heroes.keys) {
      assert(tag != null);
      if (heroes[tag].length == 1) {
        result[tag] = heroes[tag].values.first;
      } else {
        assert(heroes[tag].length > 1);
        assert(!heroes[tag].containsKey(null));
        assert(heroes[tag].keys.where((Key key) => mostValuableKeys.contains(key)).length <= 1);
        Key mostValuableKey = mostValuableKeys.firstWhere((Key key) => heroes[tag].containsKey(key), orElse: () => null);
        if (mostValuableKey != null)
          result[tag] = heroes[tag][mostValuableKey];
      }
    }
    assert(!result.containsKey(null));
    return result;
  }

  @override
  HeroState createState() => new HeroState();
}

class HeroState extends State<Hero> implements HeroHandle {

  GlobalKey _key = new GlobalKey();
  Size _placeholderSize;

  @override
  bool get alwaysAnimate => config.alwaysAnimate;

  @override
  _HeroManifest _takeChild(Rect animationArea, Animation<double> currentAnimation) {
    assert(mounted);
    final RenderBox renderObject = context.findRenderObject();
    assert(renderObject != null);
    assert(!renderObject.needsLayout);
    assert(renderObject.hasSize);
    if (_placeholderSize == null) {
      // We are a "from" hero, about to depart on a quest.
      // Remember our size so that we can leave a placeholder.
      _placeholderSize = renderObject.size;
    }
    final Point heroTopLeft = renderObject.localToGlobal(Point.origin);
    final Point heroBottomRight = renderObject.localToGlobal(renderObject.size.bottomRight(Point.origin));
    final Rect heroArea = new Rect.fromLTRB(heroTopLeft.x, heroTopLeft.y, heroBottomRight.x, heroBottomRight.y);
    final RelativeRect startRect = new RelativeRect.fromRect(heroArea, animationArea);
    _HeroManifest result = new _HeroManifest(
      key: _key, // might be null, e.g. if the hero is returning to us
      config: config,
      sourceStates: new HashSet<HeroState>.from(<HeroState>[this]),
      currentRect: startRect,
      currentTurns: config.turns.toDouble()
    );
    if (_key != null)
      setState(() { _key = null; });
    return result;
  }

  void _setChild(GlobalKey value) {
    assert(_key == null);
    assert(_placeholderSize != null);
    assert(mounted);
    setState(() {
      _key = value;
      _placeholderSize = null;
    });
  }

  void _resetChild() {
    if (mounted)
      _setChild(null);
  }

  @override
  Widget build(BuildContext context) {
    if (_placeholderSize != null) {
      assert(_key == null);
      return new SizedBox(width: _placeholderSize.width, height: _placeholderSize.height);
    }
    return new KeyedSubtree(
      key: _key,
      child: config.child
    );
  }

}


class _HeroQuestState implements HeroHandle {
  _HeroQuestState({
    this.tag,
    this.key,
    this.child,
    this.sourceStates,
    this.targetRect,
    this.targetTurns,
    this.targetState,
    this.currentRect,
    this.currentTurns
  }) {
    assert(tag != null);
  }

  final Object tag;
  final GlobalKey key;
  final Widget child;
  final Set<HeroState> sourceStates;
  final RelativeRect targetRect;
  final int targetTurns;
  final HeroState targetState;
  final RelativeRectTween currentRect;
  final Tween<double> currentTurns;

  @override
  bool get alwaysAnimate => true;

  bool get taken => _taken;
  bool _taken = false;

  @override
  _HeroManifest _takeChild(Rect animationArea, Animation<double> currentAnimation) {
    assert(!taken);
    _taken = true;
    Set<HeroState> states = sourceStates;
    if (targetState != null)
      states = states.union(new HashSet<HeroState>.from(<HeroState>[targetState]));
    return new _HeroManifest(
      key: key,
      config: child,
      sourceStates: states,
      currentRect: currentRect.evaluate(currentAnimation),
      currentTurns: currentTurns.evaluate(currentAnimation)
    );
  }

  Widget build(BuildContext context, Animation<double> animation) {
    return new PositionedTransition(
      rect: currentRect.animate(animation),
      child: new RotationTransition(
        turns: currentTurns.animate(animation),
        child: new KeyedSubtree(
          key: key,
          child: child
        )
      )
    );
  }
}

class _HeroMatch {
  const _HeroMatch(this.from, this.to, this.tag);
  final HeroHandle from;
  final HeroHandle to;
  final Object tag;
}

class HeroParty {
  HeroParty({ this.onQuestFinished });

  final VoidCallback onQuestFinished;

  List<_HeroQuestState> _heroes = <_HeroQuestState>[];
  bool get isEmpty => _heroes.isEmpty;

  Map<Object, HeroHandle> getHeroesToAnimate() {
    Map<Object, HeroHandle> result = new Map<Object, HeroHandle>();
    for (_HeroQuestState hero in _heroes)
      result[hero.tag] = hero;
    assert(!result.containsKey(null));
    return result;
  }

  RelativeRectTween createRectTween(RelativeRect begin, RelativeRect end) {
    return new RelativeRectTween(begin: begin, end: end);
  }

  Tween<double> createTurnsTween(double begin, double end) {
    assert(end.floor() == end);
    return new Tween<double>(begin: begin, end: end);
  }

  void animate(Map<Object, HeroHandle> heroesFrom, Map<Object, HeroHandle> heroesTo, Rect animationArea) {
    assert(!heroesFrom.containsKey(null));
    assert(!heroesTo.containsKey(null));

    // make a list of pairs of heroes, based on the from and to lists
    Map<Object, _HeroMatch> heroes = <Object, _HeroMatch>{};
    for (Object tag in heroesFrom.keys)
      heroes[tag] = new _HeroMatch(heroesFrom[tag], heroesTo[tag], tag);
    for (Object tag in heroesTo.keys) {
      if (!heroes.containsKey(tag))
        heroes[tag] = new _HeroMatch(heroesFrom[tag], heroesTo[tag], tag);
    }

    // create a heroating hero out of each pair
    final List<_HeroQuestState> _newHeroes = <_HeroQuestState>[];
    for (_HeroMatch heroPair in heroes.values) {
      assert(heroPair.from != null || heroPair.to != null);
      if ((heroPair.from == null && !heroPair.to.alwaysAnimate) ||
          (heroPair.to == null && !heroPair.from.alwaysAnimate))
        continue;
      _HeroManifest from = heroPair.from?._takeChild(animationArea, _currentAnimation);
      assert(heroPair.to == null || heroPair.to is HeroState);
      _HeroManifest to = heroPair.to?._takeChild(animationArea, _currentAnimation);
      assert(from != null || to != null);
      assert(to == null || to.sourceStates.length == 1);
      assert(to == null || to.currentTurns.floor() == to.currentTurns);
      HeroState targetState = to != null ? to.sourceStates.elementAt(0) : null;
      Set<HeroState> sourceStates = from != null ? from.sourceStates : new HashSet<HeroState>();
      sourceStates.remove(targetState);
      RelativeRect sourceRect = from != null ? from.currentRect :
        new RelativeRect.fromRect(to.currentRect.toRect(animationArea).center & Size.zero, animationArea);
      RelativeRect targetRect = to != null ? to.currentRect :
        new RelativeRect.fromRect(from.currentRect.toRect(animationArea).center & Size.zero, animationArea);
      double sourceTurns = from != null ? from.currentTurns : 0.0;
      double targetTurns = to != null ? to.currentTurns : 0.0;
      _newHeroes.add(new _HeroQuestState(
        tag: heroPair.tag,
        key: from != null ? from.key : to.key,
        child: to != null ? to.config : from.config,
        sourceStates: sourceStates,
        targetRect: targetRect,
        targetTurns: targetTurns.floor(),
        targetState: targetState,
        currentRect: createRectTween(sourceRect, targetRect),
        currentTurns: createTurnsTween(sourceTurns, targetTurns)
      ));
    }

    assert(!_heroes.any((_HeroQuestState hero) => !hero.taken));
    _heroes = _newHeroes;
  }

  Animation<double> _currentAnimation;

  void _clearCurrentAnimation() {
    _currentAnimation?.removeStatusListener(_handleUpdate);
    _currentAnimation = null;
  }

  void setAnimation(Animation<double> animation) {
    assert(animation != null || _heroes.length == 0);
    if (animation != _currentAnimation) {
      _clearCurrentAnimation();
      _currentAnimation = animation;
      _currentAnimation?.addStatusListener(_handleUpdate);
    }
  }

  void _handleUpdate(AnimationStatus status) {
    if (status == AnimationStatus.completed ||
        status == AnimationStatus.dismissed) {
      for (_HeroQuestState hero in _heroes) {
        if (hero.targetState != null)
          hero.targetState._setChild(hero.key);
        for (HeroState source in hero.sourceStates)
          source._resetChild();
      }
      _heroes.clear();
      _clearCurrentAnimation();
      if (onQuestFinished != null)
        onQuestFinished();
    }
  }

  @override
  String toString() => '$_heroes';
}

class HeroController extends NavigatorObserver {
  HeroController() {
    _party = new HeroParty(onQuestFinished: _handleQuestFinished);
  }

  HeroParty _party;
  Animation<double> _animation;
  PageRoute<dynamic> _from;
  PageRoute<dynamic> _to;

  final List<OverlayEntry> _overlayEntries = new List<OverlayEntry>();

  @override
  void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
    assert(navigator != null);
    assert(route != null);
    if (route is PageRoute<dynamic>) {
      assert(route.animation != null);
      if (previousRoute is PageRoute<dynamic>) // could be null
        _from = previousRoute;
      _to = route;
      _animation = route.animation;
      _checkForHeroQuest();
    }
  }

  @override
  void didPop(Route<dynamic> route, Route<dynamic> previousRoute) {
    assert(navigator != null);
    assert(route != null);
    if (route is PageRoute<dynamic>) {
      assert(route.animation != null);
      if (previousRoute is PageRoute<dynamic>) {
        _to = previousRoute;
        _from = route;
        _animation = route.animation;
        _checkForHeroQuest();
      }
    }
  }

  void _checkForHeroQuest() {
    if (_from != null && _to != null && _from != _to) {
      _to.offstage = _to.animation.status != AnimationStatus.completed;
      WidgetsBinding.instance.addPostFrameCallback(_updateQuest);
    }
  }

  void _handleQuestFinished() {
    _removeHeroesFromOverlay();
    _from = null;
    _to = null;
    _animation = null;
  }

  Rect _getAnimationArea(BuildContext context) {
    RenderBox box = context.findRenderObject();
    Point topLeft = box.localToGlobal(Point.origin);
    Point bottomRight = box.localToGlobal(box.size.bottomRight(Point.origin));
    return new Rect.fromLTRB(topLeft.x, topLeft.y, bottomRight.x, bottomRight.y);
  }

  void _removeHeroesFromOverlay() {
    for (OverlayEntry entry in _overlayEntries)
      entry.remove();
    _overlayEntries.clear();
  }

  void _addHeroToOverlay(Widget hero, Object tag, OverlayState overlay) {
    OverlayEntry entry = new OverlayEntry(builder: (_) => hero);
    assert(_animation.status != AnimationStatus.dismissed && _animation.status != AnimationStatus.completed);
    if (_animation.status == AnimationStatus.forward)
      _to.insertHeroOverlayEntry(entry, tag, overlay);
    else
      _from.insertHeroOverlayEntry(entry, tag, overlay);
    _overlayEntries.add(entry);
  }

  Set<Key> _getMostValuableKeys() {
    assert(_from != null);
    assert(_to != null);
    Set<Key> result = new HashSet<Key>();
    if (_from.settings.mostValuableKeys != null)
      result.addAll(_from.settings.mostValuableKeys);
    if (_to.settings.mostValuableKeys != null)
      result.addAll(_to.settings.mostValuableKeys);
    return result;
  }

  void _updateQuest(Duration timeStamp) {
    Set<Key> mostValuableKeys = _getMostValuableKeys();
    Map<Object, HeroHandle> heroesFrom = _party.isEmpty ?
        Hero.of(_from.subtreeContext, mostValuableKeys) : _party.getHeroesToAnimate();

    Map<Object, HeroHandle> heroesTo = Hero.of(_to.subtreeContext, mostValuableKeys);
    _to.offstage = false;

    Animation<double> animation = _animation;
    Curve curve = Curves.ease;
    if (animation.status == AnimationStatus.reverse) {
      animation = new ReverseAnimation(animation);
      curve = new Interval(animation.value, 1.0, curve: curve);
    }

    _party.animate(heroesFrom, heroesTo, _getAnimationArea(navigator.context));
    _removeHeroesFromOverlay();
    _party.setAnimation(new CurvedAnimation(
      parent: animation,
      curve: curve
    ));
    for (_HeroQuestState hero in _party._heroes) {
      Widget widget = hero.build(navigator.context, animation);
      _addHeroToOverlay(widget, hero.tag, navigator.overlay);
    }
  }
}
