import 'package:flutter/painting.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'danmaku.freezed.dart';
part 'danmaku.g.dart';


class DanmakuPanelSettings {
  final bool enabled;
  final double opacity;
  final double area;
  final double fontSize;
  final double speed;
  final double offset;
  final bool showTop;
  final bool showScroll;
  final bool showBottom;
  final bool colored;
  final double strokeWidth;
  final bool bold;
  final String? fontFamily;
  final double lineHeight;
  final bool fixedToScroll;
  final int maxRows;
  final bool heatmapEnabled;

  const DanmakuPanelSettings({
    this.enabled = true,
    this.opacity = 1.0,
    this.area = 0.85,
    this.fontSize = 20,
    this.speed = 1.0,
    this.offset = 0.0,
    this.showTop = true,
    this.showScroll = true,
    this.showBottom = false,
    this.colored = true,
    this.strokeWidth = 1.5,
    this.bold = false,
    this.fontFamily,
    this.lineHeight = 1.6,
    this.fixedToScroll = false,
    this.maxRows = 0,
    this.heatmapEnabled = true,
  });

  DanmakuPanelSettings copyWith({
    bool? enabled,
    double? opacity,
    double? area,
    double? fontSize,
    double? speed,
    double? offset,
    bool? showTop,
    bool? showScroll,
    bool? showBottom,
    bool? colored,
    double? strokeWidth,
    bool? bold,
    Object? fontFamily = _sentinel,
    double? lineHeight,
    bool? fixedToScroll,
    int? maxRows,
    bool? heatmapEnabled,
  }) => DanmakuPanelSettings(
    enabled: enabled ?? this.enabled,
    opacity: opacity ?? this.opacity,
    area: area ?? this.area,
    fontSize: fontSize ?? this.fontSize,
    speed: speed ?? this.speed,
    offset: offset ?? this.offset,
    showTop: showTop ?? this.showTop,
    showScroll: showScroll ?? this.showScroll,
    showBottom: showBottom ?? this.showBottom,
    colored: colored ?? this.colored,
    strokeWidth: strokeWidth ?? this.strokeWidth,
    bold: bold ?? this.bold,
    fontFamily: fontFamily == _sentinel
        ? this.fontFamily
        : fontFamily as String?,
    lineHeight: lineHeight ?? this.lineHeight,
    fixedToScroll: fixedToScroll ?? this.fixedToScroll,
    maxRows: maxRows ?? this.maxRows,
    heatmapEnabled: heatmapEnabled ?? this.heatmapEnabled,
  );

  @override
  bool operator ==(Object other) {
    return identical(this, other) ||
        other is DanmakuPanelSettings &&
            enabled == other.enabled &&
            opacity == other.opacity &&
            area == other.area &&
            fontSize == other.fontSize &&
            speed == other.speed &&
            offset == other.offset &&
            showTop == other.showTop &&
            showScroll == other.showScroll &&
            showBottom == other.showBottom &&
            colored == other.colored &&
            strokeWidth == other.strokeWidth &&
            bold == other.bold &&
            fontFamily == other.fontFamily &&
            lineHeight == other.lineHeight &&
            fixedToScroll == other.fixedToScroll &&
            maxRows == other.maxRows &&
            heatmapEnabled == other.heatmapEnabled;
  }

  @override
  int get hashCode => Object.hash(
    enabled,
    opacity,
    area,
    fontSize,
    speed,
    offset,
    showTop,
    showScroll,
    showBottom,
    colored,
    strokeWidth,
    bold,
    fontFamily,
    lineHeight,
    fixedToScroll,
    maxRows,
    heatmapEnabled,
  );
}

const Object _sentinel = Object();

class DanmakuSource {
  final String id;
  final String name;
  final String url;
  final bool enabled;

  const DanmakuSource({
    required this.id,
    required this.name,
    required this.url,
    this.enabled = true,
  });

  DanmakuSource copyWith({
    String? id,
    String? name,
    String? url,
    bool? enabled,
  }) => DanmakuSource(
    id: id ?? this.id,
    name: name ?? this.name,
    url: url ?? this.url,
    enabled: enabled ?? this.enabled,
  );

  String get displayName {
    final trimmed = name.trim();
    if (trimmed.isNotEmpty) return trimmed;
    return url;
  }

  Map<String, dynamic> toJson() => {
    'id': id,
    'name': name,
    'url': url,
    'enabled': enabled,
  };

  factory DanmakuSource.fromJson(Map<String, dynamic> json) {
    final url = (json['url'] ?? '').toString().trim();
    return DanmakuSource(
      id: (json['id'] ?? url).toString(),
      name: (json['name'] ?? '').toString(),
      url: url,
      enabled: json['enabled'] is bool ? json['enabled'] as bool : true,
    );
  }

  @override
  bool operator ==(Object other) {
    return identical(this, other) ||
        other is DanmakuSource &&
            id == other.id &&
            name == other.name &&
            url == other.url &&
            enabled == other.enabled;
  }

  @override
  int get hashCode => Object.hash(id, name, url, enabled);
}


enum DanmakuCommentStatus { loading, ready, failed }


sealed class DanmakuSessionState {
  const DanmakuSessionState();
}


class DanmakuSessionIdle extends DanmakuSessionState {
  const DanmakuSessionIdle();
}


class DanmakuSessionMatching extends DanmakuSessionState {
  const DanmakuSessionMatching();
}


class DanmakuSessionMatched extends DanmakuSessionState {
  final List<DanmakuMatchCandidate> matches;
  final int selectedIndex;
  final DanmakuCommentStatus commentStatus;
  final List<DanmakuComment> comments;

  const DanmakuSessionMatched({
    required this.matches,
    required this.selectedIndex,
    required this.commentStatus,
    this.comments = const [],
  });

  DanmakuSessionMatched copyWith({
    List<DanmakuMatchCandidate>? matches,
    int? selectedIndex,
    DanmakuCommentStatus? commentStatus,
    List<DanmakuComment>? comments,
  }) => DanmakuSessionMatched(
    matches: matches ?? this.matches,
    selectedIndex: selectedIndex ?? this.selectedIndex,
    commentStatus: commentStatus ?? this.commentStatus,
    comments: comments ?? this.comments,
  );
}


class DanmakuSessionEmpty extends DanmakuSessionState {
  const DanmakuSessionEmpty();
}


class DanmakuSessionFailed extends DanmakuSessionState {
  final String message;
  const DanmakuSessionFailed(this.message);
}


@freezed
abstract class DanmakuMatchContext with _$DanmakuMatchContext {
  const factory DanmakuMatchContext({
    required String itemId,
    required String name,
    required String? type,
    required String? seriesName,
    required int? season,
    required int? episode,
    required int durationSec,
  }) = _DanmakuMatchContext;
}

@freezed
abstract class DanmakuMatch with _$DanmakuMatch {
  const factory DanmakuMatch({
    @JsonKey(fromJson: _intRequired) required int episodeId,
    @JsonKey(fromJson: _intOrZero) @Default(0) int animeId,
    @Default('') String animeTitle,
    @Default('') String episodeTitle,
  }) = _DanmakuMatch;

  factory DanmakuMatch.fromJson(Map<String, dynamic> json) =>
      _$DanmakuMatchFromJson(json);
}

class DanmakuMatchCandidate {
  final DanmakuSource source;
  final DanmakuMatch match;

  const DanmakuMatchCandidate({required this.source, required this.match});

  int get episodeId => match.episodeId;
  int get animeId => match.animeId;
  String get animeTitle => match.animeTitle;
  String get episodeTitle => match.episodeTitle;

  @override
  bool operator ==(Object other) {
    return identical(this, other) ||
        other is DanmakuMatchCandidate &&
            source == other.source &&
            match == other.match;
  }

  @override
  int get hashCode => Object.hash(source, match);
}


@freezed
abstract class DanmakuBangumiEpisode with _$DanmakuBangumiEpisode {
  const factory DanmakuBangumiEpisode({
    @JsonKey(fromJson: _intRequired) required int episodeId,
    @Default('') String episodeTitle,
    @JsonKey(fromJson: _stringFromAny) @Default('') String episodeNumber,
  }) = _DanmakuBangumiEpisode;

  factory DanmakuBangumiEpisode.fromJson(Map<String, dynamic> json) =>
      _$DanmakuBangumiEpisodeFromJson(json);
}

int _intRequired(Object? value) => (value as num).toInt();

int _intOrZero(Object? value) => (value as num?)?.toInt() ?? 0;

String _stringFromAny(Object? value) => value?.toString() ?? '';

class DanmakuComment {
  final int cid;
  final double time;
  final int type;
  final Color color;
  final String text;
  int lane;

  DanmakuComment({
    required this.cid,
    required this.time,
    required this.type,
    required this.color,
    required this.text,
    this.lane = 0,
  });


  factory DanmakuComment.fromRaw(int cid, String p, String m) {
    final parts = p.split(',');
    final time = parts.isNotEmpty ? (double.tryParse(parts[0]) ?? 0.0) : 0.0;
    final type = parts.length > 1 ? (int.tryParse(parts[1]) ?? 1) : 1;
    final colorInt = parts.length > 2
        ? (int.tryParse(parts[2]) ?? 0xFFFFFF)
        : 0xFFFFFF;
    return DanmakuComment(
      cid: cid,
      time: time,
      type: type,
      color: Color(0xFF000000 | (colorInt & 0xFFFFFF)),
      text: m,
    );
  }
}
