library angel_framework.http.routable;

import 'dart:async';

import 'package:angel_container/angel_container.dart';
import 'package:angel_route/angel_route.dart';

import '../util.dart';
import 'hooked_service.dart';
import 'metadata.dart';
import 'request_context.dart';
import 'response_context.dart';
import 'service.dart';

final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');

/// A function that receives an incoming [RequestContext] and responds to it.
typedef FutureOr RequestHandler(RequestContext req, ResponseContext res);

/// Sequentially runs a list of [handlers] of middleware, and returns early if any does not
/// return `true`. Works well with [Router].chain.
RequestHandler waterfall(Iterable<RequestHandler> handlers) {
  return (req, res) {
    Future Function() runPipeline;

    for (var handler in handlers) {
      if (handler == null) break;

      if (runPipeline == null)
        runPipeline = () => Future.sync(() => handler(req, res));
      else {
        var current = runPipeline;
        runPipeline = () => current().then((result) => !res.isOpen
            ? new Future.value(result)
            : req.app.executeHandler(handler, req, res));
      }
    }

    runPipeline ??= () => new Future.value();
    return runPipeline();
  };
}

/// A routable server that can handle dynamic requests.
class Routable extends Router<RequestHandler> {
  final Map<Pattern, Service> _services = {};
  final Map configuration = {};

  final Container _container;

  Routable([Reflector reflector])
      : _container = reflector == null ? null : new Container(reflector),
        super();

  /// A [Container] used to inject dependencies.
  Container get container => _container;

  void close() {
    _services.clear();
    configuration.clear();
    _onService.close();
  }

  /// A set of [Service] objects that have been mapped into routes.
  Map<Pattern, Service> get services => _services;

  StreamController<Service> _onService =
      new StreamController<Service>.broadcast();

  /// Fired whenever a service is added to this instance.
  ///
  /// **NOTE**: This is a broadcast stream.
  Stream<Service> get onService => _onService.stream;

  /// Retrieves the service assigned to the given path.
  Service service(Pattern path) =>
      _services[path] ??
      _services[path.toString().replaceAll(_straySlashes, '')];

  @override
  Route<RequestHandler> addRoute(
      String method, String path, RequestHandler handler,
      {Iterable<RequestHandler> middleware: const <RequestHandler>[]}) {
    final handlers = <RequestHandler>[];
    // Merge @Middleware declaration, if any
    Middleware middlewareDeclaration =
        getAnnotation(handler, Middleware, _container?.reflector);
    if (middlewareDeclaration != null) {
      handlers.addAll(middlewareDeclaration.handlers);
    }

    final handlerSequence = <RequestHandler>[];
    handlerSequence.addAll(middleware ?? []);
    handlerSequence.addAll(handlers);

    return super.addRoute(method, path.toString(), handler,
        middleware: handlerSequence);
  }

  /// Mounts a [service] at the given [path].
  ///
  /// Returns a [HookedService] that can be used to hook into
  /// events dispatched by this service.
  HookedService<Id, Data, T> use<Id, Data, T extends Service<Id, Data>>(
      String path, T service) {
    var hooked = new HookedService<Id, Data, T>(service);
    _services[path
        .toString()
        .trim()
        .replaceAll(new RegExp(r'(^/+)|(/+$)'), '')] = hooked;
    hooked.addRoutes();
    mount(path.toString(), hooked);
    service.onHooked(hooked);
    _onService.add(hooked);
    return hooked;
  }
}