import { useState, useCallback, useRef, useMemo } from "react";

export interface HistoryState<T> {
  /** Current state value */
  current: T;
  /** Number of states that can be undone */
  undoCount: number;
  /** Number of states that can be redone */
  redoCount: number;
  /** Whether undo is available */
  canUndo: boolean;
  /** Whether redo is available */
  canRedo: boolean;
}

export interface HistoryActions<T> {
  /** Set state and push to history */
  set: (newState: T | ((prev: T) => T)) => void;
  /** Set state without pushing to history (for continuous updates like dragging) */
  setWithoutHistory: (newState: T | ((prev: T) => T)) => void;
  /** Commit the current state to history (call after continuous updates) */
  commit: () => void;
  /** Undo to previous state */
  undo: () => void;
  /** Redo to next state */
  redo: () => void;
  /** Clear all history */
  clearHistory: () => void;
  /** Reset to initial state and clear history */
  reset: (newInitialState?: T) => void;
}

export interface UseHistoryOptions {
  /** Maximum number of history states to keep (default: 100) */
  maxHistoryLength?: number;
}

/**
 * A robust undo/redo hook with long history support.
 *
 * Features:
 * - Configurable history length (default 100 states)
 * - Separate methods for tracked vs untracked updates
 * - Commit method for batching continuous operations (like dragging)
 * - Stable function references (won't cause re-renders)
 */
export function useHistory<T>(
  initialState: T,
  options: UseHistoryOptions = {},
): [HistoryState<T>, HistoryActions<T>] {
  const { maxHistoryLength = 100 } = options;

  // Use refs to store history to avoid triggering re-renders on every history change
  const pastRef = useRef<T[]>([]);
  const futureRef = useRef<T[]>([]);

  // Current state - this triggers re-renders
  const [present, setPresent] = useState<T>(initialState);

  // Counter to force re-render when history changes (for canUndo/canRedo)
  const [historyVersion, setHistoryVersion] = useState(0);

  // Track uncommitted changes
  const lastCommittedStateRef = useRef<T>(initialState);
  const hasUncommittedChangesRef = useRef(false);

  // Deep compare for detecting actual changes
  const hasStateChanged = useCallback((a: T, b: T): boolean => {
    return JSON.stringify(a) !== JSON.stringify(b);
  }, []);

  /**
   * Push current state to history and set new state
   */
  const set = useCallback(
    (newState: T | ((prev: T) => T)) => {
      setPresent((currentPresent) => {
        const resolvedState =
          typeof newState === "function"
            ? (newState as (prev: T) => T)(currentPresent)
            : newState;

        // Only push to history if state actually changed
        if (!hasStateChanged(currentPresent, resolvedState)) {
          return currentPresent;
        }

        // Push current state to past
        pastRef.current = [...pastRef.current, currentPresent];
        // Trim to max length
        if (pastRef.current.length > maxHistoryLength) {
          pastRef.current = pastRef.current.slice(
            pastRef.current.length - maxHistoryLength,
          );
        }

        // Clear future on new action
        futureRef.current = [];

        // Update last committed state
        lastCommittedStateRef.current = resolvedState;
        hasUncommittedChangesRef.current = false;

        // Trigger re-render for history state
        setHistoryVersion((v) => v + 1);

        return resolvedState;
      });
    },
    [maxHistoryLength, hasStateChanged],
  );

  /**
   * Set state without pushing to history
   * Useful for continuous updates like dragging
   */
  const setWithoutHistory = useCallback(
    (newState: T | ((prev: T) => T)) => {
      setPresent((currentPresent) => {
        const resolvedState =
          typeof newState === "function"
            ? (newState as (prev: T) => T)(currentPresent)
            : newState;

        // Mark as having uncommitted changes
        if (hasStateChanged(lastCommittedStateRef.current, resolvedState)) {
          hasUncommittedChangesRef.current = true;
        }

        return resolvedState;
      });
    },
    [hasStateChanged],
  );

  /**
   * Commit the current state to history
   * Call this after a series of setWithoutHistory calls
   */
  const commit = useCallback(() => {
    if (!hasUncommittedChangesRef.current) return;

    setPresent((currentPresent) => {
      // Only commit if there's an actual change from last committed state
      if (!hasStateChanged(lastCommittedStateRef.current, currentPresent)) {
        hasUncommittedChangesRef.current = false;
        return currentPresent;
      }

      // Push last committed state to history
      pastRef.current = [...pastRef.current, lastCommittedStateRef.current];
      if (pastRef.current.length > maxHistoryLength) {
        pastRef.current = pastRef.current.slice(
          pastRef.current.length - maxHistoryLength,
        );
      }

      // Clear future on new action
      futureRef.current = [];

      // Update last committed state
      lastCommittedStateRef.current = currentPresent;
      hasUncommittedChangesRef.current = false;

      // Trigger re-render for history state
      setHistoryVersion((v) => v + 1);

      return currentPresent;
    });
  }, [maxHistoryLength, hasStateChanged]);

  /**
   * Undo to previous state
   */
  const undo = useCallback(() => {
    // Check BEFORE the setState call - this is critical
    const past = pastRef.current;
    if (past.length === 0) return;

    // Get the previous state synchronously
    const previous = past[past.length - 1];

    // Validate previous state exists
    if (previous === undefined) return;

    // Update refs synchronously before setState
    pastRef.current = past.slice(0, -1);

    setPresent((currentPresent) => {
      // Push current to future
      futureRef.current = [currentPresent, ...futureRef.current];

      // Update last committed state
      lastCommittedStateRef.current = previous;
      hasUncommittedChangesRef.current = false;

      // Trigger re-render for history state
      setHistoryVersion((v) => v + 1);

      return previous;
    });
  }, []);

  /**
   * Redo to next state
   */
  const redo = useCallback(() => {
    // Check BEFORE the setState call - this is critical
    const future = futureRef.current;
    if (future.length === 0) return;

    // Get the next state synchronously
    const next = future[0];

    // Validate next state exists
    if (next === undefined) return;

    // Update refs synchronously before setState
    futureRef.current = future.slice(1);

    setPresent((currentPresent) => {
      // Push current to past
      pastRef.current = [...pastRef.current, currentPresent];

      // Update last committed state
      lastCommittedStateRef.current = next;
      hasUncommittedChangesRef.current = false;

      // Trigger re-render for history state
      setHistoryVersion((v) => v + 1);

      return next;
    });
  }, []);

  /**
   * Clear all history but keep current state
   */
  const clearHistory = useCallback(() => {
    pastRef.current = [];
    futureRef.current = [];
    setPresent((currentPresent) => {
      lastCommittedStateRef.current = currentPresent;
      hasUncommittedChangesRef.current = false;
      setHistoryVersion((v) => v + 1);
      return currentPresent;
    });
  }, []);

  /**
   * Reset to initial state and clear history
   */
  const reset = useCallback(
    (newInitialState?: T) => {
      const stateToUse = newInitialState ?? initialState;
      pastRef.current = [];
      futureRef.current = [];
      lastCommittedStateRef.current = stateToUse;
      hasUncommittedChangesRef.current = false;
      setPresent(stateToUse);
      setHistoryVersion((v) => v + 1);
    },
    [initialState],
  );

  // Memoize state object
  const state = useMemo<HistoryState<T>>(
    () => ({
      current: present,
      undoCount: pastRef.current.length,
      redoCount: futureRef.current.length,
      canUndo: pastRef.current.length > 0,
      canRedo: futureRef.current.length > 0,
    }),
    [present, historyVersion],
  );

  // Memoize actions object - these functions are stable
  const actions = useMemo<HistoryActions<T>>(
    () => ({
      set,
      setWithoutHistory,
      commit,
      undo,
      redo,
      clearHistory,
      reset,
    }),
    [set, setWithoutHistory, commit, undo, redo, clearHistory, reset],
  );

  return [state, actions];
}

export default useHistory;
