import 'dart:math';
import 'dart:typed_data';
import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart';
import 'package:tflite_flutter/tflite_flutter.dart' as tflite;
import 'package:image/image.dart' as imglib;

class FaceNetService {
  // singleton boilerplate
  static final FaceNetService _faceNetService = FaceNetService._internal();

  factory FaceNetService() {
    return _faceNetService;
  }
  // singleton boilerplate
  FaceNetService._internal();


  late tflite.Interpreter _interpreter;

  double threshold = 0.8;

  List? _predictedData;
  List? get predictedData => this._predictedData;

  //  saved users data
  dynamic data = {};

  Future loadModel() async {
    try {
      // this._interpreter = await tflite.Interpreter.fromAsset('mobilefacenet.tflite');
      this._interpreter = await tflite.Interpreter.fromAsset('assets/mobilefacenet.tflite');
      print('model loaded successfully');
    } catch (e) {
      print('Failed to load model.');
      print(e);
    }
  }

  setCurrentPrediction(imglib.Image cameraImage, Face face) {
    /// crops the face from the image and transforms it to an array of data
    List input = _preProcess(cameraImage, face);

    /// then reshapes input and ouput to model format 🧑‍🔧
    input = input.reshape([1, 112, 112, 3]);
    List output = List.filled(1 * 192, null, growable: false).reshape([1, 192]);

    /// runs and transforms the data 🤖
    this._interpreter.run(input, output);
    output = output.reshape([192]);

    this._predictedData = List.from(output);
    print("PREDICTEDDATA :${_predictedData}");
  }

  /// takes the predicted data previously saved and do inference
  bool? predict(List dataDB) {
    /// search closer user prediction if exists
    return _searchResult(this._predictedData, dataDB);
  }

  /// _preProess: crops the image to be more easy
  /// to detect and transforms it to model input.
  /// [cameraImage]: current image
  /// [face]: face detected
  List _preProcess(imglib.Image image, Face faceDetected) {
  // crops the face 💇
  imglib.Image croppedImage = _cropFace(image, faceDetected);
  imglib.Image img = imglib.copyResizeCropSquare(croppedImage, size: 112);

  // transforms the cropped face to array data
  Float32List imageAsList = imageToByteListFloat32(img);
  return imageAsList;
}

  /// crops the face from the image 💇
  /// [cameraImage]: current image
  /// [face]: face detected
  imglib.Image _cropFace(imglib.Image image, Face faceDetected) {
  double x = faceDetected.boundingBox.left - 10.0;
  double y = faceDetected.boundingBox.top - 10.0;
  double w = faceDetected.boundingBox.width + 10.0;
  double h = faceDetected.boundingBox.height + 10.0;
  
  return imglib.copyCrop(
    image,
    x: x.round(),
    y: y.round(),
    width: w.round(),
    height: h.round(),
  );
}


  Float32List imageToByteListFloat32(imglib.Image image) {
  /// input size = 112
  var convertedBytes = Float32List(1 * 112 * 112 * 3);
  var buffer = Float32List.view(convertedBytes.buffer);
  int pixelIndex = 0;

  for (var i = 0; i < 112; i++) {
    for (var j = 0; j < 112; j++) {
      var pixel = image.getPixel(j, i);

      /// mean: 128
      /// std: 128
      buffer[pixelIndex++] = (pixel.r - 128) / 128;  // Red channel
      buffer[pixelIndex++] = (pixel.g - 128) / 128;  // Green channel
      buffer[pixelIndex++] = (pixel.b - 128) / 128;  // Blue channel
    }
  }
  return convertedBytes.buffer.asFloat32List();
}

  /// searchs the result in the DDBB (this function should be performed by Backend)
  /// [predictedData]: Array that represents the face by the MobileFaceNet model
  bool? _searchResult(List? predictedData, List dataDB) {

    /// if no faces saved
    if (dataDB.length == 0) return null;
    double minDist = 999;
    double currDist = 0.0;
    bool predRes;

    /// search the closest result 👓
      currDist = _euclideanDistance(dataDB, predictedData);
      print("CURRDIST : ${currDist}");
      if (currDist <= threshold && currDist < minDist) {
        minDist = currDist;
        predRes = true;
      }
      else predRes = false;
    print('PREDRES $predRes');
    return predRes;
  }

  /// Adds the power of the difference between each point
  /// then computes the sqrt of the result 📐
  double _euclideanDistance(List e1, List? e2) {

    double sum = 0.0;
    print('E1 ${e1}');
    for (int i = 0; i < e1.length; i++) {
      sum += pow((e1[i] - e2![i]), 2);
    }
    return sqrt(sum);
  }

  void setPredictedData(value) {
    this._predictedData = value;
  }
}
