import 'package:common_pub/logger.dart';
import 'package:common_pub/service/controller.dart';
import 'package:common_pub/ui/map_view/map_view.dart';
import 'package:common_pub/ui/map_view/view_map_trace.dart';
import 'package:track_common/model.dart';
import 'package:track_common/model/event_state.dart';

class Flag {
  Flag(this.value);

  int value;

  Color get color => Color(value);

  @override
  bool operator ==(Object other) {
    if (other is Flag) {
      return value == other.value;
    }
    return false;
  }

  @override
  int get hashCode => value.hashCode;

  static final red = Flag(0xffff0000);
  static final yellow = Flag(0xffffcb00);
  static final blue = Flag(0xff00a0ff);

  static List<Flag> get values => [red, yellow, blue];
}

class PlayerInfo extends AppGameState {
  var id = 0;
  int heartRatePercent = 0;
}

class PlayerInfoExt {
  var name = '';
  var routeName = '';
  var cpListWant = <ControlPoint>[];
}

class EventInfo {
  var id = 0;
  var players = <PlayerInfo>[];
}

class EventInfoExt {
  var name = '';
  var cpAllCount = 0;
  String startAt = '';
  String endAt = '';
  EventState state = EventState.idle;
}

class PlayerOnMap {
  var info = PlayerInfo();
  var _ext = PlayerInfoExt();
  set ext(PlayerInfoExt v) {
    _ext = v;
    info.cpListWant = v.cpListWant;
  }

  int get id => info.id;
  String get name => _ext.name;
  List<ControlPoint> get cpList => info.cpListWant;
  final isHide = false.obs;
  var trace = <TracePoint>[].obs;
  var flag = Flag.red.obs;
  String get routeName => _ext.routeName;
  Distance get distance => info.distance;
  List<HeartRate> get hrInfo => info.hrHistory;
  List<Position> get positionList => info.positionHistory;
  DateTime watchAt = DateTime.now();
  int get heartRatePercent => info.heartRatePercent;
  Pace get pace => info.pace;
  Duration get duration => info.duration;
  ControlPoint? get cpNextWant => info.cpNextWant;

  Distance get nextDistance {
    final one = info.cpNextWant;
    if (one != null) {
      final p1 = one.position;
      final p22 = positionList.lastOrNull;
      if (p22 != null) {
        return p1.distance(p22);
      }
    }
    return const Distance(m: 1000);
  }

  String get nextCPSN {
    return info.cpNextWant?.snString ?? '';
  }
}

class EventOnMap {
  var info = EventInfo();
  var ext = EventInfoExt();
  var userList = <PlayerOnMap>[];

  int get id => info.id;

  String get name => ext.name;

  int get cpAllCount => ext.cpAllCount;
  final isHide = false.obs;

  PlayerOnMap? getUserById(int id) {
    for (final one in userList) {
      if (one.id == id) {
        return one;
      }
    }

    return null;
  }
}

abstract class MapWatchService extends GetxService {
  final Rx<MapWatch?> _instance = Rx(null);

  MapWatch? get instance => _instance.value;

  @protected
  Future<MapWatch> newInstanceByMap(MapInfo info);

  Future<void> setMap(MapInfo? mapInfo) async {
    _instance.value?.close();
    if (mapInfo != null) {
      final thisInstance = await newInstanceByMap(mapInfo);
      thisInstance.addPlugs([thisInstance.plugMap]);
      _instance.value = thisInstance;
      thisInstance.init();
      thisInstance.workFlushData();
    } else {
      _instance.value = null;
    }
  }
}

abstract class MapWatch extends PlugController {
  Future<void> workFlushData() async {
    while (isActive) {
      try {
        await flushData();
      } catch (e) {
        error(e);
      }
      await 1.seconds.delay();
    }
  }

  EventOnMap? getEventById(int id) {
    for (final one in eventList) {
      if (one.id == id) {
        return one;
      }
    }
    return null;
  }

  MapWatch({required this.id});

  final int id;
  String name = '';
  final plugMap = PlugMap();
  final eventList = <EventOnMap>[].obs;

  Future<void> flushData() async {
    final list = await getEventList(id);

    final newList = <EventOnMap>[];

    for (final one in list) {
      late EventOnMap event;
      final old = getEventById(one.id);
      if (old != null) {
        event = old;
        await updateEvent(event, one);
      } else {
        event = await initEvent(one);
      }
      newList.add(event);
    }

    eventList.value = newList;
  }

  Future<EventOnMap> initEvent(EventInfo info) async {
    final ext = await getEventInfoExt(info.id);
    final one = EventOnMap()
      ..info = info
      ..ext = ext;
    for (var p in info.players) {
      one.userList.add(await initPlayer(info, p));
    }
    return one;
  }

  Future<PlayerOnMap> initPlayer(EventInfo event, PlayerInfo playerInfo) async {
    final userId = playerInfo.id;
    final ext = await getPlayerInfoExt(event.id, userId);
    final player = PlayerOnMap()..info = playerInfo;
    player.ext = ext;
    return player;
  }

  Future<void> updateEvent(EventOnMap old, EventInfo newOne) async {
    final newUserList = <PlayerOnMap>[];

    for (final nUser in newOne.players) {
      late PlayerOnMap user;
      final oUser = old.getUserById(nUser.id);
      if (oUser != null) {
        user = await updatePlayer(oUser, nUser);
      } else {
        user = await initPlayer(old.info, nUser);
      }
      await playerUpdateMap(user);
      newUserList.add(user);
    }
    old.userList = newUserList;
  }

  Future<void> playerUpdateMap(PlayerOnMap info) async {
    for (var one in info.cpList) {
      one.onMap = await plugMap.gameMap.worldToPixel(one.position);
    }
  }

  Future<PlayerOnMap> updatePlayer(PlayerOnMap old, PlayerInfo newUser) async {
    final indexMap = <int, TracePoint>{};
    old.info = newUser;
    old.ext = old._ext;

    for (final one in old.trace) {
      indexMap[one.ts.inMilliseconds] = one;
    }

    for (final one in newUser.positionHistory) {
      final t = one.timestamp;
      final startAt = old.watchAt;
      final ts = t.difference(startAt);
      if (ts.inMilliseconds > 0 && !indexMap.containsKey(ts.inMilliseconds)) {
        final pos = one;
        final oneTrace = TracePoint()
          ..ts = ts
          ..position = pos;
        if (plugMap.isInitFinish) {
          oneTrace.onMap =
              await plugMap.gameMap.worldToPixel(oneTrace.position);
        }

        old.trace.add(oneTrace);
      } else if (old.trace.isEmpty) {
        final oneTrace = TracePoint()
          ..ts = Duration.zero
          ..position = one;
        if (plugMap.isInitFinish) {
          oneTrace.onMap =
              await plugMap.gameMap.worldToPixel(oneTrace.position);

          old.trace.add(oneTrace);
        }
      }
    }

    return old;
  }

  @protected
  Future<List<EventInfo>> getEventList(int mapId);

  @protected
  Future<EventInfoExt> getEventInfoExt(int id);

  @protected
  Future<PlayerInfoExt> getPlayerInfoExt(int eventId, int userId);
}
