/*
 * The MIT License (MIT)
 * 
 * Copyright (c) 2019 Alibaba Group
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';

import '../flutter_boost.dart';
import '../support/logger.dart';
import 'boost_container.dart';
import 'container_coordinator.dart';

enum ContainerOperation { Push, Onstage, Pop, Remove }

typedef BoostContainerObserver = void Function(
    ContainerOperation operation, BoostContainerSettings settings);

@immutable
class BoostContainerManager extends StatefulWidget {
  const BoostContainerManager({
    Key key,
    this.initNavigator,
    this.prePushRoute,
    this.postPushRoute,
  }) : super(key: key);

  final Navigator initNavigator;
  final PrePushRoute prePushRoute;
  final PostPushRoute postPushRoute;

  @override
  ContainerManagerState createState() => ContainerManagerState();

  static ContainerManagerState tryOf(BuildContext context) {
    final ContainerManagerState manager =
        context.findAncestorStateOfType<ContainerManagerState>();
    return manager;
  }

  static ContainerManagerState of(BuildContext context) {
    final ContainerManagerState manager =
        context.findAncestorStateOfType<ContainerManagerState>();
    assert(manager != null, 'not in flutter boost');
    return manager;
  }
}

class ContainerManagerState extends State<BoostContainerManager> {
  final GlobalKey<OverlayState> _overlayKey = GlobalKey<OverlayState>();
  final List<BoostContainer> _offstage = <BoostContainer>[];

  List<_ContainerOverlayEntry> _leastEntries;

  BoostContainer _onstage;
  bool _foreground = true;

  String _lastShownContainer;

  PrePushRoute get prePushRoute => widget.prePushRoute;

  PostPushRoute get postPushRoute => widget.postPushRoute;

  bool get foreground => _foreground;

  // Number of containers.
  int get containerCounts => _offstage.length;

  List<BoostContainer> get offstage => _offstage;

  // Setting for current visible container.
  BoostContainerSettings get onstageSettings => _onstage.settings;

  // Current visible container.
  BoostContainerState get onstageContainer => _stateOf(_onstage);

  BoostContainerState get subContainer =>
      _offstage.isEmpty ? null : _stateOf(_offstage.last);

  @override
  void initState() {
    super.initState();

    assert(widget.initNavigator != null);
    _onstage = BoostContainer.copy(widget.initNavigator);

    WidgetsBinding.instance.addPostFrameCallback((_) {
      setState(() {});
    });
  }

  void updateFocuse() {
    final BoostContainerState now = _stateOf(_onstage);
    if (now != null) {
      FocusScope.of(context).setFirstFocus(now.focusScopeNode);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Overlay(
      key: _overlayKey,
      initialEntries: const <OverlayEntry>[],
    );
  }

  BoostContainerState _stateOf(BoostContainer container) {
    if (container.key is GlobalKey<BoostContainerState>) {
      final GlobalKey<BoostContainerState> globalKey =
          container.key as GlobalKey<BoostContainerState>;
      return globalKey.currentState;
    }

    assert(
        false, 'key of BoostContainer must be GlobalKey<BoostContainerState>');
    return null;
  }

  void _onShownContainerChanged(String old, String now) {
    Logger.log('onShownContainerChanged old:$old now:$now');

    final Map<String, dynamic> properties = <String, dynamic>{};
    properties['newName'] = now;
    properties['oldName'] = old;

    FlutterBoost.singleton.channel
        .invokeMethod<dynamic>('onShownContainerChanged', properties);
  }

  void _refreshOverlayEntries() {
    final OverlayState overlayState = _overlayKey.currentState;

    if (overlayState == null) {
      return;
    }

    if (_leastEntries != null && _leastEntries.isNotEmpty) {
      for (final _ContainerOverlayEntry entry in _leastEntries) {
        entry.remove();
      }
    }

    final List<BoostContainer> containers = <BoostContainer>[];
    containers.addAll(_offstage);

    assert(_onstage != null, 'Should have a least one BoostContainer');
    containers.add(_onstage);

    _leastEntries = containers
        .map<_ContainerOverlayEntry>(
            (BoostContainer container) => _ContainerOverlayEntry(container))
        .toList(growable: false);

    overlayState.insertAll(_leastEntries);

    SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) {
      final String now = _onstage.settings.uniqueId;
      if (_lastShownContainer != now) {
        final String old = _lastShownContainer;
        _lastShownContainer = now;
        _onShownContainerChanged(old, now);
      }
      updateFocuse();
    });
  }

  @override
  void setState(VoidCallback fn) {
    if (SchedulerBinding.instance.schedulerPhase ==
        SchedulerPhase.persistentCallbacks) {
      SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
        Logger.log('_refreshOverlayEntries in addPostFrameCallback');
        _refreshOverlayEntries();
      });
    } else {
      Logger.log('_refreshOverlayEntries in setState');
      _refreshOverlayEntries();
    }

    fn();
    //return super.setState(fn);
  }

  void setForeground() {
    _foreground = true;
    ContainerCoordinator.performContainerLifeCycle(
        _onstage.settings, ContainerLifeCycle.Foreground);
  }

  void setBackground() {
    _foreground = false;
    ContainerCoordinator.performContainerLifeCycle(
        _onstage.settings, ContainerLifeCycle.Background);
  }

  //If container exists bring it to front else
  //create a container.
  void showContainer(BoostContainerSettings settings) {
    if (settings.uniqueId == _onstage.settings.uniqueId) {
      _onShownContainerChanged(null, settings.uniqueId);
      return;
    }

    final int index = _offstage.indexWhere((BoostContainer container) =>
        container.settings.uniqueId == settings.uniqueId);
    if (index > -1) {
      _offstage.add(_onstage);
      _onstage = _offstage.removeAt(index);

      setState(() {});

      for (final BoostContainerObserver observer in FlutterBoost
          .singleton.observersHolder
          .observersOf<BoostContainerObserver>()) {
        observer(ContainerOperation.Onstage, _onstage.settings);
      }
      Logger.log('ContainerObserver#2 didOnstage');
    } else {
      pushContainer(settings);
    }
  }

  BoostContainerState containerStateOf(String id) {
    if (id == _onstage.settings.uniqueId) {
      return _stateOf(_onstage);
    }

    final BoostContainer container = _offstage.firstWhere(
        (BoostContainer container) => container.settings.uniqueId == id,
        orElse: () => null);

    return container == null ? null : _stateOf(container);
  }

  bool containsContainer(String id) {
    if (id == _onstage.settings.uniqueId) {
      return true;
    }

    return _offstage
        .any((BoostContainer container) => container.settings.uniqueId == id);
  }

  void pushContainer(BoostContainerSettings settings) {
    assert(settings.uniqueId != _onstage.settings.uniqueId);
    assert(_offstage.every((BoostContainer container) =>
        container.settings.uniqueId != settings.uniqueId));

    _offstage.add(_onstage);
    _onstage = BoostContainer.obtain(widget.initNavigator, settings);

    setState(() {});

    for (final BoostContainerObserver observer in FlutterBoost
        .singleton.observersHolder
        .observersOf<BoostContainerObserver>()) {
      observer(ContainerOperation.Push, _onstage.settings);
    }
    Logger.log('ContainerObserver#2 didPush');
  }

  void pop() {
    assert(canPop());

    final BoostContainer old = _onstage;
    _onstage = _offstage.removeLast();
    setState(() {});

    final Set<BoostContainerObserver> observers = FlutterBoost
        .singleton.observersHolder
        .observersOf<BoostContainerObserver>();

    for (final BoostContainerObserver observer in observers) {
      observer(ContainerOperation.Pop, old.settings);
    }

    Logger.log('ContainerObserver#2 didPop');
  }

  void remove(String uniqueId) {
    if (_onstage.settings.uniqueId == uniqueId) {
      pop();
    } else {
      final BoostContainer container = _offstage.firstWhere(
        (BoostContainer container) => container.settings.uniqueId == uniqueId,
        orElse: () => null,
      );

      if (container != null) {
        _offstage.remove(container);
        setState(() {});

        final Set<BoostContainerObserver> observers = FlutterBoost
            .singleton.observersHolder
            .observersOf<BoostContainerObserver>();

        for (final BoostContainerObserver observer in observers) {
          observer(ContainerOperation.Remove, container.settings);
        }

        Logger.log('ContainerObserver#2 didRemove');
      }
    }
  }

  bool canPop() => _offstage.isNotEmpty;

  String dump() {
    String info;
    info = 'onstage#:\n  ${_onstage?.desc()}\noffstage#:';

    for (final BoostContainer container in _offstage.reversed) {
      info = '$info\n  ${container?.desc()}';
    }

    return info;
  }
}

class _ContainerOverlayEntry extends OverlayEntry {
  _ContainerOverlayEntry(BoostContainer container)
      : super(
          builder: (BuildContext ctx) => container,
          opaque: true,
          maintainState: true,
        );

  bool _removed = false;

  @override
  void remove() {
    assert(!_removed);
    if (_removed) {
      return;
    }
    _removed = true;
    super.remove();
  }
}