// Copyright 2013 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 'dart:async';
import 'dart:math';

import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';

import 'configuration.dart';
import 'information_provider.dart';
import 'logging.dart';
import 'match.dart';
import 'router.dart';

/// The function signature of [GoRouteInformationParser.onParserException].
///
/// The `routeMatchList` parameter contains the exception explains the issue
/// occurred.
///
/// The returned [RouteMatchList] is used as parsed result for the
/// [GoRouterDelegate].
typedef ParserExceptionHandler = RouteMatchList Function(
  BuildContext context,
  RouteMatchList routeMatchList,
);

/// Converts between incoming URLs and a [RouteMatchList] using [RouteMatcher].
/// Also performs redirection using [RouteRedirector].
class GoRouteInformationParser extends RouteInformationParser<RouteMatchList> {
  /// Creates a [GoRouteInformationParser].
  GoRouteInformationParser({
    required this.configuration,
    required this.onParserException,
  }) : _routeMatchListCodec = RouteMatchListCodec(configuration);

  /// The route configuration used for parsing [RouteInformation]s.
  final RouteConfiguration configuration;

  /// The exception handler that is called when parser can't handle the incoming
  /// uri.
  ///
  /// This method must return a [RouteMatchList] for the parsed result.
  final ParserExceptionHandler? onParserException;

  final RouteMatchListCodec _routeMatchListCodec;

  final Random _random = Random();

  /// The future of current route parsing.
  ///
  /// This is used for testing asynchronous redirection.
  @visibleForTesting
  Future<RouteMatchList>? debugParserFuture;

  /// Called by the [Router]. The
  @override
  Future<RouteMatchList> parseRouteInformationWithDependencies(
    RouteInformation routeInformation,
    BuildContext context,
  ) {
    assert(routeInformation.state != null);
    final Object state = routeInformation.state!;

    if (state is! RouteInformationState) {
      // This is a result of browser backward/forward button or state
      // restoration. In this case, the route match list is already stored in
      // the state.
      final RouteMatchList matchList =
          _routeMatchListCodec.decode(state as Map<Object?, Object?>);
      return debugParserFuture = _redirect(context, matchList)
          .then<RouteMatchList>((RouteMatchList value) {
        if (value.isError && onParserException != null) {
          // TODO(chunhtai): Figure out what to return if context is invalid.
          // ignore: use_build_context_synchronously
          return onParserException!(context, value);
        }
        return value;
      });
    }

    Uri uri = routeInformation.uri;
    if (uri.hasEmptyPath) {
      uri = uri.replace(path: '/');
    } else if (uri.path.length > 1 && uri.path.endsWith('/')) {
      // Remove trailing `/`.
      uri = uri.replace(path: uri.path.substring(0, uri.path.length - 1));
    }
    final RouteMatchList initialMatches = configuration.findMatch(
      uri,
      extra: state.extra,
    );
    if (initialMatches.isError) {
      log('No initial matches: ${routeInformation.uri.path}');
    }

    return debugParserFuture = _redirect(
      context,
      initialMatches,
    ).then<RouteMatchList>((RouteMatchList matchList) {
      if (matchList.isError && onParserException != null) {
        // TODO(chunhtai): Figure out what to return if context is invalid.
        // ignore: use_build_context_synchronously
        return onParserException!(context, matchList);
      }

      assert(() {
        if (matchList.isNotEmpty) {
          assert(!matchList.last.route.redirectOnly,
              'A redirect-only route must redirect to location different from itself.\n The offending route: ${matchList.last.route}');
        }
        return true;
      }());
      return _updateRouteMatchList(
        matchList,
        baseRouteMatchList: state.baseRouteMatchList,
        completer: state.completer,
        type: state.type,
      );
    });
  }

  @override
  Future<RouteMatchList> parseRouteInformation(
      RouteInformation routeInformation) {
    throw UnimplementedError(
        'use parseRouteInformationWithDependencies instead');
  }

  /// for use by the Router architecture as part of the RouteInformationParser
  @override
  RouteInformation? restoreRouteInformation(RouteMatchList configuration) {
    if (configuration.isEmpty) {
      return null;
    }
    String? location;
    if (GoRouter.optionURLReflectsImperativeAPIs &&
        (configuration.matches.last is ImperativeRouteMatch ||
            configuration.matches.last is ShellRouteMatch)) {
      RouteMatchBase route = configuration.matches.last;

      while (route is! ImperativeRouteMatch) {
        if (route is ShellRouteMatch && route.matches.isNotEmpty) {
          route = route.matches.last;
        } else {
          break;
        }
      }

      if (route case final ImperativeRouteMatch safeRoute) {
        location = safeRoute.matches.uri.toString();
      }
    }
    return RouteInformation(
      uri: Uri.parse(location ?? configuration.uri.toString()),
      state: _routeMatchListCodec.encode(configuration),
    );
  }

  Future<RouteMatchList> _redirect(
      BuildContext context, RouteMatchList routeMatch) {
    final FutureOr<RouteMatchList> redirectedFuture = configuration
        .redirect(context, routeMatch, redirectHistory: <RouteMatchList>[]);
    if (redirectedFuture is RouteMatchList) {
      return SynchronousFuture<RouteMatchList>(redirectedFuture);
    }
    return redirectedFuture;
  }

  RouteMatchList _updateRouteMatchList(
    RouteMatchList newMatchList, {
    required RouteMatchList? baseRouteMatchList,
    required Completer<Object?>? completer,
    required NavigatingType type,
  }) {
    switch (type) {
      case NavigatingType.push:
        return baseRouteMatchList!.push(
          ImperativeRouteMatch(
            pageKey: _getUniqueValueKey(),
            completer: completer!,
            matches: newMatchList,
          ),
        );
      case NavigatingType.pushReplacement:
        final RouteMatch routeMatch = baseRouteMatchList!.last;
        baseRouteMatchList = baseRouteMatchList.remove(routeMatch);
        if (baseRouteMatchList.isEmpty) {
          return newMatchList;
        }
        return baseRouteMatchList.push(
          ImperativeRouteMatch(
            pageKey: _getUniqueValueKey(),
            completer: completer!,
            matches: newMatchList,
          ),
        );
      case NavigatingType.replace:
        final RouteMatch routeMatch = baseRouteMatchList!.last;
        baseRouteMatchList = baseRouteMatchList.remove(routeMatch);
        if (baseRouteMatchList.isEmpty) {
          return newMatchList;
        }
        return baseRouteMatchList.push(
          ImperativeRouteMatch(
            pageKey: routeMatch.pageKey,
            completer: completer!,
            matches: newMatchList,
          ),
        );
      case NavigatingType.go:
        return newMatchList;
      case NavigatingType.restore:
        // Still need to consider redirection.
        return baseRouteMatchList!.uri.toString() != newMatchList.uri.toString()
            ? newMatchList
            : baseRouteMatchList;
    }
  }

  ValueKey<String> _getUniqueValueKey() {
    return ValueKey<String>(String.fromCharCodes(
        List<int>.generate(32, (_) => _random.nextInt(33) + 89)));
  }
}