// Copyright 2016 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 'dart:ui' as ui;

import 'package:flutter/services.dart';
import 'package:mojo_services/mojo/gfx/composition/scene_token.mojom.dart' as mojom;
import 'package:mojo_services/mojo/ui/layouts.mojom.dart' as mojom;
import 'package:mojo_services/mojo/ui/view_provider.mojom.dart' as mojom;
import 'package:mojo_services/mojo/ui/views.mojom.dart' as mojom;
import 'package:mojo/application.dart';
import 'package:mojo/core.dart' as core;
import 'package:mojo/mojo/service_provider.mojom.dart' as mojom;

import 'box.dart';
import 'object.dart';

mojom.ViewHostProxy _initViewHostProxy() {
  int viewHost = ui.takeViewHostHandle();
  assert(() {
    if (viewHost == 0)
      debugPrint('Child view are supported only when running in Mojo shell.');
    return true;
  });
  return new mojom.ViewHostProxy.fromHandle(new core.MojoHandle(viewHost));
}

// TODO(abarth): The view host is a unique resource. We should structure how we
// take the handle from the engine so that multiple libraries can interact with
// the view host safely. Unfortunately, the view host has a global namespace of
// view keys, which means any scheme for sharing the view host also needs to
// provide a mechanism for coordinating about view keys.
final mojom.ViewHostProxy _viewHostProxy = _initViewHostProxy();
final mojom.ViewHost _viewHost = _viewHostProxy?.ptr;

class ChildViewConnection {
  ChildViewConnection({ this.url }) {
    mojom.ServiceProviderProxy incomingServices = new mojom.ServiceProviderProxy.unbound();
    mojom.ServiceProviderStub outgoingServices = new mojom.ServiceProviderStub.unbound();
    assert(_viewToken == null);
    mojom.ViewProviderProxy viewProvider = new mojom.ViewProviderProxy.unbound();
    shell.connectToService(url, viewProvider);
    _unresolvedViewToken = _awaitResponse(viewProvider.ptr.createView(incomingServices, outgoingServices), viewProvider);
    _connection = new ApplicationConnection(outgoingServices, incomingServices);
  }

  final String url;

  ApplicationConnection get connection => _connection;
  ApplicationConnection _connection;

  Future<mojom.ViewToken> _unresolvedViewToken;
  mojom.ViewToken _viewToken;

  Future<mojom.ViewToken> _awaitResponse(
    Future<mojom.ViewProviderCreateViewResponseParams> response,
    mojom.ViewProviderProxy viewProvider
  ) async {
    mojom.ViewToken viewToken = (await response).viewToken;
    viewProvider.close();
    assert(_viewToken == null);
    _viewToken = viewToken;
    assert(_viewKey == null);
    if (_attached)
      _addChildToViewHost();
    return viewToken;
  }

  static int _nextViewKey = 1;
  int _viewKey;

  void _addChildToViewHost() {
    assert(_attached);
    assert(_viewToken != null);
    assert(_viewKey == null);
    _viewKey = _nextViewKey++;
    _viewHost.addChild(_viewKey, _viewToken);
  }

  void _removeChildFromViewHost() {
    assert(!_attached);
    assert(_viewToken != null);
    assert(_viewKey != null);
    _viewHost.removeChild(_viewKey);
    _viewKey = null;
  }

  // The number of render objects attached to this view. In between frames, we
  // might have more than one connected if we get added to a new render object
  // before we get removed from the old render object. By the time we get around
  // to computing our layout, we must be back to just having one render object.
  int _attachments = 0;
  bool get _attached => _attachments > 0;

  void _attach() {
    assert(_attachments >= 0);
    ++_attachments;
    if (_viewToken != null && _viewKey == null)
      _addChildToViewHost();
  }

  void _detach() {
    assert(_attached);
    --_attachments;
    scheduleMicrotask(_removeChildFromViewHostIfNeeded);
  }

  void _removeChildFromViewHostIfNeeded() {
    assert(_attachments >= 0);
    if (_attachments == 0)
      _removeChildFromViewHost();
  }

  Future<mojom.ViewLayoutInfo> _layout({ Size size, double scale }) async {
    assert(_attached);
    assert(_attachments == 1);
    assert(_viewKey != null);
    int width = (size.width * scale).round();
    int height = (size.height * scale).round();
    // TODO(abarth): Ideally we would propagate our actually constraints to be
    // able to support rich cross-app layout. For now, we give the child tight
    // constraints for simplicity.
    mojom.BoxConstraints childConstraints = new mojom.BoxConstraints()
      ..minWidth = width
      ..maxWidth = width
      ..minHeight = height
      ..maxHeight = height;
    mojom.ViewLayoutParams layoutParams = new mojom.ViewLayoutParams()
      ..constraints = childConstraints
      ..devicePixelRatio = scale;
    return (await _viewHost.layoutChild(_viewKey, layoutParams)).info;
  }

  String toString() {
    return '$runtimeType(url: $url)';
  }
}

class RenderChildView extends RenderBox {
  RenderChildView({
    ChildViewConnection child,
    double scale
  }) : _child = child, _scale = scale {
    if (_child != null)
      _awaitViewToken();
  }

  ChildViewConnection get child => _child;
  ChildViewConnection _child;
  void set child (ChildViewConnection value) {
    if (value == _child)
      return;
    if (attached)
      _child?._detach();
    _child = value;
    _layoutInfo = null;
    if (attached)
      _child?._attach();

    if (_child == null) {
      markNeedsPaint();
    } else if (_child._viewToken != null) {
      // We've already connected to the view, so we're ready to invalidate our
      // layout immediately.
      markNeedsLayout();
    } else {
      // Otherwise, we're still in the process of connecting, so we need to
      // repaint now (to remove any old child view), and we need to watch for
      // the view token resolving before attempting layout.
      markNeedsPaint();
      _awaitViewToken();
    }
  }

  void _awaitViewToken() {
    _child._unresolvedViewToken.then(_handleViewTokenResolved);
  }

  double get scale => _scale;
  double _scale;
  void set scale (double value) {
    if (value == _scale)
      return;
    _scale = value;
    if (_child != null)
      markNeedsLayout();
  }

  void attach() {
    super.attach();
    _child?._attach();
  }

  void detach() {
    _child?._detach();
    super.detach();
  }

  bool get alwaysNeedsCompositing => true;
  bool get sizedByParent => true;

  void performResize() {
    size = constraints.biggest;
  }

  void performLayout() {
    if (_child != null && _child._viewToken != null)
      _child._layout(size: size, scale: scale).then(_handleLayoutInfoChanged);
  }

  mojom.ViewLayoutInfo _layoutInfo;

  void _handleLayoutInfoChanged(mojom.ViewLayoutInfo layoutInfo) {
    _layoutInfo = layoutInfo;
    markNeedsPaint();
  }

  void _handleViewTokenResolved(mojom.ViewToken viewToken) {
    // The _viewToken might not match viewToken if _child changed between the
    // time we started waiting for the future and the time it resolved.
    if (attached && _child?._viewToken == viewToken)
      markNeedsLayout();
  }

  bool hitTestSelf(Point position) => true;

  void paint(PaintingContext context, Offset offset) {
    assert(needsCompositing);
    if (_layoutInfo != null)
      context.pushChildScene(offset, _layoutInfo);
  }

  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('child: $child');
    description.add('scale: $scale');
  }
}
