import * as React from "react";
import * as R from "ramda";

import {
  BaseFocusable as BaseFocusableInterface,
  BoundingRect,
} from "./BaseFocusable.interface";

import { focusManager } from "@applicaster/zapp-react-native-utils/appUtils/focusManager";
import { noop } from "@applicaster/zapp-react-native-utils/functionUtils";

type ScrollDirection = FocusManager.IOS.Direction | FocusManager.Web.Direction;

type Props = {
  initialFocus?: boolean;
  id: string;
  groupId?: string;
  preferredFocus?: boolean;
  onRegister?: (focusable: FocusManager.TouchableRef) => void;
  onUnregister?: (focusable: FocusManager.TouchableRef) => void;
  willReceiveFocus?: FocusManager.FocusEventCB;
  hasReceivedFocus?: FocusManager.FocusEventCB;
  willLoseFocus?: FocusManager.FocusEventCB;
  hasLostFocus?: FocusManager.FocusEventCB;
  failedLostFocus?: FocusManager.FocusEventCB;
  onPress?: (nativeEvent?: Nullable<React.MouseEvent>) => void;
  onLongPress?: (nativeEvent?: Nullable<React.MouseEvent>) => void;
  onPressIn?: (nativeEvent?: Nullable<React.MouseEvent>) => void;
  onPressOut?: (nativeEvent?: Nullable<React.MouseEvent>) => void;
  onFocus?: FocusManager.FocusEventCB;
  onBlur?: FocusManager.FocusEventCB;
  selected?: boolean;
  skipFocusManagerRegistration?: boolean;
};

export class BaseFocusable<
  T extends Props = Props,
> extends BaseFocusableInterface<T> {
  constructor(props) {
    super(props);
    const { initialFocus } = this.props;

    this.state = {
      focused: initialFocus || false,
    };

    this.ref = React.createRef();
  }

  componentDidMount() {
    this._isMounted = true;
    const component = this;

    const { skipFocusManagerRegistration } = this.props;

    if (skipFocusManagerRegistration) {
      return;
    }

    focusManager.register({
      id: this.getId(),
      component: component,
    });

    const { initialFocus } = this.props;

    if (initialFocus) {
      focusManager.setFocus(this.getId());
    }
  }

  getId(props?: { id: string; groupId: string }) {
    const { id } = props || this.props;

    return id;
  }

  getRect() {
    let boundingRect: BoundingRect = {};

    if (this.ref && this.ref.current) {
      boundingRect = this.ref.current.getBoundingClientRect();
    }

    return boundingRect;
  }

  isMounted() {
    return this._isMounted;
  }

  componentWillUnmount() {
    this._isMounted = false;

    const { skipFocusManagerRegistration } = this.props;

    if (skipFocusManagerRegistration) {
      return;
    }

    focusManager.unregister(this.getId(), { group: this.isGroup || false });
  }

  componentDidUpdate(prevProps) {
    const { skipFocusManagerRegistration } = this.props;

    if (skipFocusManagerRegistration) {
      return;
    }

    if (prevProps?.id && this.props?.id && prevProps.id !== this.props.id) {
      focusManager.unregister(this.getId(prevProps), {
        group: this.isGroup || false,
      });
    }
  }

  measureView() {}

  onRegister = (focusable?: FocusManager.FocusableRef) => {
    const { onRegister = noop } = this.props;
    onRegister(focusable);
  };

  onUnregister = (focusable?: FocusManager.FocusableRef) => {
    const { onUnregister = noop } = this.props;
    onUnregister(focusable);
  };

  willLoseFocus: FocusManager.FocusEventCB = (focusable, scrollDirection) => {
    const { willLoseFocus = noop } = this.props;
    willLoseFocus(focusable, scrollDirection);
  };

  willReceiveFocus: FocusManager.FocusEventCB = (
    focusable,
    scrollDirection
  ) => {
    const { willReceiveFocus = noop } = this.props;
    willReceiveFocus(focusable, scrollDirection);
  };

  /**
   * will invoke the underlying component's focus method
   * @param {Object} focusable - sender
   * @param {Object} scrollDirection
   * @returns {Promise}
   */
  onFocus: FocusManager.FocusEventCB = (
    focusable,
    scrollDirection,
    context
  ) => {
    const { onFocus = noop } = this.props;
    this.setFocusedState(true);
    onFocus(focusable, scrollDirection, context);
  };

  /**
   * will invoke the underlying component's hasReceivedFocus method
   * @param {Object} focusable - sender
   * @param {Object} scrollDirection
   * @returns {Promise}
   */
  hasReceivedFocus: FocusManager.FocusEventCB = (
    focusable,
    scrollDirection
  ) => {
    const { hasReceivedFocus = noop } = this.props;
    hasReceivedFocus(focusable, scrollDirection);
  };

  /**
   * will invoke the underlying component's hasLostFocus method
   * @param {Object} focusable - sender
   * @param {Object} scrollDirection
   * @returns {Promise}
   */
  hasLostFocus: FocusManager.FocusEventCB = (focusable, scrollDirection) => {
    const { hasLostFocus = noop } = this.props;
    hasLostFocus(focusable, scrollDirection);
  };

  /**
   * will invoke the underlying component's failedLostFocus method
   * @param {Object} focusable - sender
   * @param {Object} scrollDirection
   * @returns {Promise}
   */
  failedLostFocus: FocusManager.FocusEventCB = (
    focusable: FocusManager.FocusableRef | undefined,
    scrollDirection?: ScrollDirection
  ) => {
    const { failedLostFocus = noop } = this.props;
    failedLostFocus(focusable, scrollDirection);
  };

  /**
   * will invoke the underlying component's onBlur method
   * @param {Object} focusable - sender
   * @param {Object} scrollDirection
   * @returns {Promise}
   */
  onBlur: FocusManager.FocusEventCB = (focusable, scrollDirection) => {
    const { onBlur = noop } = this.props;
    this.setFocusedState(false);
    onBlur(focusable, scrollDirection);
  };

  onPress = (keyEvent?: Nullable<React.MouseEvent>) => {
    const { onPress = noop } = this.props;
    onPress(keyEvent);
  };

  /**
   * will invoke the underlying component's onLongPress method
   * @param {Object} keyEvent
   * @returns {Promise}
   */

  onLongPress = (keyEvent?: Nullable<React.MouseEvent>) => {
    const { onLongPress = noop } = this.props;
    onLongPress(keyEvent);
  };

  onPressOut = (keyEvent?: Nullable<React.MouseEvent>) => {
    const { onPressOut = noop } = this.props;
    onPressOut(keyEvent);
  };

  onPressIn = (keyEvent?: Nullable<React.MouseEvent>) => {
    const { onPressIn = noop } = this.props;
    onPressIn(keyEvent);
  };

  isInGroup(group: FocusManager.TouchableRef) {
    const { groupId } = this.props;
    const { id } = group.props;

    return id === groupId;
  }

  setFocusedState = (focused) => {
    if (this._isMounted) {
      this.setState({ focused });
    }
  };

  /**
   * will invoke the underlying component's focus method
   * @param {Object} scrollDirection
   * @returns {Promise}
   */
  focus(_, scrollDirection, context?: FocusManager.FocusContext) {
    return this.onFocus(this, scrollDirection, context); // invokeComponentMethod(this, "onFocus", scrollDirection, context);
  }

  /**
   * will invoke the underlying component's blur method
   * @param {Object} scrollDirection
   * @returns {Promise}
   */
  blur(
    _,
    scrollDirection?: FocusManager.Web.Direction | FocusManager.IOS.Direction,
    context?: FocusManager.FocusContext
  ) {
    return this.onBlur(this, scrollDirection, context);
  }

  /**
   * Sets the focus on this item. Will trigger sequentially a sequence of
   * functions (willReceiveFocus, focus, hasReceivedFocus). If these functions (defined in the Focusable
   * Item underlying component) return promises, execution will wait before it proceeds to the next. This
   * is useful for triggering sequential operations that specifically require to fully run before or after
   * focus is actually set on that item.
   * @param {string} scrollDirection string representation of the direction of the navigation which landed
   * to this item being focused
   */
  _executeFocusSequence(methodNames, scrollDirection, context) {
    return R.reduce(
      (sequence, methodName) => {
        const method = this[methodName]; // Access the method by name

        if (typeof method !== "function") {
          throw new Error(
            `Method '${methodName}' not found or not a function.`
          );
        }

        return sequence
          .then(() => method.call(this, this, scrollDirection, context))
          .catch((e) => {
            throw e; // Re-throw for consistent error handling
          });
      },
      Promise.resolve(),
      methodNames
    );
  }

  setFocus(
    scrollDirection?: ScrollDirection,
    context?: FocusManager.FocusContext
  ) {
    const focusMethods = ["willReceiveFocus", "focus", "hasReceivedFocus"];

    return this._executeFocusSequence(focusMethods, scrollDirection, context);
  }

  setBlur(
    scrollDirection?: ScrollDirection,
    context?: FocusManager.FocusContext
  ) {
    const blurMethods = ["willLoseFocus", "blur", "hasLostFocus"];

    return this._executeFocusSequence(blurMethods, scrollDirection, context);
  }
}
