content: import 'dart:async';
  import 'dart:ui' as ui;
  
  import 'package:flutter/material.dart';
  import 'package:flutter/services.dart';
  import 'package:flutter_hbb/common/shared_state.dart';
  import 'package:flutter_hbb/common/widgets/toolbar.dart';
  import 'package:flutter_hbb/consts.dart';
  import 'package:flutter_hbb/mobile/widgets/floating_mouse.dart';
  import 'package:flutter_hbb/mobile/widgets/floating_mouse_widgets.dart';
  import 'package:flutter_hbb/mobile/widgets/gesture_help.dart';
  import 'package:flutter_hbb/models/chat_model.dart';
  import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
  import 'package:flutter_svg/svg.dart';
  import 'package:get/get.dart';
  import 'package:provider/provider.dart';
  import 'package:wakelock_plus/wakelock_plus.dart';
  
  import '../../common.dart';
  import '../../common/widgets/overlay.dart';
  import '../../common/widgets/dialog.dart';
  import '../../common/widgets/remote_input.dart';
  import '../../models/input_model.dart';
  import '../../models/model.dart';
  import '../../models/platform_model.dart';
  import '../../utils/image.dart';
  import '../widgets/dialog.dart';
  
  final initText = '1' * 1024;
  
  // Workaround for Android (default input method, Microsoft SwiftKey keyboard) when using physical keyboard.
  // When connecting a physical keyboard, `KeyEvent.physicalKey.usbHidUsage` are wrong is using Microsoft SwiftKey keyboard.
  // https://github.com/flutter/flutter/issues/159384
  // https://github.com/flutter/flutter/issues/159383
  void _disableAndroidSoftKeyboard({bool? isKeyboardVisible}) {
    if (isAndroid) {
      if (isKeyboardVisible != true) {
        // `enable_soft_keyboard` will be set to `true` when clicking the keyboard icon, in `openKeyboard()`.
        gFFI.invokeMethod("enable_soft_keyboard", false);
      }
    }
  }
  
  class RemotePage extends StatefulWidget {
    RemotePage(
        {Key? key,
        required this.id,
        this.password,
        this.isSharedPassword,
        this.forceRelay})
        : super(key: key);
  
    final String id;
    final String? password;
    final bool? isSharedPassword;
    final bool? forceRelay;
  
    @override
    State<RemotePage> createState() => _RemotePageState(id);
  }
  
  class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
    Timer? _timer;
    bool _showBar = !isWebDesktop;
    bool _showGestureHelp = false;
    String _value = '';
    Orientation? _currentOrientation;
    double _viewInsetsBottom = 0;
  
    Timer? _timerDidChangeMetrics;
  
    final _blockableOverlayState = BlockableOverlayState();
  
    final keyboardVisibilityController = KeyboardVisibilityController();
    late final StreamSubscription<bool> keyboardSubscription;
    final FocusNode _mobileFocusNode = FocusNode();
    final FocusNode _physicalFocusNode = FocusNode();
    var _showEdit = false; // use soft keyboard
  
    InputModel get inputModel => gFFI.inputModel;
    SessionID get sessionId => gFFI.sessionId;
  
    final TextEditingController _textController =
        TextEditingController(text: initText);
  
    _RemotePageState(String id) {
      initSharedStates(id);
      gFFI.chatModel.voiceCallStatus.value = VoiceCallStatus.notStarted;
      gFFI.dialogManager.loadMobileActionsOverlayVisible();
    }
  
    @override
    void initState() {
      super.initState();
      gFFI.ffiModel.updateEventListener(sessionId, widget.id);
      gFFI.start(
        widget.id,
        password: widget.password,
        isSharedPassword: widget.isSharedPassword,
        forceRelay: widget.forceRelay,
      );
      WidgetsBinding.instance.addPostFrameCallback((_) {
        SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
        gFFI.dialogManager
            .showLoading(translate('Connecting...'), onCancel: closeConnection);
      });
      if (!isWeb) {
        WakelockPlus.enable();
      }
      _physicalFocusNode.requestFocus();
      gFFI.inputModel.listenToMouse(true);
      gFFI.qualityMonitorModel.checkShowQualityMonitor(sessionId);
      keyboardSubscription =
          keyboardVisibilityController.onChange.listen(onSoftKeyboardChanged);
      gFFI.chatModel
          .changeCurrentKey(MessageKey(widget.id, ChatModel.clientModeID));
      _blockableOverlayState.applyFfi(gFFI);
      gFFI.imageModel.addCallbackOnFirstImage((String peerId) {
        gFFI.recordingModel
            .updateStatus(bind.sessionGetIsRecording(sessionId: gFFI.sessionId));
        if (gFFI.recordingModel.start) {
          showToast(translate('Automatically record outgoing sessions'));
        }
        _disableAndroidSoftKeyboard(
            isKeyboardVisible: keyboardVisibilityController.isVisible);
      });
      WidgetsBinding.instance.addObserver(this);
    }
  
    @override
    Future<void> dispose() async {
      WidgetsBinding.instance.removeObserver(this);
      // https://github.com/flutter/flutter/issues/64935
      super.dispose();
      gFFI.dialogManager.hideMobileActionsOverlay(store: false);
      gFFI.inputModel.listenToMouse(false);
      gFFI.imageModel.disposeImage();
      gFFI.cursorModel.disposeImages();
      await gFFI.invokeMethod("enable_soft_keyboard", true);
      _mobileFocusNode.dispose();
      _physicalFocusNode.dispose();
      await gFFI.close();
      _timer?.cancel();
      _timerDidChangeMetrics?.cancel();
      gFFI.dialogManager.dismissAll();
      await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
          overlays: SystemUiOverlay.values);
      if (!isWeb) {
        await WakelockPlus.disable();
      }
      await keyboardSubscription.cancel();
      removeSharedStates(widget.id);
      // `on_voice_call_closed` should be called when the connection is ended.
      // The inner logic of `on_voice_call_closed` will check if the voice call is active.
      // Only one client is considered here for now.
      gFFI.chatModel.onVoiceCallClosed("End connetion");
    }
  
    @override
    void didChangeAppLifecycleState(AppLifecycleState state) {
      if (state == AppLifecycleState.resumed) {
        trySyncClipboard();
      }
    }
  
    // For client side
    // When swithing from other app to this app, try to sync clipboard.
    void trySyncClipboard() {
      gFFI.invokeMethod("try_sync_clipboard");
    }
  
    @override
    void didChangeMetrics() {
      // If the soft keyboard is visible and the canvas has been changed(panned or scaled)
      // Don't try reset the view style and focus the cursor.
      if (gFFI.cursorModel.lastKeyboardIsVisible &&
          gFFI.canvasModel.isMobileCanvasChanged) {
        return;
      }
  
      final newBottom = MediaQueryData.fromView(ui.window).viewInsets.bottom;
      _timerDidChangeMetrics?.cancel();
      _timerDidChangeMetrics = Timer(Duration(milliseconds: 100), () async {
        // We need this comparation because poping up the floating action will also trigger `didChangeMetrics()`.
        if (newBottom != _viewInsetsBottom) {
          gFFI.canvasModel.mobileFocusCanvasCursor();
          _viewInsetsBottom = newBottom;
        }
      });
    }
  
    // to-do: It should be better to use transparent color instead of the bgColor.
    // But for now, the transparent color will cause the canvas to be white.
    // I'm sure that the white color is caused by the Overlay widget in BlockableOverlay.
    // But I don't know why and how to fix it.
    Widget emptyOverlay(Color bgColor) => BlockableOverlay(
          /// the Overlay key will be set with _blockableOverlayState in BlockableOverlay
          /// see override build() in [BlockableOverlay]
          state: _blockableOverlayState,
          underlying: Container(
            color: bgColor,
          ),
        );
  
    void onSoftKeyboardChanged(bool visible) {
      if (!visible) {
        SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
        // [pi.version.isNotEmpty] -> check ready or not, avoid login without soft-keyboard
        if (gFFI.chatModel.chatWindowOverlayEntry == null &&
            gFFI.ffiModel.pi.version.isNotEmpty) {
          gFFI.invokeMethod("enable_soft_keyboard", false);
        }
      } else {
        _timer?.cancel();
        _timer = Timer(kMobileDelaySoftKeyboardFocus, () {
          SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
              overlays: SystemUiOverlay.values);
          _mobileFocusNode.requestFocus();
        });
      }
      // update for Scaffold
      setState(() {});
    }
  
    void _handleIOSSoftKeyboardInput(String newValue) {
      var oldValue = _value;
      _value = newValue;
      var i = newValue.length - 1;
      for (; i >= 0 && newValue[i] != '1'; --i) {}
      var j = oldValue.length - 1;
      for (; j >= 0 && oldValue[j] != '1'; --j) {}
      if (i < j) j = i;
      var subNewValue = newValue.substring(j + 1);
      var subOldValue = oldValue.substring(j + 1);
  
      // get common prefix of subNewValue and subOldValue
      var common = 0;
      for (;
          common < subOldValue.length &&
              common < subNewValue.length &&
              subNewValue[common] == subOldValue[common];
          ++common) {}
  
      // get newStr from subNewValue
      var newStr = "";
      if (subNewValue.length > common) {
        newStr = subNewValue.substring(common);
      }
  
      // Set the value to the old value and early return if is still composing. (1 && 2)
      // 1. The composing range is valid
      // 2. The new string is shorter than the composing range.
      if (_textController.value.isComposingRangeValid) {
        final composingLength = _textController.value.composing.end -
            _textController.value.composing.start;
        if (composingLength > newStr.length) {
          _value = oldValue;
          return;
        }
      }
  
      // Delete the different part in the old value.
      for (i = 0; i < subOldValue.length - common; ++i) {
        inputModel.inputKey('VK_BACK');
      }
  
      // Input the new string.
      if (newStr.length > 1) {
        bind.sessionInputString(sessionId: sessionId, value: newStr);
      } else {
        inputChar(newStr);
      }
    }
  
    void _handleNonIOSSoftKeyboardInput(String newValue) {
      var oldValue = _value;
      _value = newValue;
      if (oldValue.isNotEmpty &&
          newValue.isNotEmpty &&
          oldValue[0] == '1' &&
          newValue[0] != '1') {
        // clipboard
        oldValue = '';
      }
      if (newValue.length == oldValue.length) {
        // ?
      } else if (newValue.length < oldValue.length) {
        final char = 'VK_BACK';
        inputModel.inputKey(char);
      } else {
        final content = newValue.substring(oldValue.length);
        if (content.length > 1) {
          if (oldValue != '' &&
              content.length == 2 &&
              (content == '""' ||
                  content == '()' ||
                  content == '[]' ||
                  content == '<>' ||
                  content == "{}" ||
                  content == '”“' ||
                  content == '《》' ||
                  content == '（）' ||
                  content == '【】')) {
            // can not only input content[0], because when input ], [ are also auo insert, which cause ] never be input
            bind.sessionInputString(sessionId: sessionId, value: content);
            openKeyboard();
            return;
          }
          bind.sessionInputString(sessionId: sessionId, value: content);
        } else {
          inputChar(content);
        }
      }
    }
  
    // handle mobile virtual keyboard
    void handleSoftKeyboardInput(String newValue) {
      if (isIOS) {
        _handleIOSSoftKeyboardInput(newValue);
      } else {
        _handleNonIOSSoftKeyboardInput(newValue);
      }
    }
  
    void inputChar(String char) {
      if (char == '\n') {
        char = 'VK_RETURN';
      } else if (char == ' ') {
        char = 'VK_SPACE';
      }
      inputModel.inputKey(char);
    }
  
    void openKeyboard() {
      gFFI.invokeMethod("enable_soft_keyboard", true);
      // destroy first, so that our _value trick can work
      _value = initText;
      _textController.text = _value;
      setState(() => _showEdit = false);
      _timer?.cancel();
      _timer = Timer(kMobileDelaySoftKeyboard, () {
        // show now, and sleep a while to requestFocus to
        // make sure edit ready, so that keyboard won't show/hide/show/hide happen
        setState(() => _showEdit = true);
        _timer?.cancel();
        _timer = Timer(kMobileDelaySoftKeyboardFocus, () {
          SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
              overlays: SystemUiOverlay.values);
          _mobileFocusNode.requestFocus();
        });
      });
    }
  
    Widget _bottomWidget() => _showGestureHelp
        ? getGestureHelp()
        : (_showBar && gFFI.ffiModel.pi.displays.isNotEmpty
            ? getBottomAppBar()
            : Offstage());
  
    @override
    Widget build(BuildContext context) {
      final keyboardIsVisible =
          keyboardVisibilityController.isVisible && _showEdit;
      final showActionButton = !_showBar || keyboardIsVisible || _showGestureHelp;
  
      return WillPopScope(
        onWillPop: () async {
          clientClose(sessionId, gFFI.dialogManager);
          return false;
        },
        child: Scaffold(
            // workaround for https://github.com/rustdesk/rustdesk/issues/3131
            floatingActionButtonLocation: keyboardIsVisible
                ? FABLocation(FloatingActionButtonLocation.endFloat, 0, -35)
                : null,
            floatingActionButton: !showActionButton
                ? null
                : FloatingActionButton(
                    mini: !keyboardIsVisible,
                    child: Icon(
                      (keyboardIsVisible || _showGestureHelp)
                          ? Icons.expand_more
                          : Icons.expand_less,
                      color: Colors.white,
                    ),
                    backgroundColor: MyTheme.accent,
                    onPressed: () {
                      setState(() {
                        if (keyboardIsVisible) {
                          _showEdit = false;
                          gFFI.invokeMethod("enable_soft_keyboard", false);
                          _mobileFocusNode.unfocus();
                          _physicalFocusNode.requestFocus();
                        } else if (_showGestureHelp) {
                          _showGestureHelp = false;
                        } else {
                          _showBar = !_showBar;
                        }
                      });
                    }),
            bottomNavigationBar: Obx(() => Stack(
                  alignment: Alignment.bottomCenter,
                  children: [
                    gFFI.ffiModel.pi.isSet.isTrue &&
                            gFFI.ffiModel.waitForFirstImage.isTrue
                        ? emptyOverlay(MyTheme.canvasColor)
                        : () {
                            gFFI.ffiModel.tryShowAndroidActionsOverlay();
                            return Offstage();
                          }(),
                    _bottomWidget(),
                    gFFI.ffiModel.pi.isSet.isFalse
                        ? emptyOverlay(MyTheme.canvasColor)
                        : Offstage(),
                  ],
                )),
            body: Obx(
              () => getRawPointerAndKeyBody(Overlay(
                initialEntries: [
                  OverlayEntry(builder: (context) {
                    return Container(
                      color: kColorCanvas,
                      child: isWebDesktop
                          ? getBodyForDesktopWithListener()
                          : SafeArea(
                              child:
                                  OrientationBuilder(builder: (ctx, orientation) {
                                if (_currentOrientation != orientation) {
                                  Timer(const Duration(milliseconds: 200), () {
                                    gFFI.dialogManager
                                        .resetMobileActionsOverlay(ffi: gFFI);
                                    _currentOrientation = orientation;
                                    gFFI.canvasModel.updateViewStyle();
                                  });
                                }
                                return Container(
                                  color: MyTheme.canvasColor,
                                  child: inputModel.isPhysicalMouse.value
                                      ? getBodyForMobile()
                                      : RawTouchGestureDetectorRegion(
                                          child: getBodyForMobile(),
                                          ffi: gFFI,
                                        ),
                                );
                              }),
                            ),
                    );
                  })
                ],
              )),
            )),
      );
    }
  
    Widget getRawPointerAndKeyBody(Widget child) {
      final ffiModel = Provider.of<FfiModel>(context);
      return RawPointerMouseRegion(
        cursor: ffiModel.keyboard ? SystemMouseCursors.none : MouseCursor.defer,
        inputModel: inputModel,
        // Disable RawKeyFocusScope before the connecting is established.
        // The "Delete" key on the soft keyboard may be grabbed when inputting the password dialog.
        child: gFFI.ffiModel.pi.isSet.isTrue
            ? RawKeyFocusScope(
                focusNode: _physicalFocusNode,
                inputModel: inputModel,
                child: child)
            : child,
      );
    }
  
    Widget getBottomAppBar() {
      final ffiModel = Provider.of<FfiModel>(context);
      return BottomAppBar(
        elevation: 10,
        color: MyTheme.accent,
        child: Row(
          mainAxisSize: MainAxisSize.max,
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: <Widget>[
            Row(
                children: <Widget>[
                      IconButton(
                        color: Colors.white,
                        icon: Icon(Icons.clear),
                        onPressed: () {
                          clientClose(sessionId, gFFI.dialogManager);
                        },
                      ),
                      IconButton(
                        color: Colors.white,
                        icon: Icon(Icons.tv),
                        onPressed: () {
                          setState(() => _showEdit = false);
                          showOptions(context, widget.id, gFFI.dialogManager);
                        },
                      )
                    ] +
                    (isWebDesktop || ffiModel.viewOnly || !ffiModel.keyboard
                        ? []
                        : gFFI.ffiModel.isPeerAndroid
                            ? [
                                IconButton(
                                    color: Colors.white,
                                    icon: Icon(Icons.keyboard),
                                    onPressed: openKeyboard),
                                IconButton(
                                  color: Colors.white,
                                  icon: const Icon(Icons.build),
                                  onPressed: () => gFFI.dialogManager
                                      .toggleMobileActionsOverlay(ffi: gFFI),
                                )
                              ]
                            : [
                                IconButton(
                                    color: Colors.white,
                                    icon: Icon(Icons.keyboard),
                                    onPressed: openKeyboard),
                                IconButton(
                                  color: Colors.white,
                                  icon: Icon(gFFI.ffiModel.touchMode
                                      ? Icons.touch_app
                                      : Icons.mouse),
                                  onPressed: () => setState(
                                      () => _showGestureHelp = !_showGestureHelp),
                                ),
                              ]) +
                    (isWeb
                        ? []
                        : <Widget>[
                            futureBuilder(
                                future: gFFI.invokeMethod(
                                    "get_value", "KEY_IS_SUPPORT_VOICE_CALL"),
                                hasData: (isSupportVoiceCall) => IconButton(
                                      color: Colors.white,
                                      icon: isAndroid && isSupportVoiceCall
                                          ? SvgPicture.asset('assets/chat.svg',
                                              colorFilter: ColorFilter.mode(
                                                  Colors.white, BlendMode.srcIn))
                                          : Icon(Icons.message),
                                      onPressed: () =>
                                          isAndroid && isSupportVoiceCall
                                              ? showChatOptions(widget.id)
                                              : onPressedTextChat(widget.id),
                                    ))
                          ]) +
                    [
                      IconButton(
                        color: Colors.white,
                        icon: Icon(Icons.more_vert),
                        onPressed: () {
                          setState(() => _showEdit = false);
                          showActions(widget.id);
                        },
                      ),
                    ]),
            Obx(() => IconButton(
                  color: Colors.white,
                  icon: Icon(Icons.expand_more),
                  onPressed: gFFI.ffiModel.waitForFirstImage.isTrue
                      ? null
                      : () {
                          setState(() => _showBar = !_showBar);
                        },
                )),
          ],
        ),
      );
    }
  
    bool get showCursorPaint =>
        !gFFI.ffiModel.isPeerAndroid && !gFFI.canvasModel.cursorEmbedded;
  
    Widget getBodyForMobile() {
      final keyboardIsVisible = keyboardVisibilityController.isVisible;
      return Container(
          color: MyTheme.canvasColor,
          child: Stack(children: () {
            final paints = [
              ImagePaint(),
              Positioned(
                top: 10,
                right: 10,
                child: QualityMonitor(gFFI.qualityMonitorModel),
              ),
              KeyHelpTools(
                  keyboardIsVisible: keyboardIsVisible,
                  showGestureHelp: _showGestureHelp),
              SizedBox(
                width: 0,
                height: 0,
                child: !_showEdit
                    ? Container()
                    : TextFormField(
                        textInputAction: TextInputAction.newline,
                        autocorrect: false,
                        // Flutter 3.16.9 Android.
                        // `enableSuggestions` causes secure keyboard to be shown.
                        // https://github.com/flutter/flutter/issues/139143
                        // https://github.com/flutter/flutter/issues/146540
                        // enableSuggestions: false,
                        autofocus: true,
                        focusNode: _mobileFocusNode,
                        maxLines: null,
                        controller: _textController,
                        // trick way to make backspace work always
                        keyboardType: TextInputType.multiline,
                        // `onChanged` may be called depending on the input method if this widget is wrapped in
                        // `Focus(onKeyEvent: ..., child: ...)`
                        // For `Backspace` button in the soft keyboard:
                        // en/fr input method:
                        //      1. The button will not trigger `onKeyEvent` if the text field is not empty.
                        //      2. The button will trigger `onKeyEvent` if the text field is empty.
                        // ko/zh/ja input method: the button will trigger `onKeyEvent`
                        //                     and the event will not popup if `KeyEventResult.handled` is returned.
                        onChanged: handleSoftKeyboardInput,
                      ).workaroundFreezeLinuxMint(),
              ),
            ];
            if (showCursorPaint) {
              paints.add(CursorPaint(widget.id));
            }
            if (gFFI.ffiModel.touchMode) {
              paints.add(FloatingMouse(
                ffi: gFFI,
              ));
            } else {
              paints.add(FloatingMouseWidgets(
                ffi: gFFI,
              ));
            }
            return paints;
          }()));
    }
  
    Widget getBodyForDesktopWithListener() {
      final ffiModel = Provider.of<FfiModel>(context);
      var paints = <Widget>[ImagePaint()];
      if (showCursorPaint) {
        final cursor = bind.sessionGetToggleOptionSync(
            sessionId: sessionId, arg: 'show-remote-cursor');
        if (ffiModel.keyboard || cursor) {
          paints.add(CursorPaint(widget.id));
        }
      }
      return Container(
          color: MyTheme.canvasColor, child: Stack(children: paints));
    }
  
    List<TTextMenu> _getMobileActionMenus() {
      if (gFFI.ffiModel.pi.platform != kPeerPlatformAndroid ||
          !gFFI.ffiModel.keyboard) {
        return [];
      }
      final enabled = versionCmp(gFFI.ffiModel.pi.version, '1.2.7') >= 0;
      if (!enabled) return [];
      return [
        TTextMenu(
          child: Text(translate('Back')),
          onPressed: () => gFFI.inputModel.onMobileBack(),
        ),
        TTextMenu(
          child: Text(translate('Home')),
          onPressed: () => gFFI.inputModel.onMobileHome(),
        ),
        TTextMenu(
          child: Text(translate('Apps')),
          onPressed: () => gFFI.inputModel.onMobileApps(),
        ),
        TTextMenu(
          child: Text(translate('Volume up')),
          onPressed: () => gFFI.inputModel.onMobileVolumeUp(),
        ),
        TTextMenu(
          child: Text(translate('Volume down')),
          onPressed: () => gFFI.inputModel.onMobileVolumeDown(),
        ),
        TTextMenu(
          child: Text(translate('Power')),
          onPressed: () => gFFI.inputModel.onMobilePower(),
        ),
      ];
    }
  
    void showActions(String id) async {
      final size = MediaQuery.of(context).size;
      final x = 120.0;
      final y = size.height;
      final mobileActionMenus = _getMobileActionMenus();
      final menus = toolbarControls(context, id, gFFI);
  
      final List<PopupMenuEntry<int>> more = [
        ...mobileActionMenus
            .asMap()
            .entries
            .map((e) =>
                PopupMenuItem<int>(child: e.value.getChild(), value: e.key))
            .toList(),
        if (mobileActionMenus.isNotEmpty) PopupMenuDivider(),
        ...menus
            .asMap()
            .entries
            .map((e) => PopupMenuItem<int>(
                child: e.value.getChild(),
                value: e.key + mobileActionMenus.length))
            .toList(),
      ];
      () async {
        var index = await showMenu(
          context: context,
          position: RelativeRect.fromLTRB(x, y, x, y),
          items: more,
          elevation: 8,
        );
        if (index != null) {
          if (index < mobileActionMenus.length) {
            mobileActionMenus[index].onPressed?.call();
          } else if (index < mobileActionMenus.length + more.length) {
            menus[index - mobileActionMenus.length].onPressed?.call();
          }
        }
      }();
    }
  
    onPressedTextChat(String id) {
      gFFI.chatModel.changeCurrentKey(MessageKey(id, ChatModel.clientModeID));
      gFFI.chatModel.toggleChatOverlay();
    }
  
    showChatOptions(String id) async {
      onPressVoiceCall() => bind.sessionRequestVoiceCall(sessionId: sessionId);
      onPressEndVoiceCall() => bind.sessionCloseVoiceCall(sessionId: sessionId);
  
      makeTextMenu(String label, Widget icon, VoidCallback onPressed,
              {TextStyle? labelStyle}) =>
          TTextMenu(
            child: Text(translate(label), style: labelStyle),
            trailingIcon: Transform.scale(
              scale: (isDesktop || isWebDesktop) ? 0.8 : 1,
              child: IgnorePointer(
                child: IconButton(
                  onPressed: null,
                  icon: icon,
                ),
              ),
            ),
            onPressed: onPressed,
          );
  
      final isInVoice = [
        VoiceCallStatus.waitingForResponse,
        VoiceCallStatus.connected
      ].contains(gFFI.chatModel.voiceCallStatus.value);
      final menus = [
        makeTextMenu('Text chat', Icon(Icons.message, color: MyTheme.accent),
            () => onPressedTextChat(widget.id)),
        isInVoice
            ? makeTextMenu(
                'End voice call',
                SvgPicture.asset(
                  'assets/call_wait.svg',
                  colorFilter:
                      ColorFilter.mode(Colors.redAccent, BlendMode.srcIn),
                ),
                onPressEndVoiceCall,
                labelStyle: TextStyle(color: Colors.redAccent))
            : makeTextMenu(
                'Voice call',
                SvgPicture.asset(
                  'assets/call_wait.svg',
                  colorFilter: ColorFilter.mode(MyTheme.accent, BlendMode.srcIn),
                ),
                onPressVoiceCall),
      ];
  
      final menuItems = menus
          .asMap()
          .entries
          .map((e) => PopupMenuItem<int>(child: e.value.getChild(), value: e.key))
          .toList();
      Future.delayed(Duration.zero, () async {
        final size = MediaQuery.of(context).size;
        final x = 120.0;
        final y = size.height;
        var index = await showMenu(
          context: context,
          position: RelativeRect.fromLTRB(x, y, x, y),
          items: menuItems,
          elevation: 8,
        );
        if (index != null && index < menus.length) {
          menus[index].onPressed?.call();
        }
      });
    }
  
    /// aka changeTouchMode
    BottomAppBar getGestureHelp() {
      return BottomAppBar(
          child: SingleChildScrollView(
              controller: ScrollController(),
              padding: EdgeInsets.symmetric(vertical: 10),
              child: GestureHelp(
                touchMode: gFFI.ffiModel.touchMode,
                onTouchModeChange: (t) {
                  gFFI.ffiModel.toggleTouchMode();
                  final v = gFFI.ffiModel.touchMode ? 'Y' : '';
                  bind.sessionPeerOption(
                      sessionId: sessionId, name: kOptionTouchMode, value: v);
                },
                virtualMouseMode: gFFI.ffiModel.virtualMouseMode,
              )));
    }
  
    // * Currently mobile does not enable map mode
    // void changePhysicalKeyboardInputMode() async {
    //   var current = await bind.sessionGetKeyboardMode(id: widget.id) ?? "legacy";
    //   gFFI.dialogManager.show((setState, close) {
    //     void setMode(String? v) async {
    //       await bind.sessionSetKeyboardMode(id: widget.id, value: v ?? "");
    //       setState(() => current = v ?? '');
    //       Future.delayed(Duration(milliseconds: 300), close);
    //     }
    //
    //     return CustomAlertDialog(
    //         title: Text(translate('Physical Keyboard Input Mode')),
    //         content: Column(mainAxisSize: MainAxisSize.min, children: [
    //           getRadio('Legacy mode', 'legacy', current, setMode),
    //           getRadio('Map mode', 'map', current, setMode),
    //         ]));
    //   }, clickMaskDismiss: true);
    // }
  }
  
  class KeyHelpTools extends StatefulWidget {
    final bool keyboardIsVisible;
    final bool showGestureHelp;
  
    /// need to show by external request, etc [keyboardIsVisible] or [changeTouchMode]
    bool get requestShow => keyboardIsVisible || showGestureHelp;
  
    KeyHelpTools(
        {required this.keyboardIsVisible, required this.showGestureHelp});
  
    @override
    State<KeyHelpTools> createState() => _KeyHelpToolsState();
  }
  
  class _KeyHelpToolsState extends State<KeyHelpTools> {
    var _more = true;
    var _fn = false;
    var _pin = false;
    final _keyboardVisibilityController = KeyboardVisibilityController();
    final _key = GlobalKey();
  
    InputModel get inputModel => gFFI.inputModel;
  
    Widget wrap(String text, void Function() onPressed,
        {bool? active, IconData? icon}) {
      return TextButton(
          style: TextButton.styleFrom(
            minimumSize: Size(0, 0),
            padding: EdgeInsets.symmetric(vertical: 10, horizontal: 9.75),
            //adds padding inside the button
            tapTargetSize: MaterialTapTargetSize.shrinkWrap,
            //limits the touch area to the button area
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(5.0),
            ),
            backgroundColor: active == true ? MyTheme.accent80 : null,
          ),
          child: icon != null
              ? Icon(icon, size: 14, color: Colors.white)
              : Text(translate(text),
                  style: TextStyle(color: Colors.white, fontSize: 11)),
          onPressed: onPressed);
    }
  
    _updateRect() {
      RenderObject? renderObject = _key.currentContext?.findRenderObject();
      if (renderObject == null) {
        return;
      }
      if (renderObject is RenderBox) {
        final size = renderObject.size;
        Offset pos = renderObject.localToGlobal(Offset.zero);
        gFFI.cursorModel.keyHelpToolsVisibilityChanged(
            Rect.fromLTWH(pos.dx, pos.dy, size.width, size.height),
            widget.keyboardIsVisible);
      }
    }
  
    @override
    Widget build(BuildContext context) {
      final hasModifierOn = inputModel.ctrl ||
          inputModel.alt ||
          inputModel.shift ||
          inputModel.command;
  
      if (!_pin && !hasModifierOn && !widget.requestShow) {
        gFFI.cursorModel
            .keyHelpToolsVisibilityChanged(null, widget.keyboardIsVisible);
        return Offstage();
      }
      final size = MediaQuery.of(context).size;
  
      final pi = gFFI.ffiModel.pi;
      final isMac = pi.platform == kPeerPlatformMacOS;
      final isWin = pi.platform == kPeerPlatformWindows;
      final isLinux = pi.platform == kPeerPlatformLinux;
      final modifiers = <Widget>[
        wrap('Ctrl ', () {
          setState(() => inputModel.ctrl = !inputModel.ctrl);
        }, active: inputModel.ctrl),
        wrap(' Alt ', () {
          setState(() => inputModel.alt = !inputModel.alt);
        }, active: inputModel.alt),
        wrap('Shift', () {
          setState(() => inputModel.shift = !inputModel.shift);
        }, active: inputModel.shift),
        wrap(isMac ? ' Cmd ' : ' Win ', () {
          setState(() => inputModel.command = !inputModel.command);
        }, active: inputModel.command),
      ];
      final keys = <Widget>[
        wrap(
            ' Fn ',
            () => setState(
                  () {
                    _fn = !_fn;
                    if (_fn) {
                      _more = false;
                    }
                  },
                ),
            active: _fn),
        wrap(
            '',
            () => setState(
                  () => _pin = !_pin,
                ),
            active: _pin,
            icon: Icons.push_pin),
        wrap(
            ' ... ',
            () => setState(
                  () {
                    _more = !_more;
                    if (_more) {
                      _fn = false;
                    }
                  },
                ),
            active: _more),
      ];
      final fn = <Widget>[
        SizedBox(width: 9999),
      ];
      for (var i = 1; i <= 12; ++i) {
        final name = 'F$i';
        fn.add(wrap(name, () {
          inputModel.inputKey('VK_$name');
        }));
      }
      final more = <Widget>[
        SizedBox(width: 9999),
        wrap('Esc', () {
          inputModel.inputKey('VK_ESCAPE');
        }),
        wrap('Tab', () {
          inputModel.inputKey('VK_TAB');
        }),
        wrap('Home', () {
          inputModel.inputKey('VK_HOME');
        }),
        wrap('End', () {
          inputModel.inputKey('VK_END');
        }),
        wrap('Ins', () {
          inputModel.inputKey('VK_INSERT');
        }),
        wrap('Del', () {
          inputModel.inputKey('VK_DELETE');
        }),
        wrap('PgUp', () {
          inputModel.inputKey('VK_PRIOR');
        }),
        wrap('PgDn', () {
          inputModel.inputKey('VK_NEXT');
        }),
        // to-do: support PrtScr on Mac
        if (isWin || isLinux)
          wrap('PrtScr', () {
            inputModel.inputKey('VK_SNAPSHOT');
          }),
        if (isWin || isLinux)
          wrap('ScrollLock', () {
            inputModel.inputKey('VK_SCROLL');
          }),
        if (isWin || isLinux)
          wrap('Pause', () {
            inputModel.inputKey('VK_PAUSE');
          }),
        if (isWin || isLinux)
          // Maybe it's better to call it "Menu"
          // https://en.wikipedia.org/wiki/Menu_key
          wrap('Menu', () {
            inputModel.inputKey('Apps');
          }),
        wrap('Enter', () {
          inputModel.inputKey('VK_ENTER');
        }),
        SizedBox(width: 9999),
        wrap('', () {
          inputModel.inputKey('VK_LEFT');
        }, icon: Icons.keyboard_arrow_left),
        wrap('', () {
          inputModel.inputKey('VK_UP');
        }, icon: Icons.keyboard_arrow_up),
        wrap('', () {
          inputModel.inputKey('VK_DOWN');
        }, icon: Icons.keyboard_arrow_down),
        wrap('', () {
          inputModel.inputKey('VK_RIGHT');
        }, icon: Icons.keyboard_arrow_right),
        wrap(isMac ? 'Cmd+C' : 'Ctrl+C', () {
          sendPrompt(isMac, 'VK_C');
        }),
        wrap(isMac ? 'Cmd+V' : 'Ctrl+V', () {
          sendPrompt(isMac, 'VK_V');
        }),
        wrap(isMac ? 'Cmd+S' : 'Ctrl+S', () {
          sendPrompt(isMac, 'VK_S');
        }),
      ];
      final space = size.width > 320 ? 4.0 : 2.0;
      // 500 ms is long enough for this widget to be built!
      Future.delayed(Duration(milliseconds: 500), () {
        _updateRect();
      });
      return Container(
          key: _key,
          color: Color(0xAA000000),
          padding: EdgeInsets.only(
              top: _keyboardVisibilityController.isVisible ? 24 : 4, bottom: 8),
          child: Wrap(
            spacing: space,
            runSpacing: space,
            children: <Widget>[SizedBox(width: 9999)] +
                modifiers +
                keys +
                (_fn ? fn : []) +
                (_more ? more : []),
          ));
    }
  }
  
  class ImagePaint extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
      final m = Provider.of<ImageModel>(context);
      final c = Provider.of<CanvasModel>(context);
      var s = c.scale;
      final adjust = c.getAdjustY();
      return CustomPaint(
        painter: ImagePainter(
            image: m.image, x: c.x / s, y: (c.y + adjust) / s, scale: s),
      );
    }
  }
  
  class CursorPaint extends StatelessWidget {
    late final String id;
    CursorPaint(this.id);
  
    @override
    Widget build(BuildContext context) {
      final m = Provider.of<CursorModel>(context);
      final c = Provider.of<CanvasModel>(context);
      final ffiModel = Provider.of<FfiModel>(context);
      final s = c.scale;
      double hotx = m.hotx;
      double hoty = m.hoty;
      var image = m.image;
      if (image == null) {
        if (preDefaultCursor.image != null) {
          image = preDefaultCursor.image;
          hotx = preDefaultCursor.image!.width / 2;
          hoty = preDefaultCursor.image!.height / 2;
        }
      }
      if (preForbiddenCursor.image != null &&
          !ffiModel.viewOnly &&
          !ffiModel.keyboard &&
          !ShowRemoteCursorState.find(id).value) {
        image = preForbiddenCursor.image;
        hotx = preForbiddenCursor.image!.width / 2;
        hoty = preForbiddenCursor.image!.height / 2;
      }
      if (image == null) {
        return Offstage();
      }
  
      final minSize = 12.0;
      double mins =
          minSize / (image.width > image.height ? image.width : image.height);
      double factor = 1.0;
      if (s < mins) {
        factor = s / mins;
      }
      final s2 = s < mins ? mins : s;
      final adjust = c.getAdjustY();
      return CustomPaint(
        painter: ImagePainter(
            image: image,
            x: (m.x - hotx) * factor + c.x / s2,
            y: (m.y - hoty) * factor + (c.y + adjust) / s2,
            scale: s2),
      );
    }
  }
  
  void showOptions(
      BuildContext context, String id, OverlayDialogManager dialogManager) async {
    var displays = <Widget>[];
    final pi = gFFI.ffiModel.pi;
    final image = gFFI.ffiModel.getConnectionImageText();
    if (image != null) {
      displays.add(Padding(padding: const EdgeInsets.only(top: 8), child: image));
    }
    if (pi.displays.length > 1 && pi.currentDisplay != kAllDisplayValue) {
      final cur = pi.currentDisplay;
      final children = <Widget>[];
      for (var i = 0; i < pi.displays.length; ++i) {
        children.add(InkWell(
            onTap: () {
              if (i == cur) return;
              openMonitorInTheSameTab(i, gFFI, pi);
              gFFI.dialogManager.dismissAll();
            },
            child: Ink(
                width: 40,
                height: 40,
                decoration: BoxDecoration(
                    border: Border.all(color: Theme.of(context).hintColor),
                    borderRadius: BorderRadius.circular(2),
                    color: i == cur
                        ? Theme.of(context).primaryColor.withOpacity(0.6)
                        : null),
                child: Center(
                    child: Text((i + 1).toString(),
                        style: TextStyle(
                            color: i == cur ? Colors.white : Colors.black87,
                            fontWeight: FontWeight.bold))))));
      }
      displays.add(Padding(
          padding: const EdgeInsets.only(top: 8),
          child: Wrap(
            alignment: WrapAlignment.center,
            spacing: 8,
            children: children,
          )));
    }
    if (displays.isNotEmpty) {
      displays.add(const Divider(color: MyTheme.border));
    }
  
    List<TRadioMenu<String>> viewStyleRadios =
        await toolbarViewStyle(context, id, gFFI);
    List<TRadioMenu<String>> imageQualityRadios =
        await toolbarImageQuality(context, id, gFFI);
    List<TRadioMenu<String>> codecRadios = await toolbarCodec(context, id, gFFI);
    List<TToggleMenu> cursorToggles = await toolbarCursor(context, id, gFFI);
    List<TToggleMenu> displayToggles =
        await toolbarDisplayToggle(context, id, gFFI);
  
    List<TToggleMenu> privacyModeList = [];
    // privacy mode
    final privacyModeState = PrivacyModeState.find(id);
    if (gFFI.ffiModel.keyboard && gFFI.ffiModel.pi.features.privacyMode) {
      privacyModeList = toolbarPrivacyMode(privacyModeState, context, id, gFFI);
      if (privacyModeList.length == 1) {
        displayToggles.add(privacyModeList[0]);
      }
    }
  
    dialogManager.show((setState, close, context) {
      var viewStyle =
          (viewStyleRadios.isNotEmpty ? viewStyleRadios[0].groupValue : '').obs;
      var imageQuality =
          (imageQualityRadios.isNotEmpty ? imageQualityRadios[0].groupValue : '')
              .obs;
      var codec = (codecRadios.isNotEmpty ? codecRadios[0].groupValue : '').obs;
      final radios = [
        for (var e in viewStyleRadios)
          Obx(() => getRadio<String>(
              e.child,
              e.value,
              viewStyle.value,
              e.onChanged != null
                  ? (v) {
                      e.onChanged?.call(v);
                      if (v != null) viewStyle.value = v;
                    }
                  : null)),
        const Divider(color: MyTheme.border),
        for (var e in imageQualityRadios)
          Obx(() => getRadio<String>(
              e.child,
              e.value,
              imageQuality.value,
              e.onChanged != null
                  ? (v) {
                      e.onChanged?.call(v);
                      if (v != null) imageQuality.value = v;
                    }
                  : null)),
        const Divider(color: MyTheme.border),
        for (var e in codecRadios)
          Obx(() => getRadio<String>(
              e.child,
              e.value,
              codec.value,
              e.onChanged != null
                  ? (v) {
                      e.onChanged?.call(v);
                      if (v != null) codec.value = v;
                    }
                  : null)),
        if (codecRadios.isNotEmpty) const Divider(color: MyTheme.border),
      ];
      final rxCursorToggleValues = cursorToggles.map((e) => e.value.obs).toList();
      final cursorTogglesList = cursorToggles
          .asMap()
          .entries
          .map((e) => Obx(() => CheckboxListTile(
              contentPadding: EdgeInsets.zero,
              visualDensity: VisualDensity.compact,
              value: rxCursorToggleValues[e.key].value,
              onChanged: e.value.onChanged != null
                  ? (v) {
                      e.value.onChanged?.call(v);
                      if (v != null) rxCursorToggleValues[e.key].value = v;
                    }
                  : null,
              title: e.value.child)))
          .toList();
  
      final rxToggleValues = displayToggles.map((e) => e.value.obs).toList();
      final displayTogglesList = displayToggles
          .asMap()
          .entries
          .map((e) => Obx(() => CheckboxListTile(
              contentPadding: EdgeInsets.zero,
              visualDensity: VisualDensity.compact,
              value: rxToggleValues[e.key].value,
              onChanged: e.value.onChanged != null
                  ? (v) {
                      e.value.onChanged?.call(v);
                      if (v != null) rxToggleValues[e.key].value = v;
                    }
                  : null,
              title: e.value.child)))
          .toList();
      final toggles = [
        ...cursorTogglesList,
        if (cursorToggles.isNotEmpty) const Divider(color: MyTheme.border),
        ...displayTogglesList,
      ];
  
      Widget privacyModeWidget = Offstage();
      if (privacyModeList.length > 1) {
        privacyModeWidget = ListTile(
          contentPadding: EdgeInsets.zero,
          visualDensity: VisualDensity.compact,
          title: Text(translate('Privacy mode')),
          onTap: () => setPrivacyModeDialog(
              dialogManager, privacyModeList, privacyModeState),
        );
      }
  
      var popupDialogMenus = List<Widget>.empty(growable: true);
      final resolution = getResolutionMenu(gFFI, id);
      if (resolution != null) {
        popupDialogMenus.add(ListTile(
          contentPadding: EdgeInsets.zero,
          visualDensity: VisualDensity.compact,
          title: resolution.child,
          onTap: () {
            close();
            resolution.onPressed?.call();
          },
        ));
      }
      final virtualDisplayMenu = getVirtualDisplayMenu(gFFI, id);
      if (virtualDisplayMenu != null) {
        popupDialogMenus.add(ListTile(
          contentPadding: EdgeInsets.zero,
          visualDensity: VisualDensity.compact,
          title: virtualDisplayMenu.child,
          onTap: () {
            close();
            virtualDisplayMenu.onPressed?.call();
          },
        ));
      }
      if (popupDialogMenus.isNotEmpty) {
        popupDialogMenus.add(const Divider(color: MyTheme.border));
      }
  
      return CustomAlertDialog(
        content: Column(
            mainAxisSize: MainAxisSize.min,
            children: displays +
                radios +
                popupDialogMenus +
                toggles +
                [privacyModeWidget]),
      );
    }, clickMaskDismiss: true, backDismiss: true).then((value) {
      _disableAndroidSoftKeyboard();
    });
  }
  
  TTextMenu? getVirtualDisplayMenu(FFI ffi, String id) {
    if (!showVirtualDisplayMenu(ffi)) {
      return null;
    }
    return TTextMenu(
      child: Text(translate("Virtual display")),
      onPressed: () {
        ffi.dialogManager.show((setState, close, context) {
          final children = getVirtualDisplayMenuChildren(ffi, id, close);
          return CustomAlertDialog(
            title: Text(translate('Virtual display')),
            content: Column(
              mainAxisSize: MainAxisSize.min,
              children: children,
            ),
          );
        }, clickMaskDismiss: true, backDismiss: true).then((value) {
          _disableAndroidSoftKeyboard();
        });
      },
    );
  }
  
  TTextMenu? getResolutionMenu(FFI ffi, String id) {
    final ffiModel = ffi.ffiModel;
    final pi = ffiModel.pi;
    final resolutions = pi.resolutions;
    final display = pi.tryGetDisplayIfNotAllDisplay(display: pi.currentDisplay);
  
    final visible =
        ffiModel.keyboard && (resolutions.length > 1) && display != null;
    if (!visible) return null;
  
    return TTextMenu(
      child: Text(translate("Resolution")),
      onPressed: () {
        ffi.dialogManager.show((setState, close, context) {
          final children = resolutions
              .map((e) => getRadio<String>(
                    Text('${e.width}x${e.height}'),
                    '${e.width}x${e.height}',
                    '${display.width}x${display.height}',
                    (value) {
                      close();
                      bind.sessionChangeResolution(
                        sessionId: ffi.sessionId,
                        display: pi.currentDisplay,
                        width: e.width,
                        height: e.height,
                      );
                    },
                  ))
              .toList();
          return CustomAlertDialog(
            title: Text(translate('Resolution')),
            content: Column(
              mainAxisSize: MainAxisSize.min,
              children: children,
            ),
          );
        }, clickMaskDismiss: true, backDismiss: true).then((value) {
          _disableAndroidSoftKeyboard();
        });
      },
    );
  }
  
  void sendPrompt(bool isMac, String key) {
    final old = isMac ? gFFI.inputModel.command : gFFI.inputModel.ctrl;
    if (isMac) {
      gFFI.inputModel.command = true;
    } else {
      gFFI.inputModel.ctrl = true;
    }
    gFFI.inputModel.inputKey(key);
    if (isMac) {
      gFFI.inputModel.command = old;
    } else {
      gFFI.inputModel.ctrl = old;
    }
  }
  
  class FABLocation extends FloatingActionButtonLocation {
    FloatingActionButtonLocation location;
    double offsetX;
    double offsetY;
    FABLocation(this.location, this.offsetX, this.offsetY);
  
    @override
    Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) {
      final offset = location.getOffset(scaffoldGeometry);
      return Offset(offset.dx + offsetX, offset.dy + offsetY);
    }
  }
  