// Copyright 2014 The Flutter 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 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

/// This sample demonstrates creating a custom page transition that is able to
/// override the outgoing transition of the route behind it in the navigation
/// stack using [DelegatedTransitionBuilder], using a [MaterialApp.router]
/// pattern of navigation.

void main() {
  runApp(FlexibleRouteTransitionsApp());
}

class FlexibleRouteTransitionsApp extends StatelessWidget {
  FlexibleRouteTransitionsApp({super.key});

  final _MyRouteInformationParser _routeInformationParser =
      _MyRouteInformationParser();
  final MyRouterDelegate _routerDelegate = MyRouterDelegate();

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Mixing Routes',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        pageTransitionsTheme: const PageTransitionsTheme(
          builders: <TargetPlatform, PageTransitionsBuilder>{
            // By default the zoom builder is used on all platforms but iOS. Normally
            // on iOS the default is the Cupertino sliding transition. Setting
            // it to use zoom on all platforms allows the example to show multiple
            // transitions in one app for all platforms.
            TargetPlatform.iOS: ZoomPageTransitionsBuilder(),
          },
        ),
      ),
      routeInformationParser: _routeInformationParser,
      routerDelegate: _routerDelegate,
    );
  }
}

class _MyRouteInformationParser
    extends RouteInformationParser<MyPageConfiguration> {
  @override
  SynchronousFuture<MyPageConfiguration> parseRouteInformation(
    RouteInformation routeInformation,
  ) {
    return SynchronousFuture<MyPageConfiguration>(
      MyPageConfiguration.values.firstWhere((
        MyPageConfiguration pageConfiguration,
      ) {
        return pageConfiguration.uriString == routeInformation.uri.toString();
      }, orElse: () => MyPageConfiguration.unknown),
    );
  }

  @override
  RouteInformation? restoreRouteInformation(MyPageConfiguration configuration) {
    return RouteInformation(uri: configuration.uri);
  }
}

class MyRouterDelegate extends RouterDelegate<MyPageConfiguration> {
  final Set<VoidCallback> _listeners = <VoidCallback>{};
  final List<MyPageConfiguration> _pages = <MyPageConfiguration>[];

  void _notifyListeners() {
    for (final VoidCallback listener in _listeners) {
      listener();
    }
  }

  void onNavigateToHome() {
    _pages.clear();
    _pages.add(MyPageConfiguration.home);
    _notifyListeners();
  }

  void onNavigateToZoom() {
    _pages.add(MyPageConfiguration.zoom);
    _notifyListeners();
  }

  void onNavigateToIOS() {
    _pages.add(MyPageConfiguration.iOS);
    _notifyListeners();
  }

  void onNavigateToVertical() {
    _pages.add(MyPageConfiguration.vertical);
    _notifyListeners();
  }

  @override
  void addListener(VoidCallback listener) {
    _listeners.add(listener);
  }

  @override
  void removeListener(VoidCallback listener) {
    _listeners.remove(listener);
  }

  @override
  Future<bool> popRoute() {
    if (_pages.isEmpty) {
      return SynchronousFuture<bool>(false);
    }
    _pages.removeLast();
    _notifyListeners();
    return SynchronousFuture<bool>(true);
  }

  @override
  Future<void> setNewRoutePath(MyPageConfiguration configuration) {
    _pages.add(configuration);
    _notifyListeners();
    return SynchronousFuture<void>(null);
  }

  @override
  Widget build(BuildContext context) {
    return Navigator(
      restorationScopeId: 'root',
      onDidRemovePage: (Page<dynamic> page) {
        _pages.remove(MyPageConfiguration.fromName(page.name!));
      },
      pages: _pages
          .map(
            (MyPageConfiguration page) => switch (page) {
              MyPageConfiguration.unknown => _MyUnknownPage<void>(),
              MyPageConfiguration.home => _MyHomePage<void>(
                routerDelegate: this,
              ),
              MyPageConfiguration.zoom => _ZoomPage<void>(routerDelegate: this),
              MyPageConfiguration.iOS => _IOSPage<void>(routerDelegate: this),
              MyPageConfiguration.vertical => _VerticalPage<void>(
                routerDelegate: this,
              ),
            },
          )
          .toList(),
    );
  }
}

class _MyUnknownPage<T> extends MaterialPage<T> {
  _MyUnknownPage()
    : super(
        restorationId: 'unknown-page',
        child: Scaffold(
          appBar: AppBar(title: const Text('404')),
          body: const Center(child: Text('404')),
        ),
      );

  @override
  String get name => MyPageConfiguration.unknown.name;
}

class _MyHomePage<T> extends MaterialPage<T> {
  _MyHomePage({required this.routerDelegate})
    : super(
        restorationId: 'home-page',
        child: _MyPageScaffold(title: 'Home', routerDelegate: routerDelegate),
      );

  final MyRouterDelegate routerDelegate;

  @override
  String get name => MyPageConfiguration.home.name;
}

class _ZoomPage<T> extends MaterialPage<T> {
  _ZoomPage({required this.routerDelegate})
    : super(
        restorationId: 'zoom-page',
        child: _MyPageScaffold(
          title: 'Zoom Route',
          routerDelegate: routerDelegate,
        ),
      );

  final MyRouterDelegate routerDelegate;

  @override
  String get name => MyPageConfiguration.zoom.name;
}

class _IOSPage<T> extends CupertinoPage<T> {
  _IOSPage({required this.routerDelegate})
    : super(
        restorationId: 'ios-page',
        child: _MyPageScaffold(
          title: 'Cupertino Route',
          routerDelegate: routerDelegate,
        ),
      );

  final MyRouterDelegate routerDelegate;

  @override
  String get name => MyPageConfiguration.iOS.name;
}

class _VerticalPage<T> extends _VerticalTransitionPage<T> {
  _VerticalPage({required this.routerDelegate})
    : super(
        restorationId: 'vertical-page',
        child: _MyPageScaffold(
          title: 'Vertical Route',
          routerDelegate: routerDelegate,
        ),
      );

  final MyRouterDelegate routerDelegate;

  @override
  String get name => MyPageConfiguration.vertical.name;
}

class _MyPageScaffold extends StatelessWidget {
  const _MyPageScaffold({required this.title, required this.routerDelegate});

  final String title;

  final MyRouterDelegate routerDelegate;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            TextButton(
              onPressed: () {
                routerDelegate.onNavigateToVertical();
              },
              child: const Text('Crazy Vertical Transition'),
            ),
            TextButton(
              onPressed: () {
                routerDelegate.onNavigateToZoom();
              },
              child: const Text('Zoom Transition'),
            ),
            TextButton(
              onPressed: () {
                routerDelegate.onNavigateToIOS();
              },
              child: const Text('Cupertino Transition'),
            ),
          ],
        ),
      ),
    );
  }
}

// A Page that applies a _VerticalPageTransition.
class _VerticalTransitionPage<T> extends Page<T> {
  const _VerticalTransitionPage({
    required this.child,
    this.maintainState = true,
    this.fullscreenDialog = false,
    this.allowSnapshotting = true,
    super.key,
    super.canPop,
    super.onPopInvoked,
    super.name,
    super.arguments,
    super.restorationId,
  });

  final Widget child;

  final bool maintainState;

  final bool fullscreenDialog;

  final bool allowSnapshotting;

  @override
  Route<T> createRoute(BuildContext context) {
    return _PageBasedVerticalPageRoute<T>(page: this);
  }
}

class _PageBasedVerticalPageRoute<T> extends PageRoute<T> {
  _PageBasedVerticalPageRoute({
    required _VerticalTransitionPage<T> page,
    super.allowSnapshotting,
  }) : super(settings: page);

  _VerticalTransitionPage<T> get _page =>
      settings as _VerticalTransitionPage<T>;

  @override
  bool get maintainState => _page.maintainState;

  @override
  bool get fullscreenDialog => _page.fullscreenDialog;

  @override
  String get debugLabel => '${super.debugLabel}(${_page.name})';

  @override
  DelegatedTransitionBuilder? get delegatedTransition =>
      _VerticalPageTransition._delegatedTransitionBuilder;

  @override
  Color? get barrierColor => const Color(0x00000000);

  @override
  bool get barrierDismissible => false;

  @override
  String? get barrierLabel => 'Should be no visible barrier...';

  @override
  bool get opaque => false;

  @override
  Duration get transitionDuration => const Duration(milliseconds: 2000);

  @override
  Widget buildPage(
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
  ) {
    return _page.child;
  }

  @override
  Widget buildTransitions(
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
    Widget child,
  ) {
    return _VerticalPageTransition(
      primaryRouteAnimation: animation,
      secondaryRouteAnimation: secondaryAnimation,
      child: child,
    );
  }
}

// A page transition that slides off the screen vertically, and uses
// delegatedTransition to ensure that the outgoing route slides with it.
class _VerticalPageTransition extends StatelessWidget {
  _VerticalPageTransition({
    required Animation<double> primaryRouteAnimation,
    required this.secondaryRouteAnimation,
    required this.child,
  }) : _primaryPositionAnimation = CurvedAnimation(
         parent: primaryRouteAnimation,
         curve: _curve,
         reverseCurve: _curve,
       ).drive(_kBottomUpTween),
       _secondaryPositionAnimation = CurvedAnimation(
         parent: secondaryRouteAnimation,
         curve: _curve,
         reverseCurve: _curve,
       ).drive(_kTopDownTween);

  final Animation<Offset> _primaryPositionAnimation;

  final Animation<Offset> _secondaryPositionAnimation;

  final Animation<double> secondaryRouteAnimation;

  final Widget child;

  static const Curve _curve = Curves.decelerate;

  static final Animatable<Offset> _kBottomUpTween = Tween<Offset>(
    begin: const Offset(0.0, 1.0),
    end: Offset.zero,
  );

  static final Animatable<Offset> _kTopDownTween = Tween<Offset>(
    begin: Offset.zero,
    end: const Offset(0.0, -1.0),
  );

  // When the _VerticalTransitionPageRoute animates onto or off of the navigation
  // stack, this transition is given to the route below it so that they animate in
  // sync.
  static Widget _delegatedTransitionBuilder(
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
    bool allowSnapshotting,
    Widget? child,
  ) {
    final Animatable<Offset> tween = Tween<Offset>(
      begin: Offset.zero,
      end: const Offset(0.0, -1.0),
    ).chain(CurveTween(curve: _curve));

    return SlideTransition(
      position: secondaryAnimation.drive(tween),
      child: child,
    );
  }

  @override
  Widget build(BuildContext context) {
    assert(debugCheckHasDirectionality(context));
    final TextDirection textDirection = Directionality.of(context);
    return SlideTransition(
      position: _secondaryPositionAnimation,
      textDirection: textDirection,
      transformHitTests: false,
      child: SlideTransition(
        position: _primaryPositionAnimation,
        textDirection: textDirection,
        child: ClipRRect(
          borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
          child: child,
        ),
      ),
    );
  }
}

enum MyPageConfiguration {
  home(uriString: '/'),
  zoom(uriString: '/zoom'),
  iOS(uriString: '/iOS'),
  vertical(uriString: '/vertical'),
  unknown(uriString: '/404');

  const MyPageConfiguration({required this.uriString});

  final String uriString;

  static MyPageConfiguration fromName(String testName) {
    return values.firstWhere(
      (MyPageConfiguration page) => page.name == testName,
    );
  }

  Uri get uri => Uri.parse(uriString);
}
