/*
 * This file is part of PiliPlus
 *
 * PiliPlus is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * PiliPlus is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with PiliPlus.  If not, see <https://www.gnu.org/licenses/>.
 */

import 'dart:math' as math;

import 'package:PiliPlus/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart'
    show touchSlopH;
import 'package:PiliPlus/common/widgets/gesture/image_horizontal_drag_gesture_recognizer.dart';
import 'package:PiliPlus/utils/extension/num_ext.dart';
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/physics.dart' show FrictionSimulation;
import 'package:flutter/scheduler.dart' show SchedulerBinding;
import 'package:flutter/services.dart' show HardwareKeyboard;

///
/// created by dom on 2026/02/14
///

class Viewer extends StatefulWidget {
  const Viewer({
    super.key,
    required this.minScale,
    required this.maxScale,
    this.isLongPic = false,
    required this.containerSize,
    required this.childSize,
    required this.onDragStart,
    required this.onDragUpdate,
    required this.onDragEnd,
    required this.doubleTapGestureRecognizer,
    required this.horizontalDragGestureRecognizer,
    required this.onChangePage,
    required this.child,
  });

  final double minScale;
  final double maxScale;
  final bool isLongPic;
  final Size containerSize;
  final Size childSize;
  final Widget child;

  final ValueChanged<ScaleStartDetails>? onDragStart;
  final ValueChanged<ScaleUpdateDetails>? onDragUpdate;
  final ValueChanged<ScaleEndDetails>? onDragEnd;
  final ValueChanged<int>? onChangePage;

  final DoubleTapGestureRecognizer doubleTapGestureRecognizer;
  final ImageHorizontalDragGestureRecognizer horizontalDragGestureRecognizer;

  @override
  State<StatefulWidget> createState() => _ViewerState();
}

class _ViewerState extends State<Viewer> with SingleTickerProviderStateMixin {
  static const double _scaleFactor = kDefaultMouseScrollToScaleFactor;

  _GestureType? _gestureType;

  final Matrix4 _matrix = Matrix4.identity();

  late double __scale;
  double get _scale => __scale;
  set _scale(double value) {
    __scale = value;
    _matrix[0] = _matrix[5] = _matrix[10] = value;
  }

  late Offset __position;
  Offset get _position => __position;
  set _position(Offset value) {
    __position = value;
    _matrix
      ..[12] = value.dx
      ..[13] = value.dy;
  }

  Offset? _scalePos;
  double? _scaleStart;
  Offset? _referenceFocalPoint;

  late Size _imageSize;

  late final DoubleTapGestureRecognizer _doubleTapGestureRecognizer;
  late final ImageHorizontalDragGestureRecognizer
  _horizontalDragGestureRecognizer;
  late final ScaleGestureRecognizer _scaleGestureRecognizer;

  Offset? _downPos;
  late final AnimationController _animationController;

  late double _scaleFrom, _scaleTo;
  late Offset _positionFrom, _positionTo;

  void _listener() {
    final t = Curves.easeOut.transform(_animationController.value);
    _scale = t.lerp(_scaleFrom, _scaleTo);
    _position = Offset.lerp(_positionFrom, _positionTo, t)!;
    setState(() {});
  }

  void _reset() {
    _scale = widget.minScale;
    _position = .zero;
  }

  void _initSize() {
    _reset();
    _imageSize = applyBoxFit(
      .scaleDown,
      widget.childSize,
      widget.containerSize,
    ).destination;
    if (widget.isLongPic) {
      final containerWidth = widget.containerSize.width;
      final containerHeight = widget.containerSize.height;
      final imageHeight = _imageSize.height * _scale;
      _position = Offset(
        (1 - _scale) * containerWidth / 2,
        (imageHeight - _scale * containerHeight) / 2,
      );
    }
  }

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

    _animationController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 300),
    )..addListener(_listener);

    _doubleTapGestureRecognizer = widget.doubleTapGestureRecognizer;
    _horizontalDragGestureRecognizer = widget.horizontalDragGestureRecognizer;

    _scaleGestureRecognizer = ScaleGestureRecognizer(debugOwner: this)
      ..dragStartBehavior = .start
      ..onStart = _onScaleStart
      ..onUpdate = _onScaleUpdate
      ..onEnd = _onScaleEnd
      ..gestureSettings = DeviceGestureSettings(touchSlop: touchSlopH);
  }

  @override
  void didUpdateWidget(Viewer oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.containerSize != widget.containerSize ||
        oldWidget.childSize != widget.childSize) {
      _initSize();
    }
  }

  @override
  void dispose() {
    _stopFling();
    _animationController
      ..removeListener(_listener)
      ..dispose();
    _scaleGestureRecognizer.dispose();
    super.dispose();
  }

  Offset _toScene(Offset localFocalPoint) {
    return (localFocalPoint - _position) / _scale;
  }

  Offset _clampPosition(Offset offset, double scale) {
    final containerSize = widget.containerSize;
    final imageWidth = _imageSize.width * scale;
    final imageHeight = _imageSize.height * scale;

    final center = containerSize * (1 - scale) / 2;

    final dxOffset = (imageWidth - containerSize.width) / 2;
    final dyOffset = (imageHeight - containerSize.height) / 2;

    return Offset(
      imageWidth > containerSize.width
          ? clampDouble(
              offset.dx,
              center.width - dxOffset,
              center.width + dxOffset,
            )
          : center.width,
      imageHeight > containerSize.height
          ? clampDouble(
              offset.dy,
              center.height - dyOffset,
              center.height + dyOffset,
            )
          : center.height,
    );
  }

  Offset _matrixTranslate(Offset translation) {
    if (translation == .zero) {
      return _position;
    }
    return _clampPosition(_position + translation * _scale, _scale);
  }

  void _onDoubleTapDown(TapDownDetails details) {
    _downPos = details.localPosition;
  }

  void _onDoubleTap() {
    if (!mounted) return;
    if (_animationController.isAnimating) return;
    EasyThrottle.throttle(
      'VIEWER_TAP',
      const Duration(milliseconds: 555),
      _handleDoubleTap,
    );
  }

  void _handleDoubleTap() {
    if (!mounted) return;
    if (_animationController.isAnimating) return;
    _stopFling();
    _scaleFrom = _scale;
    _positionFrom = _position;

    double endScale;
    if (_scale == widget.minScale) {
      endScale = widget.maxScale * 0.6;
      if (endScale <= widget.minScale) {
        endScale = widget.maxScale;
      }
    } else {
      endScale = widget.minScale;
    }
    final position = _clampPosition(
      Offset.lerp(_downPos!, _position, endScale / _scale)!,
      endScale,
    );

    _scaleTo = endScale;
    _positionTo = position;

    _animationController
      ..duration = const Duration(milliseconds: 300)
      ..forward(from: 0);
  }

  static bool _calc(Offset initialPosition, Offset lastPosition) {
    final offset = lastPosition - initialPosition;
    return offset.dy.abs() > offset.dx.abs();
  }

  void _onScaleStart(ScaleStartDetails details) {
    _stopFling();

    if (_animationController.isAnimating) {
      _animationController.stop();
    }

    if (details.pointerCount == 1) {
      if (widget.isLongPic) {
        final imageHeight = _scale * _imageSize.height;
        final containerHeight = widget.containerSize.height;
        if (_scalePos != null && _calc(_scalePos!, details.focalPoint)) {
          final bool drag;
          if (details.focalPoint.dy > _scalePos!.dy) {
            drag = _position.dy.equals(
              (imageHeight - _scale * containerHeight) / 2,
              1e-6,
            );
          } else {
            drag = _position.dy.equals(containerHeight - imageHeight, 1e-6);
          }
          if (drag) {
            _gestureType = .drag;
            widget.onDragStart?.call(details);
            return;
          }
        }
      } else if (_scale == widget.minScale) {
        _gestureType = .drag;
        widget.onDragStart?.call(details);
        return;
      }
    }

    _scaleStart = _scale;
    _referenceFocalPoint = _toScene(details.localFocalPoint);
  }

  void _onScaleUpdate(ScaleUpdateDetails details) {
    if (_gestureType == .drag) {
      widget.onDragUpdate?.call(details);
      return;
    }

    if (details.scale != 1.0) {
      _gestureType = .scale;
      _scale = clampDouble(
        _scaleStart! * details.scale,
        widget.minScale,
        widget.maxScale,
      );

      final Offset focalPointSceneScaled = _toScene(details.localFocalPoint);
      _position = _matrixTranslate(
        focalPointSceneScaled - _referenceFocalPoint!,
      );
      setState(() {});
    } else {
      _gestureType = .pan;
      final Offset focalPointScene = _toScene(details.localFocalPoint);
      final Offset translationChange = focalPointScene - _referenceFocalPoint!;
      _position = _matrixTranslate(translationChange);
      _referenceFocalPoint = _toScene(details.localFocalPoint);
      setState(() {});
    }
  }

  /// ref https://github.com/ahnaineh/custom_interactive_viewer
  int? _flingFrameCallbackId;
  Simulation? _flingSimulation;
  Duration? _flingStartTime;
  double _lastFlingElapsedSeconds = 0.0;
  Offset _flingDirection = Offset.zero;

  /// Calculate appropriate friction based on velocity magnitude
  double _calculateDynamicFriction(double velocityMagnitude) {
    // Use higher friction for faster flicks
    // These values can be tuned for the feel you want
    if (velocityMagnitude > 5000) {
      return 0.03; // Higher friction for very fast flicks
    } else if (velocityMagnitude > 3000) {
      return 0.02; // Medium friction for moderate flicks
    } else {
      return 0.01; // Lower friction for gentle movements
    }
  }

  void _startFling(Velocity velocity) {
    _stopFling();

    final double velocityMagnitude = velocity.pixelsPerSecond.distance;
    final double frictionCoefficient = _calculateDynamicFriction(
      velocityMagnitude,
    );

    _flingSimulation = FrictionSimulation(
      frictionCoefficient,
      0.0,
      velocityMagnitude,
    );

    _flingDirection = velocityMagnitude > 0
        ? velocity.pixelsPerSecond / velocityMagnitude
        : Offset.zero;

    _flingStartTime = null;
    _lastFlingElapsedSeconds = 0.0;
    _scheduleFlingFrame();
  }

  void _scheduleFlingFrame() {
    _flingFrameCallbackId = SchedulerBinding.instance.scheduleFrameCallback(
      _handleFlingFrame,
    );
  }

  void _handleFlingFrame(Duration timeStamp) {
    if (_flingSimulation == null) return;

    _flingStartTime ??= timeStamp;
    final double elapsedSeconds =
        (timeStamp - _flingStartTime!).inMicroseconds / 1e6;

    final double distance = _flingSimulation!.x(elapsedSeconds);
    final double prevDistance = _flingSimulation!.x(_lastFlingElapsedSeconds);
    final double delta = distance - prevDistance;
    _lastFlingElapsedSeconds = elapsedSeconds;

    if ((prevDistance != 0.0 && delta.abs() < 0.1) ||
        _flingSimulation!.isDone(elapsedSeconds)) {
      _stopFling();
      return;
    }

    final Offset movement = _flingDirection * delta;
    _position = _clampPosition(_position + movement, _scale);
    setState(() {});

    if (_flingSimulation!.isDone(elapsedSeconds)) {
      _stopFling();
    } else {
      _scheduleFlingFrame();
    }
  }

  void _stopFling() {
    if (_flingFrameCallbackId != null) {
      SchedulerBinding.instance.cancelFrameCallbackWithId(
        _flingFrameCallbackId!,
      );
      _flingFrameCallbackId = null;
    }
    _flingStartTime = null;
    _lastFlingElapsedSeconds = 0.0;
    _flingSimulation = null;
  }

  /// ref [InteractiveViewer]
  void _onScaleEnd(ScaleEndDetails details) {
    switch (_gestureType) {
      case _GestureType.pan:
        final double velocityMagnitude =
            details.velocity.pixelsPerSecond.distance;
        if (velocityMagnitude >= 200.0) {
          _startFling(details.velocity);
        }

      case _GestureType.scale:
        // if (details.scaleVelocity.abs() < 0.1) {
        //   return;
        // }
        // final double scale = _scale;
        // final FrictionSimulation frictionSimulation = FrictionSimulation(
        //   _interactionEndFrictionCoefficient * _scaleFactor,
        //   scale,
        //   details.scaleVelocity / 10,
        // );
        // final double tFinal = _getFinalTime(
        //   details.scaleVelocity.abs(),
        //   _interactionEndFrictionCoefficient,
        //   effectivelyMotionless: 0.1,
        // );
        // _scaleAnimation = _scaleController.drive(
        //   Tween<double>(
        //     begin: scale,
        //     end: frictionSimulation.x(tFinal),
        //   ).chain(CurveTween(curve: Curves.decelerate)),
        // )..addListener(_handleScaleAnimation);
        // _animationController
        //   ..duration = Duration(milliseconds: (tFinal * 1000).round())
        //   ..forward(from: 0);
        break;
      case _GestureType.drag:
        widget.onDragEnd?.call(details);
      case null:
    }
    _scalePos = null;
    _gestureType = null;
  }

  @override
  Widget build(BuildContext context) {
    return Listener(
      behavior: .opaque,
      onPointerDown: _onPointerDown,
      onPointerPanZoomStart: _onPointerPanZoomStart,
      onPointerSignal: _onPointerSignal,
      child: ClipRect(
        child: Transform(
          transform: _matrix,
          child: widget.child,
        ),
      ),
    );
  }

  void _onPointerDown(PointerDownEvent event) {
    _scalePos = event.position;
    _doubleTapGestureRecognizer
      ..onDoubleTapDown = _onDoubleTapDown
      ..onDoubleTap = _onDoubleTap;
    _horizontalDragGestureRecognizer.addPointer(
      event,
      isPointerAllowed: _isAtEdge(event.localPosition),
    );
    _scaleGestureRecognizer.addPointer(event);
  }

  void _onPointerPanZoomStart(PointerPanZoomStartEvent event) {
    _scaleGestureRecognizer.addPointerPanZoom(event);
  }

  bool _isAtEdge(Offset position) {
    if (_scale <= widget.minScale) {
      _horizontalDragGestureRecognizer.setAtBothEdges();
      return true;
    }
    final containerWidth = widget.containerSize.width;
    final imageWidth = _imageSize.width * _scale;
    if (imageWidth <= containerWidth) {
      _horizontalDragGestureRecognizer.setAtBothEdges();
      return true;
    }
    final dx = (1 - _scale) * containerWidth / 2;
    final dxOffset = (imageWidth - containerWidth) / 2;
    if (_position.dx.equals(dx + dxOffset, 1e-6)) {
      _horizontalDragGestureRecognizer.isAtLeftEdge = true;
      return true;
    }
    if (_position.dx.equals(dx - dxOffset, 1e-6)) {
      _horizontalDragGestureRecognizer.isAtRightEdge = true;
      return true;
    }
    return false;
  }

  void _onPointerSignal(PointerSignalEvent event) {
    if (event is PointerScrollEvent) {
      if (widget.onChangePage != null &&
          !HardwareKeyboard.instance.isControlPressed) {
        widget.onChangePage!.call(event.scrollDelta.dy < 0 ? -1 : 1);
        return;
      }
      _stopFling();
      final double scaleChange = math.exp(-event.scrollDelta.dy / _scaleFactor);
      final Offset local = event.localPosition;
      final Offset focalPointScene = _toScene(local);
      _scale = clampDouble(
        _scale * scaleChange,
        widget.minScale,
        widget.maxScale,
      );
      final Offset focalPointSceneScaled = _toScene(local);
      _position = _matrixTranslate(focalPointSceneScaled - focalPointScene);
      setState(() {});
    }
  }
}

enum _GestureType { pan, scale, drag }
