// This floating mouse widget simulates a physical mouse when connecting from mobile to desktop in touch mode.

import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/input_model.dart';
import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_hbb/utils/image.dart';
import 'package:provider/provider.dart';

const int _kDotCount = 60;
const double _kDotAngle = 2 * pi / _kDotCount;
final Color _kDefaultColor = Colors.grey.withOpacity(0.7);
final Color _kDefaultHighlightColor = Colors.white24.withOpacity(0.7);
final Color _kTapDownColor = Color(0xFFD51928).withOpacity(0.7);
const double _baseMouseWidth = 112.0;
const double _baseMouseHeight = 138.0;
const double _kShowPressedScale = 1.2;
const double kScaleMax = 1.8;
const double kScaleMin = 0.8;

double? _tryParseCoordinateFromEvt(Map<String, dynamic>? evt, String key) {
  if (evt == null) return null;
  final coord = evt[key];
  if (coord == null) return null;
  return double.tryParse(coord);
}

class FloatingMouse extends StatefulWidget {
  final FFI ffi;
  const FloatingMouse({
    super.key,
    required this.ffi,
  });

  @override
  State<FloatingMouse> createState() => _FloatingMouseState();
}

class _CanvasScrollState {
  static const double speedPressed = 3.0;
  final InputModel inputModel;
  final CanvasModel canvasModel;
  final int _intervalMillis = 30;
  Timer? _timer;
  double _dx = 0;
  double _dy = 0;
  double _speed = 1.0;
  Rect _displayRect = Rect.zero;
  Offset _mouseGlobalPosition = Offset.zero;

  _CanvasScrollState({required this.inputModel, required this.canvasModel});

  double get step => 5.0 * canvasModel.scale;

  set scrollX(double speed) {
    _dx = step;
    setSpeed(speed);
  }

  set scrollY(double speed) {
    _dy = step;
    setSpeed(speed);
  }

  void tryCancel() {
    _dx = 0;
    _dy = 0;
    if (_timer == null) return;
    _timer?.cancel();
    _timer = null;
  }

  void setPressedSpeed() {
    setSpeed(_speed > 0
        ? _CanvasScrollState.speedPressed
        : -_CanvasScrollState.speedPressed);
  }

  void setReleasedSpeed() {
    setSpeed(_speed > 0 ? 1.0 : -1.0);
  }

  void setSpeed(double newSpeed) {
    _speed = newSpeed;
    if (_speed > 0) {
      _speed = _speed.clamp(0.1, 10.0);
    } else {
      _speed = _speed.clamp(-10.0, -0.1);
    }
    if (_dx != 0) {
      _dx = step * _speed;
    } else if (_dy != 0) {
      _dy = step * _speed;
    }
  }

  void tryStart(Rect displayRect, Offset mouseGlobalPosition) {
    _displayRect = displayRect;
    _mouseGlobalPosition = mouseGlobalPosition;
    if (_timer != null) return;
    _timer = Timer.periodic(Duration(milliseconds: _intervalMillis), (timer) {
      if (_dx == 0 && _dy == 0) {
        tryCancel();
      } else {
        if (_dx != 0) {
          canvasModel.panX(_dx);
        }
        if (_dy != 0) {
          canvasModel.panY(_dy);
        }
        final evt = inputModel.processEventToPeer(
            InputModel.getMouseEventMove(), _mouseGlobalPosition,
            moveCanvas: false);
        if (shouldCancelScrollTimer(evt)) {
          tryCancel();
        }
      }
    });
  }

  bool shouldCancelScrollTimer(Map<String, dynamic>? evt) {
    if (evt == null) {
      return true;
    }
    double s = canvasModel.scale;
    assert(s > 0, 'canvasModel.scale should always be positive');
    if (s <= 0) {
      return true;
    }
    if (_dx != 0) {
      final x = _tryParseCoordinateFromEvt(evt, 'x');
      if (x == null) {
        return true;
      } else {
        if (_dx < 0) {
          if (isDoubleEqual(_displayRect.right - 1, x)) {
            return true;
          } else {
            final dxDisplay = _dx / s;
            if ((x - dxDisplay) > (_displayRect.right - 1)) {
              canvasModel.panX((x - _displayRect.right + 1) * s);
              return true;
            }
          }
        } else {
          if (isDoubleEqual(x, _displayRect.left)) {
            return true;
          } else {
            final dxDisplay = _dx / s;
            if ((x - dxDisplay) < _displayRect.left) {
              canvasModel.panX((x - _displayRect.left) * s);
              return true;
            }
          }
        }
      }
    }
    if (_dy != 0) {
      final y = _tryParseCoordinateFromEvt(evt, 'y');
      if (y == null) {
        return true;
      } else {
        if (_dy < 0) {
          if (isDoubleEqual(_displayRect.bottom - 1, y)) {
            return true;
          } else {
            final dyDisplay = _dy / s;
            if ((y - dyDisplay) > (_displayRect.bottom - 1)) {
              canvasModel.panY((y - _displayRect.bottom + 1) * s);
              return true;
            }
          }
        } else {
          if (isDoubleEqual(y, _displayRect.top)) {
            return true;
          } else {
            final dyDisplay = _dy / s;
            if ((y - dyDisplay) < _displayRect.top) {
              canvasModel.panY((y - _displayRect.top) * s);
              return true;
            }
          }
        }
      }
    }
    return false;
  }
}

class _FloatingMouseState extends State<FloatingMouse> {
  Rect? _lastBlockedRect;
  final GlobalKey _scrollWheelUpKey = GlobalKey();
  final GlobalKey _scrollWheelDownKey = GlobalKey();
  final GlobalKey _mouseWidgetKey = GlobalKey();
  final GlobalKey _cursorPaintKey = GlobalKey();

  Offset _position = Offset.zero;
  bool _isInitialized = false;
  double _baseMouseScale = 1.0;
  double _mouseScale = 1.0;
  bool _isExpanded = true;
  bool _isScrolling = false;
  Offset? _scrollCenter;
  double _snappedPointerAngle = 0.0;
  double? _lastSnappedAngle;
  late final _CanvasScrollState _canvasScrollState;
  Orientation? _previousOrientation;
  Timer? _collapseTimer;
  late final VirtualMouseMode _virtualMouseMode;

  void _resetCollapseTimer() {
    _collapseTimer?.cancel();
    if (_isExpanded) {
      _collapseTimer = Timer(const Duration(seconds: 7), () {
        if (mounted && _isExpanded) {
          final minMouseScale = (_baseMouseScale * 0.3);
          setState(() {
            _mouseScale = minMouseScale;
            _isExpanded = false;
            _position += _expandOffset;
          });
        }
      });
    }
  }

  double get mouseWidth => _baseMouseWidth * _mouseScale;
  double get mouseHeight => _baseMouseHeight * _mouseScale;

  InputModel get _inputModel => widget.ffi.inputModel;
  CursorModel get _cursorModel => widget.ffi.cursorModel;
  CanvasModel get _canvasModel => widget.ffi.canvasModel;

  Offset get _expandOffset =>
      Offset(84 * _baseMouseScale, 12 * _baseMouseScale);

  @override
  void initState() {
    super.initState();
    _virtualMouseMode = widget.ffi.ffiModel.virtualMouseMode;
    _virtualMouseMode.addListener(_onVirtualMouseModeChanged);
    _canvasScrollState =
        _CanvasScrollState(inputModel: _inputModel, canvasModel: _canvasModel);
    _cursorModel.blockEvents = false;
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _resetPosition();
      _resetCollapseTimer();
    });
  }

  void _onVirtualMouseModeChanged() {
    if (mounted) {
      setState(() {
        if (_virtualMouseMode.showVirtualMouse) {
          _isExpanded = true;
          _resetCollapseTimer();
        }
      });
    }
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    final currentOrientation = MediaQuery.of(context).orientation;
    if (_previousOrientation != null &&
        _previousOrientation != currentOrientation) {
      _resetPosition();
    }
    _previousOrientation = currentOrientation;
  }

  void _resetPosition() {
    setState(() {
      final size = MediaQuery.of(context).size;
      _position = Offset(
        (size.width - _baseMouseWidth * _mouseScale) / 2,
        (size.height - _baseMouseHeight * _mouseScale) / 2,
      );
      _isInitialized = true;
    });
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (mounted) _updateBlockedRect();
    });
  }

  @override
  void dispose() {
    if (_lastBlockedRect != null) {
      _cursorModel.removeBlockedRect(_lastBlockedRect!);
    }
    _virtualMouseMode.removeListener(_onVirtualMouseModeChanged);
    _canvasScrollState.tryCancel();
    _cursorModel.blockEvents = false;
    _collapseTimer?.cancel();
    super.dispose();
  }

  void _updateBlockedRect() {
    final context = _mouseWidgetKey.currentContext;
    if (context == null) return;
    final renderBox = context.findRenderObject() as RenderBox?;
    if (renderBox == null || !renderBox.attached) return;

    final newRect = renderBox.localToGlobal(Offset.zero) & renderBox.size;

    if (_lastBlockedRect != null) {
      _cursorModel.removeBlockedRect(_lastBlockedRect!);
    }
    _cursorModel.addBlockedRect(newRect);
    _lastBlockedRect = newRect;
  }

  Offset _getMouseGlobalPosition() {
    final RenderBox? renderBox =
        _cursorPaintKey.currentContext?.findRenderObject() as RenderBox?;
    if (renderBox != null) {
      return renderBox.localToGlobal(Offset.zero);
    } else {
      return _position;
    }
  }

  static Offset? _getPositionFromMouseRetEvt(Map<String, dynamic>? evt) {
    final x = _tryParseCoordinateFromEvt(evt, 'x');
    final y = _tryParseCoordinateFromEvt(evt, 'y');
    if (x == null || y == null) {
      return null;
    }
    return Offset(x, y);
  }

  // Returns true if [value] is within 2.01 pixels of [edge].
  // We need this near check because it can make the auto scroll easier to trigger and control.
  bool _isValueNearEdge(double edge, double value) {
    return (value - edge).abs() < 2.01;
  }

  bool _isValueAtEdge(double edge, double value) {
    return (value - edge).abs() < 0.01;
  }

  bool _isValueAtOrOutsideEdge(double edge, double? value) {
    // If value is null, then consider it outside the edge.
    return value == null || isDoubleEqual(value, edge);
  }

  // If the mouse is very close to the edge of the display,
  // we can only start auto scroll when the mouse is at the edge of the screen.
  bool _shouldAutoScrollIfCursorNearRemoteEdge(double remoteEdge,
      double remoteValue, double localEdge, double localValue) {
    if ((remoteEdge - remoteValue).abs() < 100.0) {
      if (!_isValueAtEdge(localEdge, localValue)) {
        return false;
      }
    }
    return true;
  }

  void _onMoveUpdateDelta(Offset delta) {
    _resetCollapseTimer();
    final context = this.context;
    final size = MediaQuery.of(context).size;
    Offset newPosition = _position + delta;
    double minX = 0;
    double minY = 0;
    double maxX = size.width - mouseWidth;
    double maxY = size.height - mouseHeight;
    newPosition = Offset(
      newPosition.dx.clamp(minX, maxX),
      newPosition.dy.clamp(minY, maxY),
    );
    setState(() {
      final isPositionChanged = !(isDoubleEqual(newPosition.dx, _position.dx) &&
          isDoubleEqual(newPosition.dy, _position.dy));
      _position = newPosition;
      if (!_isExpanded) {
        return;
      }

      Offset? mouseGlobalPosition;
      Offset? positionInRemoteDisplay;
      if (isPositionChanged) {
        mouseGlobalPosition = _getMouseGlobalPosition();
        final evt = _inputModel.handleMouse(
            InputModel.getMouseEventMove(), mouseGlobalPosition,
            moveCanvas: false);
        positionInRemoteDisplay = _getPositionFromMouseRetEvt(evt);
        WidgetsBinding.instance.addPostFrameCallback((_) {
          if (mounted) _updateBlockedRect();
        });
      }

      // Get the display rect
      final displayRect = widget.ffi.ffiModel.displaysRect();
      if (displayRect == null) {
        _canvasScrollState.tryCancel();
        return;
      }

      // Get the mouse global position and position in remote display
      mouseGlobalPosition ??= _getMouseGlobalPosition();
      if (positionInRemoteDisplay == null) {
        final evt = _inputModel.processEventToPeer(
            InputModel.getMouseEventMove(), mouseGlobalPosition,
            moveCanvas: false);
        positionInRemoteDisplay = _getPositionFromMouseRetEvt(evt);
      }

      // Check if need to start auto canvas scroll
      // If:
      // 1. The mouse is near the edge of the screen.
      // 2. The position in remote display is in the rect of the display.
      // 3. If the remote cursor is near the edge of the remote display,
      //    then the local mouse must be at the edge of the screen.
      // Then start auto canvas scroll.
      if (_isValueNearEdge(minX, _position.dx)) {
        bool shouldStartScroll = true;
        if (_isValueAtOrOutsideEdge(
            displayRect.left, positionInRemoteDisplay?.dx)) {
          shouldStartScroll = false;
        }
        if (positionInRemoteDisplay != null) {
          if (!_shouldAutoScrollIfCursorNearRemoteEdge(displayRect.left,
              positionInRemoteDisplay.dx, minX, _position.dx)) {
            shouldStartScroll = false;
          }
        }
        if (!shouldStartScroll) {
          _canvasScrollState.tryCancel();
          return;
        }
        _canvasScrollState.scrollX = 1.0 * _CanvasScrollState.speedPressed;
      } else if (_isValueNearEdge(minY, _position.dy)) {
        bool shouldStartScroll = true;
        if (_isValueAtOrOutsideEdge(
            displayRect.top, positionInRemoteDisplay?.dy)) {
          shouldStartScroll = false;
        }
        if (positionInRemoteDisplay != null) {
          if (!_shouldAutoScrollIfCursorNearRemoteEdge(displayRect.top,
              positionInRemoteDisplay.dy, minY, _position.dy)) {
            shouldStartScroll = false;
          }
        }
        if (!shouldStartScroll) {
          _canvasScrollState.tryCancel();
          return;
        }
        _canvasScrollState.scrollY = 1.0 * _CanvasScrollState.speedPressed;
      } else if (_isValueNearEdge(maxX, _position.dx)) {
        bool shouldStartScroll = true;
        if (_isValueAtOrOutsideEdge(
            displayRect.right - 1, positionInRemoteDisplay?.dx)) {
          shouldStartScroll = false;
        }
        if (positionInRemoteDisplay != null) {
          if (!_shouldAutoScrollIfCursorNearRemoteEdge(displayRect.right - 1,
              positionInRemoteDisplay.dx, maxX, _position.dx)) {
            shouldStartScroll = false;
          }
        }
        if (!shouldStartScroll) {
          _canvasScrollState.tryCancel();
          return;
        }
        _canvasScrollState.scrollX = -1.0 * _CanvasScrollState.speedPressed;
      } else if (_isValueNearEdge(maxY, _position.dy)) {
        bool shouldStartScroll = true;
        if (_isValueAtOrOutsideEdge(
            displayRect.bottom - 1, positionInRemoteDisplay?.dy)) {
          shouldStartScroll = false;
        }
        if (positionInRemoteDisplay != null) {
          if (!_shouldAutoScrollIfCursorNearRemoteEdge(displayRect.bottom - 1,
              positionInRemoteDisplay.dy, maxY, _position.dy)) {
            shouldStartScroll = false;
          }
        }
        if (!shouldStartScroll) {
          _canvasScrollState.tryCancel();
          return;
        }
        _canvasScrollState.scrollY = -1.0 * _CanvasScrollState.speedPressed;
      } else {
        _canvasScrollState.tryCancel();
        return;
      }
      _canvasScrollState.tryStart(displayRect, mouseGlobalPosition);
    });
  }

  void _onDragHandleUpdate(DragUpdateDetails details) =>
      _onMoveUpdateDelta(details.delta);

  void _onBodyPointerMoveUpdate(PointerMoveEvent event) =>
      _onMoveUpdateDelta(event.delta);

  bool _containsPosition(GlobalKey key, Offset pos) {
    final contextScroll = key.currentContext;
    if (contextScroll == null) return false;
    final RenderBox? scrollWheelBox =
        contextScroll.findRenderObject() as RenderBox?;
    if (scrollWheelBox == null || !scrollWheelBox.attached) return false;
    Rect rect = scrollWheelBox.localToGlobal(Offset.zero) & scrollWheelBox.size;
    return rect.contains(pos);
  }

  void _handlePointerDown(PointerDownEvent event) {
    _resetCollapseTimer();
    if (_isScrolling) return;
    if (_containsPosition(_scrollWheelUpKey, event.position) ||
        _containsPosition(_scrollWheelDownKey, event.position)) {
      final contextMouse = _mouseWidgetKey.currentContext;
      if (contextMouse == null) return;
      final RenderBox? mouseBox = contextMouse.findRenderObject() as RenderBox?;
      if (mouseBox == null || !mouseBox.attached) return;

      // Only enter scroll mode when all RenderObjects are available.
      final Offset mouseTopLeft = mouseBox.localToGlobal(Offset.zero);
      final Size mouseSize = mouseBox.size;
      final Offset center =
          mouseTopLeft + Offset(mouseSize.width / 2, mouseSize.height / 2);

      final vector = event.position - center;
      final rawAngle = atan2(vector.dy, vector.dx);

      final closestDotIndex = (rawAngle / _kDotAngle).round();
      _lastSnappedAngle = closestDotIndex * _kDotAngle;

      setState(() {
        _isScrolling = true;
        _cursorModel.blockEvents = true;
        _scrollCenter = center;
        _snappedPointerAngle = _lastSnappedAngle!;
      });
    }
  }

  void _handlePointerMove(PointerMoveEvent event) {
    _resetCollapseTimer();
    if (!_isScrolling || _scrollCenter == null || _lastSnappedAngle == null) {
      return;
    }

    final touchPosition = event.position;
    final vector = touchPosition - _scrollCenter!;
    final rawCurrentAngle = atan2(vector.dy, vector.dx);

    final closestDotIndex = (rawCurrentAngle / _kDotAngle).round();
    final snappedCurrentAngle = closestDotIndex * _kDotAngle;

    if (snappedCurrentAngle == _lastSnappedAngle) return;

    double deltaAngle = snappedCurrentAngle - _lastSnappedAngle!;

    if (deltaAngle.abs() > pi) {
      deltaAngle = (deltaAngle > 0) ? deltaAngle - 2 * pi : deltaAngle + 2 * pi;
    }

    _lastSnappedAngle = snappedCurrentAngle;

    setState(() {
      _snappedPointerAngle = snappedCurrentAngle;
      _inputModel.scroll(deltaAngle > 0 ? -1 : 1);
    });
  }

  void _tryCancelScrolling() {
    _resetCollapseTimer();
    if (!_isScrolling) return;
    setState(() {
      _isScrolling = false;
      _cursorModel.blockEvents = false;
      _lastSnappedAngle = null;
      _scrollCenter = null;
    });
  }

  void _handlePointerUp(PointerUpEvent event) => _tryCancelScrolling();
  void _handlePointerCancel(PointerCancelEvent event) => _tryCancelScrolling();

  @override
  Widget build(BuildContext context) {
    if (!_isInitialized) {
      return const Offstage();
    }
    final virtualMouseMode = _virtualMouseMode;
    if (!virtualMouseMode.showVirtualMouse) {
      return const Offstage();
    }
    _baseMouseScale = virtualMouseMode.virtualMouseScale;
    if (_isExpanded) {
      _mouseScale = _baseMouseScale;
    } else {
      final minMouseScale = (_baseMouseScale * 0.3);
      _mouseScale = minMouseScale;
    }
    return Listener(
      onPointerDown: _isExpanded ? _handlePointerDown : null,
      onPointerMove: _handlePointerMove,
      onPointerUp: _handlePointerUp,
      onPointerCancel: _handlePointerCancel,
      behavior: HitTestBehavior.translucent,
      child: Stack(
        children: [
          if (!_isScrolling)
            Positioned(
              left: _position.dx,
              top: _position.dy,
              child: _buildMouseWithHide(),
            ),
          if (_isScrolling && _scrollCenter != null)
            Positioned.fill(
              child: Builder(
                builder: (context) {
                  final RenderBox? customPaintBox =
                      context.findRenderObject() as RenderBox?;
                  if (customPaintBox == null || !customPaintBox.attached) {
                    WidgetsBinding.instance.addPostFrameCallback((_) {
                      if (mounted && _isScrolling) setState(() {});
                    });
                    return const SizedBox.expand();
                  }
                  final Offset customPaintTopLeft =
                      customPaintBox.localToGlobal(Offset.zero);
                  final Offset localCenter =
                      _scrollCenter! - customPaintTopLeft;
                  return CustomPaint(
                    painter: DottedCirclePainter(
                      center: localCenter,
                      pointerAngle: _snappedPointerAngle,
                      scale: _mouseScale,
                    ),
                  );
                },
              ),
            ),
        ],
      ),
    );
  }

  Widget _buildMouseWithHide() {
    double minMouseScale = (_baseMouseScale * 0.3);
    if (!_isExpanded) {
      return SizedBox(
          width: mouseWidth,
          height: mouseHeight,
          child: GestureDetector(
            onPanUpdate: _onDragHandleUpdate,
            onTap: () {
              setState(() {
                _mouseScale = _baseMouseScale;
                _isExpanded = true;
                _position -= _expandOffset;
              });
              _resetCollapseTimer();
            },
            child: MouseBody(
              scrollWheelUpKey: _scrollWheelUpKey,
              scrollWheelDownKey: _scrollWheelDownKey,
              mouseWidgetKey: _mouseWidgetKey,
              inputModel: _isExpanded ? _inputModel : null,
              scale: _mouseScale,
              resetCollapseTimer: _resetCollapseTimer,
            ),
          ));
    } else {
      return SizedBox(
        width: mouseWidth,
        height: mouseHeight,
        child: Column(
          children: [
            Row(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                CursorPaint(
                  key: _cursorPaintKey,
                  scale: _mouseScale,
                ),
                const Spacer(),
                GestureDetector(
                  onTap: () {
                    _collapseTimer?.cancel();
                    setState(() {
                      _mouseScale = minMouseScale;
                      _isExpanded = false;
                      _position += _expandOffset;
                    });
                  },
                  child: Container(
                    width: 18 * _mouseScale,
                    height: 18 * _mouseScale,
                    child: Center(
                      child: Container(
                        width: 14 * _mouseScale,
                        height: 14 * _mouseScale,
                        decoration: const BoxDecoration(
                          color: Colors.grey,
                          shape: BoxShape.circle,
                        ),
                        alignment: Alignment.center,
                        child: Icon(Icons.close,
                            color: Colors.white, size: 12 * _mouseScale),
                      ),
                    ),
                  ),
                ),
              ],
            ),
            Padding(
                padding: EdgeInsets.only(left: 14 * _mouseScale),
                child: MouseBody(
                  scrollWheelUpKey: _scrollWheelUpKey,
                  scrollWheelDownKey: _scrollWheelDownKey,
                  mouseWidgetKey: _mouseWidgetKey,
                  onPointerMoveUpdate: _onBodyPointerMoveUpdate,
                  cancelCanvasScroll: _canvasScrollState.tryCancel,
                  setCanvasScrollPressed: _canvasScrollState.setPressedSpeed,
                  setCanvasScrollReleased: _canvasScrollState.setReleasedSpeed,
                  inputModel: _isExpanded ? _inputModel : null,
                  scale: _mouseScale,
                  resetCollapseTimer: _resetCollapseTimer,
                )),
          ],
        ),
      );
    }
  }
}

class MouseBody extends StatefulWidget {
  final GlobalKey scrollWheelUpKey;
  final GlobalKey scrollWheelDownKey;
  final GlobalKey mouseWidgetKey;
  final Function(PointerMoveEvent)? onPointerMoveUpdate;
  final Function()? cancelCanvasScroll;
  final Function()? setCanvasScrollPressed;
  final Function()? setCanvasScrollReleased;
  final InputModel? inputModel;
  final double scale;
  final Function()? resetCollapseTimer;
  const MouseBody({
    super.key,
    required this.scrollWheelUpKey,
    required this.scrollWheelDownKey,
    required this.mouseWidgetKey,
    required this.scale,
    this.inputModel,
    this.onPointerMoveUpdate,
    this.cancelCanvasScroll,
    this.setCanvasScrollPressed,
    this.setCanvasScrollReleased,
    this.resetCollapseTimer,
  });

  @override
  State<MouseBody> createState() => _MouseBodyState();
}

class WidgetScale {
  final double scale;
  final double translateScale;

  const WidgetScale({required this.scale, required this.translateScale});

  static WidgetScale getScale(bool down, double s) {
    if (down) {
      return WidgetScale(
          scale: s * _kShowPressedScale,
          translateScale: s * (_kShowPressedScale - 1.0) * 0.5);
    } else {
      return WidgetScale(scale: s, translateScale: 0.0);
    }
  }
}

class _MouseBodyState extends State<MouseBody> {
  bool _leftDown = false;
  bool _rightDown = false;
  bool _midDown = false;
  bool _dragDown = false;

  Widget _buildScrollUpDown(GlobalKey key, IconData iconData, double s) {
    return Container(
      key: key,
      height: 17 * s,
      child: Icon(
        iconData,
        color: _kDefaultHighlightColor,
        size: 14 * s,
      ),
    );
  }

  Widget _buildScrollMidButton(double s) {
    return Listener(
      onPointerDown: widget.inputModel != null
          ? (event) {
              widget.resetCollapseTimer?.call();
              setState(() {
                _midDown = true;
                widget.inputModel?.tapDown(MouseButtons.wheel);
              });
            }
          : null,
      onPointerUp: widget.inputModel != null
          ? (event) {
              setState(() {
                _midDown = false;
                widget.inputModel?.tapUp(MouseButtons.wheel);
                widget.cancelCanvasScroll?.call();
              });
            }
          : null,
      onPointerCancel: widget.inputModel != null
          ? (event) {
              setState(() {
                _midDown = false;
                widget.inputModel?.tapUp(MouseButtons.wheel);
                widget.cancelCanvasScroll?.call();
              });
            }
          : null,
      onPointerMove: widget.onPointerMoveUpdate,
      behavior: HitTestBehavior.opaque,
      child: Container(
        height: 28 * s,
        child: Center(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Container(
                width: 6 * s,
                height: 2 * s,
                color: _kDefaultHighlightColor,
              ),
              SizedBox(height: 3 * s),
              Container(
                width: 8 * s,
                height: 2 * s,
                color: _kDefaultHighlightColor,
              ),
              SizedBox(height: 3 * s),
              Container(
                width: 6 * s,
                height: 2 * s,
                color: _kDefaultHighlightColor,
              ),
            ],
          ),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    final s = widget.scale;
    final leftScale = WidgetScale.getScale(_leftDown, s);
    final rightScale = WidgetScale.getScale(_rightDown, s);
    final midScale = WidgetScale.getScale(_midDown, s);
    return Row(
      children: [
        SizedBox(
          key: widget.mouseWidgetKey,
          width: 80 * s,
          height: 120 * s,
          child: Column(
            children: [
              SizedBox(
                height: 55 * s,
                child: Stack(
                  clipBehavior: Clip.none,
                  children: [
                    Row(
                      crossAxisAlignment: CrossAxisAlignment.end,
                      children: [
                        // Left button
                        Transform.translate(
                          offset: Offset(
                              -(80 - 24) * 0.5 * leftScale.translateScale,
                              -32 * leftScale.translateScale),
                          child: SizedBox(
                            width: (80 - 24) * 0.5 * leftScale.scale,
                            child: Listener(
                              onPointerMove: widget.onPointerMoveUpdate,
                              onPointerDown: widget.inputModel != null
                                  ? (event) {
                                      widget.resetCollapseTimer?.call();
                                      setState(() {
                                        _leftDown = true;
                                        widget.inputModel
                                            ?.tapDown(MouseButtons.left);
                                      });
                                    }
                                  : null,
                              onPointerUp: widget.inputModel != null
                                  ? (event) => setState(() {
                                        _leftDown = false;
                                        widget.inputModel
                                            ?.tapUp(MouseButtons.left);
                                        widget.cancelCanvasScroll?.call();
                                      })
                                  : null,
                              onPointerCancel: widget.inputModel != null
                                  ? (event) => setState(() {
                                        _leftDown = false;
                                        widget.inputModel
                                            ?.tapUp(MouseButtons.left);
                                        widget.cancelCanvasScroll?.call();
                                      })
                                  : null,
                              child: Container(
                                decoration: BoxDecoration(
                                  color: _leftDown
                                      ? _kTapDownColor
                                      : _kDefaultColor,
                                  borderRadius: BorderRadius.only(
                                      topLeft: Radius.circular(22 * s)),
                                ),
                                margin: EdgeInsets.only(right: 0.5 * s),
                              ),
                            ),
                          ),
                        ),
                        const Spacer(),
                        Transform.translate(
                          offset: Offset(
                              (80 - 24) * 0.5 * rightScale.translateScale,
                              -32 * rightScale.translateScale),
                          child: SizedBox(
                            width: (80 - 24) * 0.5 * rightScale.scale,
                            child: Listener(
                              onPointerMove: widget.onPointerMoveUpdate,
                              onPointerDown: widget.inputModel != null
                                  ? (event) {
                                      widget.resetCollapseTimer?.call();
                                      setState(() {
                                        _rightDown = true;
                                        widget.inputModel
                                            ?.tapDown(MouseButtons.right);
                                      });
                                    }
                                  : null,
                              onPointerUp: widget.inputModel != null
                                  ? (event) => setState(() {
                                        _rightDown = false;
                                        widget.inputModel
                                            ?.tapUp(MouseButtons.right);
                                        widget.cancelCanvasScroll?.call();
                                      })
                                  : null,
                              onPointerCancel: widget.inputModel != null
                                  ? (event) => setState(() {
                                        _rightDown = false;
                                        widget.inputModel
                                            ?.tapUp(MouseButtons.right);
                                        widget.cancelCanvasScroll?.call();
                                      })
                                  : null,
                              child: Container(
                                decoration: BoxDecoration(
                                  color: _rightDown
                                      ? _kTapDownColor
                                      : _kDefaultColor,
                                  borderRadius: BorderRadius.only(
                                      topRight: Radius.circular(22 * s)),
                                ),
                                margin: EdgeInsets.only(left: 0.5 * s),
                              ),
                            ),
                          ),
                        ),
                      ],
                    ),
                    // Middle function area overflows Row bottom
                    Positioned(
                      left: (80 * s - 22 * s) / 2,
                      top: 0,
                      child: Transform.translate(
                        offset: Offset(0, -2 * s),
                        child: Container(
                          width: 22 * s,
                          height: 67 * s,
                          decoration: BoxDecoration(
                            color: Colors.grey.withOpacity(0.7),
                            borderRadius: BorderRadius.vertical(
                              top: Radius.circular(12 * s),
                              bottom: Radius.circular(16 * s),
                            ),
                          ),
                          padding: EdgeInsets.symmetric(vertical: 2 * s),
                          child: Column(
                            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                            children: [
                              _buildScrollUpDown(widget.scrollWheelUpKey,
                                  Icons.keyboard_arrow_up, midScale.scale),
                              _buildScrollMidButton(midScale.scale),
                              _buildScrollUpDown(widget.scrollWheelDownKey,
                                  Icons.keyboard_arrow_down, midScale.scale),
                            ],
                          ),
                        ),
                      ),
                    ),
                  ],
                ),
              ),
              // Thin gap separates upper and lower parts
              SizedBox(height: 1 * s),
              // Bottom part: drag area (top middle indentation)
              Expanded(
                child: Listener(
                  onPointerMove: widget.onPointerMoveUpdate,
                  onPointerDown: widget.inputModel != null
                      ? (event) {
                          widget.resetCollapseTimer?.call();
                          setState(() {
                            _dragDown = true;
                          });
                          widget.setCanvasScrollPressed?.call();
                        }
                      : null,
                  onPointerUp: widget.inputModel != null
                      ? (event) {
                          setState(() {
                            _dragDown = false;
                          });
                          widget.setCanvasScrollReleased?.call();
                        }
                      : null,
                  onPointerCancel: widget.inputModel != null
                      ? (event) {
                          setState(() {
                            _dragDown = false;
                          });
                          widget.setCanvasScrollReleased?.call();
                        }
                      : null,
                  behavior: HitTestBehavior.opaque,
                  child: CustomPaint(
                    painter: DragAreaTopIndentPainter(
                        color: _dragDown ? _kTapDownColor : _kDefaultColor,
                        scale: widget.scale),
                    child: Container(
                      width: 80 * s,
                      alignment: Alignment.center,
                      child: Transform.rotate(
                        angle: pi / 2,
                        child: Icon(Icons.drag_indicator,
                            color: _kDefaultHighlightColor, size: 18 * s),
                      ),
                    ),
                  ),
                ),
              ),
            ],
          ),
        ),
        const Spacer()
      ],
    );
  }
}

class DottedCirclePainter extends CustomPainter {
  final Offset center;
  final double pointerAngle;
  final double scale;
  final Offset? scrollWheelCenter;

  DottedCirclePainter(
      {required this.center,
      required this.pointerAngle,
      required this.scale,
      this.scrollWheelCenter});

  @override
  void paint(Canvas canvas, Size size) {
    final radius = 48.0 * scale;
    final circlePaint = Paint()
      ..color = Colors.grey.shade400
      ..style = PaintingStyle.fill;
    final pointerPaint = Paint()
      ..color = Color(0xFFD51928)
      ..style = PaintingStyle.fill;

    const dotRadius = 2.5;
    for (int i = 0; i < _kDotCount; i += 3) {
      final angle = i * _kDotAngle;
      final dotX = center.dx + radius * cos(angle);
      final dotY = center.dy + radius * sin(angle);
      canvas.drawCircle(Offset(dotX, dotY), dotRadius, circlePaint);
    }

    final pointerX = center.dx + radius * cos(pointerAngle);
    final pointerY = center.dy + radius * sin(pointerAngle);
    final pointerPosition = Offset(pointerX, pointerY);
    canvas.drawCircle(pointerPosition, 8.0, pointerPaint);
  }

  @override
  bool shouldRepaint(covariant DottedCirclePainter oldDelegate) {
    return oldDelegate.pointerAngle != pointerAngle ||
        oldDelegate.center != center ||
        oldDelegate.scrollWheelCenter != scrollWheelCenter;
  }
}

// Painter for the bottom center indentation of the drag area
class BottomIndentPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.grey.withOpacity(0.7)
      ..style = PaintingStyle.fill;
    // Draw bottom semicircle
    final center = Offset(size.width / 2, size.height);
    canvas.drawArc(
      Rect.fromCenter(center: center, width: size.width, height: size.height),
      pi,
      pi,
      false,
      paint,
    );
    // Use background color to carve a circular notch in the middle
    final clearPaint = Paint()..blendMode = BlendMode.clear;
    canvas.drawCircle(Offset(size.width / 2, size.height - 10), 10, clearPaint);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

// Painter for the top center indentation of the drag area
class DragAreaTopIndentPainter extends CustomPainter {
  final double scale;
  final Color color;
  DragAreaTopIndentPainter({required this.color, required this.scale});

  @override
  void paint(Canvas canvas, Size size) {
    // Use saveLayer to make the hollow part transparent
    final paint = Paint()
      ..color = color
      ..style = PaintingStyle.fill;
    canvas.saveLayer(Offset.zero & size, Paint());
    // Draw drag area main body (rectangle + bottom rounded corners)
    final rect = Rect.fromLTWH(0, 0, size.width, size.height);
    final rrect = RRect.fromRectAndCorners(
      rect,
      bottomLeft: Radius.circular(40 * scale),
      bottomRight: Radius.circular(40 * scale),
    );
    canvas.drawRRect(rrect, paint);
    // Use BlendMode.dstOut to carve a smaller semicircular notch at the top center
    final clearPaint = Paint()..blendMode = BlendMode.dstOut;
    canvas.drawArc(
      Rect.fromCenter(
          center: Offset(size.width / 2, 0),
          width: 25 * scale,
          height: 20 * scale),
      0,
      pi,
      false,
      clearPaint,
    );
    canvas.restore();
  }

  @override
  bool shouldRepaint(covariant DragAreaTopIndentPainter oldDelegate) {
    return oldDelegate.color != color || oldDelegate.scale != scale;
  }
}

class CursorPaint extends StatelessWidget {
  final double scale;
  CursorPaint({super.key, required this.scale});

  @override
  Widget build(BuildContext context) {
    final cursorModel = Provider.of<CursorModel>(context);
    double hotx = cursorModel.hotx;
    double hoty = cursorModel.hoty;
    var image = cursorModel.image;
    if (image == null) {
      if (preDefaultCursor.image != null) {
        image = preDefaultCursor.image;
        hotx = preDefaultCursor.image!.width / 2;
        hoty = preDefaultCursor.image!.height / 2;
      }
    }
    if (image == null) {
      return const Offstage();
    }
    assert(scale > 0, 'scale should always be positive');
    if (scale <= 0) {
      return const Offstage();
    }
    return CustomPaint(
      painter: ImagePainter(image: image, x: -hotx, y: -hoty, scale: scale),
    );
  }
}
