import 'dart:async';

import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_blue_plus_windows/flutter_blue_plus_windows.dart';
import 'package:uuid/uuid.dart';
import 'package:xiao_pet_tracker/bootstrap.dart';
import 'package:xiao_pet_tracker/objectbox.dart';
import 'package:xiao_pet_tracker/objectbox.g.dart';
import 'package:xiao_pet_tracker/xiao_connector/models/capture_box.dart';
import 'package:xiao_pet_tracker/xiao_connector/models/capture_point.dart';
import 'package:xiao_pet_tracker/xiao_connector/utils/utils.dart';

part 'xiao_connector_state.dart';

class XiaoConnectorCubit extends Cubit<XiaoConnectorState> {
  XiaoConnectorCubit() : super(const XiaoConnectorState());

  late BluetoothDevice device;
  List<BluetoothService> _services = [];
  late BluetoothCharacteristic _senseService;

  late StreamSubscription<List<int>> dataCapturingSubscription;

  final ObjectBox _objectBox = getIt<ObjectBox>();
  late final Box<CapturePoint> _capturePointsBox;
  late final Box<CaptureBox> _captureBoxes;

  int _rotX = 0;
  int _rotY = 0;
  int _rotZ = 0;
  int _accX = 0;
  int _accY = 0;
  int _accZ = 0;
  int _millisecondsSinceEpochSend = 0;
  String _captureType = '';
  String _uuid = '';

  bool isRecording = false;

  Future<void> init() async {
    _capturePointsBox = _objectBox.store.box<CapturePoint>();
    _captureBoxes = _objectBox.store.box<CaptureBox>();
  }

  Future<void> connect() async {
    emit(state.copyWith(status: XiaoConnectorStatus.loading));

    try {
      // scan for devices for 15 seconds
      await FlutterBluePlus.startScan(timeout: const Duration(seconds: 15));
    } catch (e) {
      emit(state.copyWith(status: XiaoConnectorStatus.failure));
      throw Exception('Start Scan Error: $e');
    }

    FlutterBluePlus.scanResults.listen(
      (results) async {
        for (final r in results) {
          if (kDebugMode) {
            print(r);
          }

          // set the xiao sense name
          if (r.device.advName == 'Trouble Example') {
            await FlutterBluePlus.stopScan();
            device = r.device;

            await device.connect();

            try {
              _services = await device.discoverServices();
            } catch (e) {
              emit(state.copyWith(status: XiaoConnectorStatus.failure));
              throw Exception('Discover Services Error: $e');
            }

            device.connectionState
                .listen((BluetoothConnectionState blueState) async {
              if (blueState == BluetoothConnectionState.disconnected) {
                debugPrint('DISCONNECTED!');
                emit(state.copyWith(status: XiaoConnectorStatus.initial));
              }
            });

            // device.cancelWhenDisconnected(deviceConnectionStream);
            emit(state.copyWith(status: XiaoConnectorStatus.connected));
          }
        }
      },
      onDone: () {
        debugPrint('DONE');
        emit(state.copyWith(status: XiaoConnectorStatus.initial));
      },
    );
  }

  Future<void> debug() async {
    print('DEBUG!');
    final accelService = _services
        .where(
          (s) => s.serviceUuid == Guid('19B10000-E8F2-537E-4F6C-D104768A1214'),
        )
        .first
        .characteristics
        .where(
          (c) =>
              c.characteristicUuid ==
              Guid('19B10000-E8F2-537E-4F6C-D104768A1216'),
        )
        .first;

    final data = await accelService.read();
    print(data);

    final gyroX = fromBytesToInt32(data[0], data[1], data[2], data[3]);
    final gyroY = fromBytesToInt32(data[4], data[5], data[6], data[7]);
    final gyroZ = fromBytesToInt32(data[8], data[9], data[10], data[11]);

    final accelX = fromBytesToInt32(data[12], data[13], data[14], data[15]);
    final accelY = fromBytesToInt32(data[16], data[17], data[18], data[19]);
    final accelZ = fromBytesToInt32(data[20], data[21], data[22], data[23]);

    final timeStamp = fromBytesToInt64(
      data[24],
      data[25],
      data[26],
      data[27],
      data[28],
      data[29],
      data[30],
      data[31],
    );

    print('GYRO x:$gyroX, y:$gyroY, z:$gyroZ');
    print('ACCEL x:$accelX, y:$accelY, z:$accelZ');
    print('TIMESTAMP: ${DateTime.fromMicrosecondsSinceEpoch(timeStamp)}');
  }

  Future<void> setCapturing({required bool on}) async {
    final senseService = _services
        .where(
          (s) => s.serviceUuid == Guid('19B10000-E8F2-537E-4F6C-D104768A1214'),
        )
        .first
        .characteristics
        .where(
          (c) =>
              c.characteristicUuid ==
              Guid('19B10000-E8F2-537E-4F6C-D104768A1215'),
        )
        .first;

    await senseService.write([if (on) 1 else 0]);
  }

  Uint8List int64BigEndianBytes(int value) =>
      Uint8List(8)..buffer.asByteData().setInt64(0, value);

  Future<void> setTime(int millisSinceEpoch) async {
    final senseService = _services
        .where(
          (s) => s.serviceUuid == Guid('19B10000-E8F2-537E-4F6C-D104768A1214'),
        )
        .first
        .characteristics
        .where(
          (c) =>
              c.characteristicUuid ==
              Guid('756E6978-5469-6D65-5374-616D70527374'),
        )
        .first;

    final List<int> bytes = int64BigEndianBytes(millisSinceEpoch);
    await senseService.write(bytes);
  }

  Future<void> startCapturing() async {
    _senseService = _services
        .where(
          (s) => s.serviceUuid == Guid('19B10000-E8F2-537E-4F6C-D104768A1214'),
        )
        .first
        .characteristics
        .where(
          (c) =>
              c.characteristicUuid ==
              Guid('19B10000-E8F2-537E-4F6C-D104768A1216'),
        )
        .first;

    dataCapturingSubscription = _senseService.onValueReceived.listen((data) {
      // rotation
      _rotX = fromBytesToInt32(data[0], data[1], data[2], data[3]);
      _rotY = fromBytesToInt32(data[4], data[5], data[6], data[7]);
      _rotZ = fromBytesToInt32(data[8], data[9], data[10], data[11]);

      _accX = fromBytesToInt32(data[12], data[13], data[14], data[15]);
      _accY = fromBytesToInt32(data[16], data[17], data[18], data[19]);
      _accZ = fromBytesToInt32(data[20], data[21], data[22], data[23]);

      final timeStamp = fromBytesToInt64(
        data[24],
        data[25],
        data[26],
        data[27],
        data[28],
        data[29],
        data[30],
        data[31],
      );
      _millisecondsSinceEpochSend = timeStamp;

      final capturePoint = CapturePoint(
        type: _captureType,
        uuid: _uuid,
        rotationX: _rotX,
        rotationY: _rotY,
        rotationZ: _rotZ,
        accelerationX: _accX,
        accelerationY: _accY,
        accelerationZ: _accZ,
        millisecondsSinceEpochReceived:
            DateTime.now().toUtc().millisecondsSinceEpoch,
        millisecondsSinceEpochSend: _millisecondsSinceEpochSend,
      );
      emit(state.copyWith(lastCapturedPoint: capturePoint));

      if (isRecording) {
        _writeToObjectBox(capturePoint: capturePoint);
      }
    });
    await setCapturing(on: true);

    // await setTime(DateTime.now().millisecondsSinceEpoch);

    device.cancelWhenDisconnected(dataCapturingSubscription);

    await _senseService.setNotifyValue(true);

    emit(state.copyWith(status: XiaoConnectorStatus.capturing));
  }

  Future<void> stopCapturing() async {
    await dataCapturingSubscription.cancel();

    await _senseService.setNotifyValue(false);

    await setCapturing(on: false);

    emit(state.copyWith(status: XiaoConnectorStatus.connected));
  }

  Future<void> disconnectDevice() async {
    await device.disconnect();
    _services = [];
    emit(state.copyWith(status: XiaoConnectorStatus.initial));
  }

  void toggleRecording({required String captureType}) {
    isRecording = !isRecording;
    if (isRecording) {
      _captureType = captureType;
      _uuid = const Uuid().v4();
      _captureBoxes.put(
        CaptureBox(
          type: _captureType,
          uuid: _uuid,
          startTime: DateTime.now(),
        ),
      );
    }
  }

  void _writeToObjectBox({
    required CapturePoint capturePoint,
  }) {
    _capturePointsBox.put(capturePoint);
  }

  List<CapturePoint> getCapturePointsOfUuid(String uuid) {
    final query =
        _capturePointsBox.query(CapturePoint_.uuid.equals(uuid)).build();

    final points = query.find();
    return points;
  }

  void deleteRecording(String uuid) {
    _capturePointsBox.query(CapturePoint_.uuid.equals(uuid)).build().remove();
    _captureBoxes.query(CaptureBox_.uuid.equals(uuid)).build().remove();
  }

  List<CaptureBox> getAllCaptureBoxes() {
    final query = (_captureBoxes.query()
          ..order(CaptureBox_.startTime, flags: Order.descending))
        .build();
    final boxes = query.find();
    return boxes;
  }

  List<CapturePoint> getAllCapturePoints() => _capturePointsBox.getAll();
}
