import 'dart:convert';

import 'package:flutter/material.dart';

import 'drawing_line.dart';
import 'paint_preset.dart';

/// Represents an undoable action.
sealed class DrawingAction {
  const DrawingAction();
}

/// Action for adding a line.
class AddLineAction extends DrawingAction {
  final DrawingLine line;
  const AddLineAction(this.line);
}

/// Action for erasing lines.
class EraseAction extends DrawingAction {
  final List<DrawingLine> erasedLines;
  const EraseAction(this.erasedLines);
}

/// Controller for managing drawing state with undo/redo support.
///
/// Manages a stack of [DrawingLine] objects and provides methods for
/// drawing, undoing, redoing, and serializing the drawing state.
class DrawingController extends ChangeNotifier {
  /// All completed lines in the drawing
  final List<DrawingLine> _lines = [];

  /// Stack of actions for undo functionality
  final List<DrawingAction> _undoStack = [];

  /// Stack of actions for redo functionality
  final List<DrawingAction> _redoStack = [];

  /// The line currently being drawn (null when not drawing)
  DrawingLine? _currentLine;

  /// Current paint preset being used
  PaintPreset _currentPreset = PaintPreset.blackPen;

  /// Whether eraser mode is active
  bool _isEraserMode = false;

  /// Lines erased in the current eraser stroke (for undo as single action)
  final List<DrawingLine> _currentErasedLines = [];

  /// Maximum number of history steps to keep
  final int maxHistorySteps;

  /// Whether there are unsaved changes since last load/clear
  bool _hasUnsavedChanges = false;

  DrawingController({this.maxHistorySteps = 50});

  // ---------------------------------------------------------------------------
  // Getters
  // ---------------------------------------------------------------------------

  /// All completed lines (read-only)
  List<DrawingLine> get lines => List.unmodifiable(_lines);

  /// The line currently being drawn
  DrawingLine? get currentLine => _currentLine;

  /// Current paint preset
  PaintPreset get currentPreset => _currentPreset;

  /// Whether eraser mode is active
  bool get isEraserMode => _isEraserMode;

  /// Whether undo is available
  bool get canUndo => _undoStack.isNotEmpty;

  /// Whether redo is available
  bool get canRedo => _redoStack.isNotEmpty;

  /// Whether there are unsaved changes since last load/clear/markSaved
  bool get hasUnsavedChanges => _hasUnsavedChanges;

  // ---------------------------------------------------------------------------
  // Drawing Operations
  // ---------------------------------------------------------------------------

  /// Starts a new line at the given normalized position.
  void startLine(Offset normalizedPoint) {
    if (_isEraserMode) {
      _currentErasedLines.clear();
      _eraseAtPoint(normalizedPoint);
      return;
    }

    _currentLine = DrawingLine(
      points: [normalizedPoint],
      color: _currentPreset.color,
      strokeWidth: _currentPreset.strokeWidth,
    );
    notifyListeners();
  }

  /// Adds a point to the current line.
  void addPoint(Offset normalizedPoint) {
    if (_isEraserMode) {
      _eraseAtPoint(normalizedPoint);
      return;
    }

    if (_currentLine == null) return;

    // Filter points that are too close to reduce memory usage
    if (_currentLine!.isPointTooClose(normalizedPoint)) return;

    _currentLine = _currentLine!.addPoint(normalizedPoint);
    notifyListeners();
  }

  /// Completes the current line and adds it to the history.
  void endLine() {
    if (_isEraserMode) {
      // If we erased lines in this stroke, record as single action
      if (_currentErasedLines.isNotEmpty) {
        _undoStack.add(EraseAction(List.from(_currentErasedLines)));
        _redoStack.clear();
        _trimHistory();
        _currentErasedLines.clear();
        _hasUnsavedChanges = true;
        notifyListeners(); // Update UI to enable undo button
      }
      return;
    }

    if (_currentLine == null) return;

    // Only add lines with at least 2 points
    if (_currentLine!.points.length >= 2) {
      _lines.add(_currentLine!);
      _undoStack.add(AddLineAction(_currentLine!));
      _redoStack.clear();
      _trimHistory();
      _hasUnsavedChanges = true;
    }

    _currentLine = null;
    notifyListeners();
  }

  /// Trims history to maxHistorySteps to prevent memory growth.
  void _trimHistory() {
    while (_undoStack.length > maxHistorySteps) {
      _undoStack.removeAt(0);
    }
  }

  // ---------------------------------------------------------------------------
  // Eraser Operations
  // ---------------------------------------------------------------------------

  /// Eraser hit radius in normalized coordinates
  static const double _eraserRadius = 0.015;

  /// Checks if a point is near a line and erases it if so.
  void _eraseAtPoint(Offset point) {
    // Find all lines that intersect with the eraser point
    final linesToRemove = <DrawingLine>[];

    for (final line in _lines) {
      if (_lineIntersectsPoint(
          line, point, _eraserRadius + line.strokeWidth / 2)) {
        linesToRemove.add(line);
      }
    }

    // Remove intersecting lines and track them for undo
    for (final line in linesToRemove) {
      _lines.remove(line);
      _currentErasedLines.add(line);
    }

    if (linesToRemove.isNotEmpty) {
      notifyListeners();
    }
  }

  /// Checks if a line intersects with a circular point.
  bool _lineIntersectsPoint(DrawingLine line, Offset point, double radius) {
    // Check if any point is within radius
    for (final linePoint in line.points) {
      if ((linePoint - point).distance <= radius) {
        return true;
      }
    }
    // Check line segments
    for (int i = 0; i < line.points.length - 1; i++) {
      if (_pointToSegmentDistance(point, line.points[i], line.points[i + 1]) <=
          radius) {
        return true;
      }
    }
    return false;
  }

  /// Calculates the distance from a point to a line segment.
  double _pointToSegmentDistance(Offset point, Offset segStart, Offset segEnd) {
    final dx = segEnd.dx - segStart.dx;
    final dy = segEnd.dy - segStart.dy;
    final lengthSquared = dx * dx + dy * dy;

    if (lengthSquared == 0) {
      // Segment is a point
      return (point - segStart).distance;
    }

    // Project point onto the line, clamping to segment
    var t = ((point.dx - segStart.dx) * dx + (point.dy - segStart.dy) * dy) /
        lengthSquared;
    t = t.clamp(0.0, 1.0);

    final projection = Offset(
      segStart.dx + t * dx,
      segStart.dy + t * dy,
    );

    return (point - projection).distance;
  }

  // ---------------------------------------------------------------------------
  // Eraser Mode
  // ---------------------------------------------------------------------------

  /// Toggles eraser mode.
  void toggleEraserMode() {
    _isEraserMode = !_isEraserMode;
    notifyListeners();
  }

  /// Sets eraser mode.
  void setEraserMode(bool enabled) {
    if (_isEraserMode != enabled) {
      _isEraserMode = enabled;
      notifyListeners();
    }
  }

  // ---------------------------------------------------------------------------
  // Undo/Redo
  // ---------------------------------------------------------------------------

  /// Undoes the last action.
  void undo() {
    if (!canUndo) return;

    final action = _undoStack.removeLast();

    switch (action) {
      case AddLineAction(:final line):
        // Remove the line that was added
        _lines.remove(line);
        _redoStack.add(action);
      case EraseAction(:final erasedLines):
        // Restore the lines that were erased
        _lines.addAll(erasedLines);
        _redoStack.add(action);
    }

    _hasUnsavedChanges = true;
    notifyListeners();
  }

  /// Redoes the last undone action.
  void redo() {
    if (!canRedo) return;

    final action = _redoStack.removeLast();

    switch (action) {
      case AddLineAction(:final line):
        // Re-add the line
        _lines.add(line);
        _undoStack.add(action);
      case EraseAction(:final erasedLines):
        // Re-erase the lines
        for (final line in erasedLines) {
          _lines.remove(line);
        }
        _undoStack.add(action);
    }

    _hasUnsavedChanges = true;
    notifyListeners();
  }

  /// Clears all lines from the canvas.
  void clear() {
    _lines.clear();
    _undoStack.clear();
    _redoStack.clear();
    _currentLine = null;
    _currentErasedLines.clear();
    _hasUnsavedChanges = false;
    notifyListeners();
  }

  // ---------------------------------------------------------------------------
  // Paint Preset
  // ---------------------------------------------------------------------------

  /// Sets the current paint preset and disables eraser mode.
  void setPreset(PaintPreset preset) {
    _currentPreset = preset;
    _isEraserMode = false;
    notifyListeners();
  }

  // ---------------------------------------------------------------------------
  // Serialization
  // ---------------------------------------------------------------------------

  /// Exports all lines to a JSON-serializable list.
  List<Map<String, dynamic>> toJsonList() {
    return _lines.map((line) => line.toJson()).toList();
  }

  /// Exports all lines to a JSON string.
  String toJsonString() {
    return jsonEncode(toJsonList());
  }

  /// Imports lines from a JSON-serializable list.
  void fromJsonList(List<Map<String, dynamic>> jsonList) {
    _lines.clear();
    _undoStack.clear();
    _redoStack.clear();
    _currentLine = null;
    _currentErasedLines.clear();
    _hasUnsavedChanges = false;

    for (final json in jsonList) {
      _lines.add(DrawingLine.fromJson(json));
    }

    notifyListeners();
  }

  /// Imports lines from a JSON string.
  void fromJsonString(String jsonString) {
    if (jsonString.isEmpty || jsonString == '[]') {
      clear();
      return;
    }

    final decoded = jsonDecode(jsonString) as List;
    if (decoded.isEmpty) {
      clear();
      return;
    }
    fromJsonList(decoded.cast<Map<String, dynamic>>());
  }

  /// Adds existing lines without clearing (for merging annotations).
  void addLines(List<DrawingLine> newLines) {
    _lines.addAll(newLines);
    _trimHistory();
    notifyListeners();
  }

  /// Marks the current state as saved (resets unsaved changes flag).
  void markSaved() {
    _hasUnsavedChanges = false;
  }

  @override
  void dispose() {
    _lines.clear();
    _undoStack.clear();
    _redoStack.clear();
    super.dispose();
  }
}
