import 'dart:math';
import 'dart:math' as math;
import 'dart:ui' as ui;

import 'package:fis_vid/processors/processor.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:quiver/core.dart';
import 'package:vid/us/vid_us_2d_visual.dart';
import 'package:vid/us/vid_us_image.dart';
import 'package:vid/us/vid_us_image_data.dart';
import 'package:vid/us/vid_us_physical_coordinate.dart';
import 'package:vid/us/vid_us_visual_area_type.dart';
import 'package:vid/us/vid_us_visual_type.dart';

class Tuple<T1, T2> {
  final T1 item1;
  final T2 item2;

  const Tuple(this.item1, this.item2);

  factory Tuple.fromList(List items) {
    if (items.length != 2) {
      throw ArgumentError(
          "Tuple.fromList: argument `items` must have length 2.");
    }
    return Tuple(items[0] as T1, items[1] as T2);
  }

  List toList() => [item1, item2];

  @override
  String toString() => '[$item1, $item2]';

  @override
  bool operator ==(Object other) =>
      other is Tuple && other.item1 == item1 && other.item2 == item2;

  @override
  int get hashCode => hash2(item1, item2);
}

class MatrixUtil {
  static List<DMatrix> multiplyMatrix(DMatrix matrix1, DMatrix matrix2) {
    // ignore: prefer_function_declarations_over_variables
    final reutrnFn = () => [matrix1, matrix2];
    MatrixTypes types = matrix1._type;
    MatrixTypes types2 = matrix2._type;
    if (types2 != MatrixTypes.identify) {
      if (types == MatrixTypes.identify) {
        matrix1 = matrix2;
      } else if (types2 == MatrixTypes.translation) {
        matrix1._offsetX += matrix2._offsetX;
        matrix1._offsetY += matrix2._offsetY;
        if (types != MatrixTypes.unknown) {
          var typeIdx = matrix1._type.index;
          typeIdx |= 1;
          matrix1._type = MatrixTypes.values[typeIdx];
          //*((int*)&matrix1._type) |= 1;
        }
      } else if (types == MatrixTypes.translation) {
        double num2 = matrix1._offsetX;
        double num3 = matrix1._offsetY;
        matrix1 = matrix2;
        matrix1._offsetX =
            ((num2 * matrix2._m11) + (num3 * matrix2._m21)) + matrix2._offsetX;
        matrix1._offsetY =
            ((num2 * matrix2._m12) + (num3 * matrix2._m22)) + matrix2._offsetY;
        if (types2 == MatrixTypes.unknown) {
          matrix1._type = MatrixTypes.unknown;
        } else {
          matrix1._type = MatrixTypes.values[
              MatrixTypes.scaling.index | MatrixTypes.translation.index];
        }
      } else {
        switch ((((types.index) << 4) | types2.index)) {
          case 0x22:
            matrix1._m11 *= matrix2._m11;
            matrix1._m22 *= matrix2._m22;
            return reutrnFn();

          case 0x23:
            matrix1._m11 *= matrix2._m11;
            matrix1._m22 *= matrix2._m22;
            matrix1._offsetX = matrix2._offsetX;
            matrix1._offsetY = matrix2._offsetY;
            matrix1._type = MatrixTypes.values[
                MatrixTypes.scaling.index | MatrixTypes.translation.index];
            return reutrnFn();

          case 0x24:
          case 0x34:
          case 0x42:
          case 0x43:
          case 0x44:
            matrix1 = DMatrix.c(
                (matrix1._m11 * matrix2._m11) + (matrix1._m12 * matrix2._m21),
                (matrix1._m11 * matrix2._m12) + (matrix1._m12 * matrix2._m22),
                (matrix1._m21 * matrix2._m11) + (matrix1._m22 * matrix2._m21),
                (matrix1._m21 * matrix2._m12) + (matrix1._m22 * matrix2._m22),
                ((matrix1._offsetX * matrix2._m11) +
                        (matrix1._offsetY * matrix2._m21)) +
                    matrix2._offsetX,
                ((matrix1._offsetX * matrix2._m12) +
                        (matrix1._offsetY * matrix2._m22)) +
                    matrix2._offsetY);
            return reutrnFn();

          case 50:
            matrix1._m11 *= matrix2._m11;
            matrix1._m22 *= matrix2._m22;
            matrix1._offsetX *= matrix2._m11;
            matrix1._offsetY *= matrix2._m22;
            return reutrnFn();

          case 0x33:
            matrix1._m11 *= matrix2._m11;
            matrix1._m22 *= matrix2._m22;
            matrix1._offsetX =
                (matrix2._m11 * matrix1._offsetX) + matrix2._offsetX;
            matrix1._offsetY =
                (matrix2._m22 * matrix1._offsetY) + matrix2._offsetY;
            return reutrnFn();
        }
      }
    }
    return reutrnFn();
  }
}

enum MatrixTypes {
  identify,
  translation,
  scaling,

  /// 占位，勿用
  uselessAndPlaceHolder,
  unknown,
}

class DMatrix {
  double _m11 = 0.0;
  double _m12 = 0.0;
  double _m21 = 0.0;
  double _m22 = 0.0;
  double _offsetX = 0.0;
  double _offsetY = 0.0;
  MatrixTypes _type = MatrixTypes.identify;
  int _padding = 0;

  DMatrix();

  factory DMatrix.c(
    double m11,
    double m12,
    double m21,
    double m22,
    double offsetX,
    double offsetY,
  ) {
    final m = DMatrix();
    m._m11 = m11;
    m._m12 = m12;
    m._m21 = m21;
    m._m22 = m22;
    m._offsetX = offsetX;
    m._offsetY = offsetY;
    m._type = MatrixTypes.unknown;
    m._padding = 0;
    _deriveMatrixType(m);
    return m;
  }

  double get determinant {
    if (_type == MatrixTypes.identify || _type == MatrixTypes.translation) {
      return 1.0;
    }
    if (_type == MatrixTypes.scaling) return _m11 * _m22;
    final calcType = MatrixTypes
        .values[MatrixTypes.scaling.index | MatrixTypes.translation.index];
    if (_type == calcType) return _m11 * _m22;
    return (_m11 * _m22) - (_m12 * _m21);
  }

  void translate(double offsetX, double offsetY) {
    if (_type == MatrixTypes.identify) {
      setMatrix(1.0, 0.0, 0.0, 1.0, offsetX, offsetY, MatrixTypes.translation);
    } else if (_type == MatrixTypes.unknown) {
      _offsetX += offsetX;
      _offsetY += offsetY;
    } else {
      _offsetX += offsetX;
      _offsetY += offsetY;
      var typeIdx = _type.index;
      typeIdx |= MatrixTypes.translation.index;
      _type = MatrixTypes.values[typeIdx];
    }
  }

  void setMatrix(
    double m11,
    double m12,
    double m21,
    double m22,
    double offsetX,
    double offsetY,
    MatrixTypes type,
  ) {
    _m11 = m11;
    _m12 = m12;
    _m21 = m21;
    _m22 = m22;
    _offsetX = offsetX;
    _offsetY = offsetY;
    _type = type;
  }

  void skew(double skewX, double skewY) {
    skewX = skewX % 360.0;
    skewY = skewY % 360.0;
    final newInstance = this *
        createSkewRadians(
            skewX * 0.017453292519943295, skewY * 0.017453292519943295);
    cover(newInstance);
  }

  Tuple<double, double> multiplyPoint(double x, double y) {
    double nx = x, ny = y;

    // ignore: prefer_function_declarations_over_variables
    final rstFn = () => Tuple(nx, ny);
    if (_type.index ==
        MatrixTypes.scaling.index | MatrixTypes.translation.index) {
      nx *= _m11;
      nx += _offsetX;
      ny *= _m22;
      ny += _offsetY;
      return rstFn();
    }
    switch (_type) {
      case MatrixTypes.identify:
        return rstFn();
      case MatrixTypes.translation:
        x += _offsetX;
        y += _offsetY;
        return rstFn();
      case MatrixTypes.scaling:
        x *= _m11;
        y *= _m22;
        return rstFn();
      case MatrixTypes.uselessAndPlaceHolder:
      case MatrixTypes.unknown:
        break;
    }
    double num = (ny * _m21) + _offsetX;
    double num2 = (nx * _m12) + _offsetY;
    nx *= _m11;
    nx += num;
    ny *= _m22;
    ny += num2;
    return rstFn();
  }

  DPoint transform(DPoint point) {
    final t = multiplyPoint(point.x, point.y);
    return DPoint(t.item1, t.item2);
  }

  void cover(DMatrix other) {
    setMatrix(
      other._m11,
      other._m12,
      other._m21,
      other._m22,
      other._offsetX,
      other._offsetY,
      other._type,
    );
    _padding = other._padding;
  }

  DMatrix copy() {
    final cp = DMatrix();
    cp.cover(this);
    return cp;
  }

  void invert() {
    if (determinant.abs() < 2.2204460492503131E-15) {
      throw Exception("Transform_NotInvertible");
    }
    final mixType = MatrixTypes
        .values[MatrixTypes.scaling.index | MatrixTypes.translation.index];
    if (_type == mixType) {
      _m11 = 1.0 / _m11;
      _m22 = 1.0 / _m22;
      _offsetX = -_offsetX * _m11;
      _offsetY = -_offsetY * _m22;
      return;
    }
    switch (_type) {
      case MatrixTypes.identify:
        break;

      case MatrixTypes.translation:
        _offsetX = -_offsetX;
        _offsetY = -_offsetY;
        return;

      case MatrixTypes.scaling:
        _m11 = 1.0 / _m11;
        _m22 = 1.0 / _m22;
        return;
      default:
        {
          double num2 = 1.0 / determinant;
          setMatrix(
              _m22 * num2,
              -_m12 * num2,
              -_m21 * num2,
              _m11 * num2,
              ((_m21 * _offsetY) - (_offsetX * _m22)) * num2,
              ((_offsetX * _m12) - (_m11 * _offsetY)) * num2,
              MatrixTypes.unknown);
          break;
        }
    }
  }

  DMatrix operator *(DMatrix other) {
    final result = MatrixUtil.multiplyMatrix(this, other);
    return result.first;
  }

  static DMatrix createSkewRadians(double skewX, double skewY) {
    DMatrix matrix = DMatrix();
    matrix.setMatrix(1.0, math.tan(skewY), math.tan(skewX), 1.0, 0.0, 0.0,
        MatrixTypes.unknown);
    return matrix;
  }

  static void _deriveMatrixType(DMatrix matrix) {
    matrix._type = MatrixTypes.identify;
    if ((matrix._m21 != 0.0) || (matrix._m12 != 0.0)) {
      matrix._type = MatrixTypes.unknown;
    } else {
      if ((matrix._m11 != 1.0) || (matrix._m22 != 1.0)) {
        matrix._type = MatrixTypes.scaling;
      }

      if ((matrix._offsetX != 0.0) || (matrix._offsetY != 0.0)) {
        var typeIdx = matrix._type.index;
        typeIdx |= MatrixTypes.translation.index;
        matrix._type = MatrixTypes.values[typeIdx];
      }
      if ((matrix._type.index &
              (MatrixTypes.scaling.index | MatrixTypes.translation.index)) ==
          MatrixTypes.identify.index) {
        matrix._type = MatrixTypes.identify;
      }
    }
  }
}

class DSkewTransform {
  // ignore: prefer_final_fields
  DMatrix _matrix = DMatrix();

  DSkewTransform();

  factory DSkewTransform.withAngle(
    double angleX,
    double angleY,
    double centerX,
    double centerY,
  ) {
    final st = DSkewTransform();
    bool flag = (centerX != 0.0) || !(centerY == 0.0);
    if (flag) {
      st._matrix.translate(-centerX, -centerY);
    }
    st._matrix.skew(angleX, angleY);
    if (flag) {
      st._matrix.translate(centerX, centerY);
    }
    return st;
  }

  factory DSkewTransform.withMatrix(DMatrix matrix) {
    final st = DSkewTransform();
    st._matrix = matrix;
    return st;
  }

  DSkewTransform get inverse {
    final inversedMatrix = _matrix.copy()..invert();
    final st = DSkewTransform.withMatrix(inversedMatrix);
    return st;
  }

  DPoint transform(DPoint point) {
    return _matrix.transform(point);
  }
}

class DPoint extends Point<double> {
  const DPoint(double x, double y) : super(x, y);

  factory DPoint.fromlPoint(Point<double> point) {
    return DPoint(point.x, point.y);
  }

  factory DPoint.fromOffset(Offset offset) {
    return DPoint(offset.dx, offset.dy);
  }

  static const zero = DPoint(0, 0);

  bool operator <(DPoint other) => magnitude < other.magnitude;
  bool operator <=(DPoint other) => magnitude <= other.magnitude;
  bool operator >(DPoint other) => magnitude > other.magnitude;
  bool operator >=(DPoint other) => magnitude >= other.magnitude;
}

class VidController extends GetxController {
  // ignore: constant_identifier_names
  static const InvalidPoint = DPoint(0, 0);

  final _currentIndex = 0.obs;
  final Rx<VidUsImageData?> _vidData = Rx(null);

  VidUsImageData get vidData => _vidData.value!;

  int get currentIndex => _currentIndex.value;

  VidUsImage get currentFrame => vidData.getImage(currentIndex);

  bool get loaded => _vidData.value != null;

  DSkewTransform? _logicalToPhysicalConvert;

  final _points = <DPoint>[].obs;

  List<DPoint> get points => _points.toList();

  double width = 0.0;
  double height = 0.0;

  final _cursorPoint = Offset.zero.obs;

  Offset get cursorPoint => _cursorPoint.value;

  final processor = BrightnessProcessor();

  final _brightness = 0.obs;

  int get brightness => _brightness.value;

  ui.Image? image;

  Future<void> codecImage([VidUsImage? frame]) async {
    image = await decodeImageFromList((frame ?? currentFrame).imageData);
  }

  @override
  void onReady() {
    super.onReady();
    _loadData();
    processor.propertyChanged.add(onProcessorPropertyChanged);
  }

  @override
  void onClose() {
    processor.propertyChanged.remove(onProcessorPropertyChanged);
    super.onClose();
  }

  onProcessorPropertyChanged(Object sender, void e) async {
    _brightness.value = processor.brightness;
    codecImage();
    // if (processor.changed) {
    //   var image = await decodeImageFromList(currentFrame.imageData);
    //   var data =
    //       (await image.toByteData(format: ui.ImageByteFormat.rawStraightRgba))!
    //           .buffer
    //           .asUint8List();
    //   frameImage = processor.process(data);
    // } else {
    //   frameImage = currentFrame.imageData;
    // }
  }

  void _loadVisual(VidUsImage frame) {
    final visual = frame.visuals.first;
    if (visual.visualType == VidUsVisualType.V2D) {
      final v2dv = visual as VidUs2DVisual;
      final physicalCoordinate =
          v2dv.physicalCoordinates[VidUsVisualAreaType.Tissue]
              as VidUsLinearTissuePhysicalCoordinate;

      _logicalToPhysicalConvert = DSkewTransform.withAngle(
        physicalCoordinate.steer,
        0,
        0,
        (physicalCoordinate.depthEnd - physicalCoordinate.depthStart) / 2,
      ).inverse;
    }
    clacSize(frame);
  }

  Future<void> _loadData() async {
    final byteData = await rootBundle.load("assets/default.VID");
    final bytes = byteData.buffer.asUint8List();
    final data = VidUsImageData(bytes);
    final firstFrame = data.getImage(0);
    _loadVisual(firstFrame);
    await codecImage(firstFrame);
    // loaded!
    _vidData.value = data;
  }

  DPoint convertPoint(DPoint point) {
    if (_logicalToPhysicalConvert != null) {
      return _logicalToPhysicalConvert!.transform(point);
    }

    // TODO: log warn
    print('Log_warn - ${DateTime.now()}: Converter not ready.');
    return InvalidPoint;
  }

  void recordPoint(Point<double> point) {
    _points.add(DPoint.fromlPoint(point));
  }

  void clearPoints() => _points.value = [];

  void finishDraw() {
    if (points.length == 2) {
      final p1 = convertPoint(points[0]);
      final p2 = convertPoint(points[1]);
      final distance = (p2 - p1).magnitude;
      print("Cale distance: $distance");
    }
  }

  void clacSize(VidUsImage frame) {
    final size = Get.size;
    width = frame.width.toDouble();
    height = frame.height.toDouble();
    if (size.width < width || size.height < height) {
      final wR = size.width / width;
      final hR = size.height / height;
      if (wR > hR) {
        height = size.height;
        width = size.width * hR;
      } else {
        height = size.height * wR;
        width = size.width;
      }
    }
  }

  void onCursorOffsetUpdate(Offset offset) {
    _cursorPoint.value = offset;
  }
}

class _MyPainter extends CustomPainter {
  _MyPainter(this.image);
  final ui.Image image;

  // ignore: prefer_final_fields
  ui.Paint _paint = ui.Paint()..isAntiAlias = true;

  @override
  void paint(ui.Canvas canvas, ui.Size size) {
    // _paint.colorFilter = const ui.ColorFilter.matrix(<double>[
    //   1, 0, 0, 0, 220, //
    //   0, 1, 0, 0, 220, //
    //   0, 0, 1, 0, 220, //
    //   0, 0, 0, 1, 0, //
    // ]);
    _paint.colorFilter = const ui.ColorFilter.matrix(<double>[
      1, 0, 0, 0, 220, //
      0, 1, 0, 0, 220, //
      0, 0, 1, 0, 220, //
      0, 0, 0, 1, 0, //
    ]);
    final centerW = size.width / 2;
    final centerH = size.height / 2;
    canvas.translate(centerW, centerH);
    final offset = ui.Offset(-centerW, -centerH);
    canvas.drawImage(image, offset, _paint);
  }

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

class VidMeasurePlayer extends StatelessWidget {
  VidMeasurePlayer({Key? key}) : super(key: key);

  final c = Get.put(VidController());

  @override
  Widget build(BuildContext context) {
    final size = MediaQuery.of(context).size;
    final stack = Stack(
      children: [
        Obx(() {
          if (c.loaded && c.image != null) {
            if (c.brightness > 255) {
              print('object');
            }
            return RepaintBoundary(
              child: CustomPaint(
                // size: ui.Size(c.width / 2, c.height / 2),
                painter: _MyPainter(c.image!),
              ),
            );
            // return SizedBox(
            //   width: c.width,
            //   height: c.height,
            //   child: RepaintBoundary(
            //     child: CustomPaint(
            //       painter: _MyPainter(c.image!),
            //     ),
            //   ),
            // );
            // return Image.memory(c.frameImage!);
          }
          return Container();
        }),
        Obx(() {
          if (c.loaded) {
            return DrawRecord(
              width: c.width,
              height: c.height,
            );
          }
          return Container();
        }),
        Obx(() {
          if (c.loaded) {
            return Positioned(
              child: const Icon(
                Icons.add,
                color: Colors.amber,
                size: 28,
              ),
              top: c.cursorPoint.dy - 14,
              left: c.cursorPoint.dx - 14,
            );
          }
          return Container();
        }),
        Obx(() {
          if (c.loaded) {
            return _VidMeasureDrawBoard(
              width: c.width,
              height: c.height,
            );
          }
          return Container();
        }),
        Positioned(
          top: 20,
          right: 40,
          child: Container(
            color: Colors.white,
            height: 50,
            width: 100,
            child: Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                IconButton(
                  onPressed: () {
                    c.processor.brightness -= 50;
                  },
                  icon: const Icon(Icons.remove),
                ),
                IconButton(
                  onPressed: () {
                    c.processor.brightness += 50;
                  },
                  icon: const Icon(Icons.add),
                ),
              ],
            ),
          ),
        )
      ],
    );
    return Material(
      color: Colors.green,
      child: stack,
    );
  }
}

class _DrawBoardController extends GetxController {
  _DrawBoardController(this.size);
  final Size size;
  final _vidController = Get.find<VidController>();

  late final _regionMax = DPoint(size.width, size.height);

  DPoint? _lastPoint;
  DPoint? _lockPoint;

  final _active = false.obs;

  bool get active => _active.value;

  void onClick(DPoint point) {
    print(point);
    if (active) {
      _active.value = false;
      _vidController.clearPoints();
      _vidController.recordPoint(_lockPoint!);
      _vidController.recordPoint(point);
      _vidController.finishDraw();
      print('logic d: ${(_lockPoint! - point).magnitude}');
    } else {
      _active.value = true;
      _lockPoint = point;
    }
  }

  void onPointUpdate(DPoint point) {
    if (point < DPoint.zero) {
      if (_lastPoint == DPoint.zero) {
        return;
      }
      point = DPoint.zero;
    } else if (point > _regionMax) {
      if (_lastPoint == _regionMax) {
        return;
      }
      point = _regionMax;
    }
    _lastPoint = point;
    _vidController.onCursorOffsetUpdate(Offset(point.x, point.y));
    if (active) {
      print('update:$point');
    }
  }
}

class _VidMeasureDrawBoard extends StatelessWidget {
  _VidMeasureDrawBoard({required this.width, required this.height});

  final double width;
  final double height;

  late final c = Get.put(_DrawBoardController(Size(width, height)));

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTapDown: (details) {
        c.onClick(DPoint.fromOffset(details.localPosition));
      },
      onPanUpdate: (details) {
        // print('onPanUpdate:${details.localPosition}');
      },
      child: MouseRegion(
        cursor: SystemMouseCursors.none,
        onHover: (event) {
          c.onPointUpdate(DPoint.fromOffset(event.localPosition));
        },
        child: SizedBox(width: width, height: height),
      ),
    );
  }
}

class VidPage extends StatelessWidget {
  const VidPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return VidMeasurePlayer();
  }
}

class DrawRecord extends StatelessWidget {
  const DrawRecord({
    Key? key,
    required this.width,
    required this.height,
  }) : super(key: key);

  final double width;
  final double height;

  @override
  Widget build(BuildContext context) {
    return RepaintBoundary(
      child: CustomPaint(
        painter: _DrawRecordPainter(),
      ),
    );
  }
}

class _DrawRecordPainter extends CustomPainter {
  final pen = Paint()
    ..color = Colors.red
    ..isAntiAlias = true;

  @override
  void paint(Canvas canvas, Size size) {
    print("draw at ${DateTime.now()}");
    canvas.drawLine(
      const Offset(100, 100),
      const Offset(200, 200),
      pen,
    );
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return false;
  }
}
