import { path } from "ramda";
import { isString } from "@applicaster/zapp-react-native-utils/stringUtils";
import * as FOCUS_EVENTS from "@applicaster/zapp-react-native-utils/appUtils/focusManager/events";

import { logger } from "./logger";
import { TreeNode } from "./TreeNode";
import { Tree } from "./Tree";
import { subscriber } from "../functionUtils";
import { getFocusableId, toFocusDirection } from "./utils";

export {
  toFocusDirection,
  isHorizontalDirection,
  isVerticalDirection,
} from "./utils";

class FocusManager {
  private static instance: FocusManager;

  private _focusedId: string | null = null;
  private _prevFocusedId: string | null = null;
  public previousNavigationDirection: FocusManager.Android.NavDir = null;

  /**
   * @deprecated */
  public focusableComponents: FocusManager.TouchableReactRef[] = [];
  private eventHandler = subscriber();

  private tree = new Tree();

  on(event: string, callback: (...args: any[]) => void) {
    this.eventHandler?.on?.(event, callback);
  }

  invokeHandler(event: string, ...args: any[]) {
    this.eventHandler?.invokeHandler?.(event, ...args);
  }

  removeHandler(event: string, callback: (...args: any[]) => void) {
    this.eventHandler?.removeHandler?.(event, callback);
  }

  get focused() {
    const focusedRef = this.focusedId
      ? FocusManager.findFocusable(this.focusedId)
      : { current: null };

    return focusedRef?.current;
  }

  get prevFocused() {
    const focusedRef = this.prevFocusedId
      ? FocusManager.findFocusable(this.prevFocusedId)
      : { current: null };

    return focusedRef?.current;
  }

  get focusedId() {
    return FocusManager.instance._focusedId;
  }

  get prevFocusedId() {
    return FocusManager.instance._prevFocusedId;
  }

  public static getInstance(): FocusManager {
    if (!FocusManager.instance) {
      FocusManager.instance = new FocusManager();
    }

    return FocusManager.instance;
  }

  public static findFocusable(id) {
    return FocusManager.instance.focusableComponents.find(
      (component) => getFocusableId(component) === id
    );
  }

  private static getNextFocusable(
    direction: FocusManager.Android.FocusNavigationDirections,
    focusable?: FocusManager.TouchableReactRef
  ) {
    const props = focusable
      ? focusable.current?.props
      : FocusManager.instance.focused?.props;

    const focusDirection = toFocusDirection(direction);
    const nextFocusable = props?.[focusDirection];

    if (!nextFocusable) {
      return null;
    }

    if (isString(nextFocusable)) {
      return FocusManager.findFocusable(nextFocusable);
    }

    return nextFocusable;
  }

  private static isFocusable(component) {
    if (!component) {
      return {
        isFocusable: false,
        error: "ID or reference to your component is missing",
      };
    }

    if (isString(component)) {
      // check if component is registered
      const _component = FocusManager.findFocusable(component);

      if (!_component) {
        return {
          isFocusable: false,
          error: `Focusable component with id ${component} is not registered`,
        };
      } else {
        return { isFocusable: true };
      }
    }

    if (!component.current) {
      return {
        isFocusable: false,
        error:
          "Reference to your component needs to include 'current' property",
      };
    }

    return { isFocusable: true };
  }

  updateFocusedSilently(nextFocus: FocusManager.TouchableReactRef) {
    const nextFocusId = getFocusableId(nextFocus);

    // Check that nextFocus is a valid focusable
    const isFocusable = FocusManager.isFocusable(nextFocus);

    if (isFocusable && nextFocusId) {
      FocusManager.instance._focusedId = nextFocusId;
    } else {
      if (!isFocusable) {
        // this will include cases when nextFocus is null, a string or doesn't have a 'current' property
        logger.warning(
          "Attempted to focus a non-focusable component, focused element wasn't changed",
          {
            attemptedId: nextFocusId,
          }
        );
      }

      if (!nextFocusId) {
        logger.warning(
          "Attempted to focus a component without a valid ID, focused element wasn't changed",
          {
            attemptedId: nextFocusId,
          }
        );
      }
    }
  }

  private setFocusLocal(nextFocus: FocusManager.TouchableReactRef) {
    FocusManager.instance._prevFocusedId = FocusManager.instance._focusedId;
    FocusManager.instance._focusedId = nextFocus?.current?.props?.id ?? null;
  }

  private setPreviousNavigationDirection(
    options: Nullable<FocusManager.Android.CallbackOptions>
  ) {
    if (options?.direction) {
      FocusManager.instance.previousNavigationDirection = options.direction;
    }
  }

  registerFocusable(
    component: FocusManager.TouchableReactRef,
    parentFocusable: FocusManager.TouchableReactRef,
    isFocusableCell: boolean
  ) {
    const focusableId = getFocusableId(component);
    const focusableComponent = FocusManager.findFocusable(focusableId);

    if (!focusableComponent && component) {
      this.focusableComponents.push(component);

      this.tree.add(component, parentFocusable, isFocusableCell);
    } else {
      logger.warning("Focusable component already registered", {
        id: focusableId,
      });
    }

    return () => this.unregisterFocusable(focusableId);
  }

  unregisterFocusable(focusableId: string) {
    const node = this.tree.find(focusableId);

    this.focusableComponents = this.focusableComponents.filter(
      (c) => c !== node?.component
    );

    this.tree.remove(focusableId);
  }

  private setNextFocus(
    nextFocus: FocusManager.TouchableReactRef,
    options?: FocusManager.Android.CallbackOptions
  ) {
    if (nextFocus?.current?.props?.blockFocus) {
      return;
    }

    if (nextFocus?.current?.props?.disableFocus) {
      const direction = FocusManager.instance.extractDirectionFromOptions(
        options ?? null
      );

      if (!direction) {
        // failed to extract direction - ignore this focus attempt
        return;
      }

      const nextNextFocus = FocusManager.getNextFocusable(direction, nextFocus);

      FocusManager.instance.setFocus(nextNextFocus, options);
    } else {
      FocusManager.instance.setFocusLocal(nextFocus);

      FocusManager.instance.blurPrevious(options);
      this.eventHandler?.invokeHandler?.(FOCUS_EVENTS.FOCUS, nextFocus);

      FocusManager.instance.setPreviousNavigationDirection(options ?? null);

      nextFocus?.current?.onFocus?.(nextFocus.current, options ?? {});
    }
  }

  blurPrevious(options?: FocusManager.Android.CallbackOptions) {
    if (options) {
      FocusManager.instance.prevFocused?.onBlur?.(
        FocusManager.instance.prevFocused,
        options
      );
    }
  }

  onDisableFocusChange = (id) => {
    if (FocusManager.instance.isFocused(id)) {
      // Move focus to next one
      const nextFocus = FocusManager.instance.focused?.props
        ?.nextFocusDown as string;

      if (nextFocus) {
        // HACK: hack to fix the hack below
        // HACK: putting call to the end of the event loop so the next component has a chane to be registered
        setTimeout(() => {
          FocusManager.instance.setFocus(nextFocus, {
            direction: "down",
          });
        }, 0);
      }
    }
  };

  setFocus(
    newFocus: FocusManager.TouchableReactRef | string,
    options?: FocusManager.Android.CallbackOptions
  ) {
    // Checks if element is focusable
    const { isFocusable, error } = FocusManager.isFocusable(newFocus);

    if (error) {
      logger.error({ message: error });

      return;
    }

    if (isFocusable) {
      let newFocusRef: FocusManager.TouchableReactRef | null = null;

      if (isString(newFocus)) {
        const newFocusable = FocusManager.findFocusable(newFocus);

        if (newFocusable) {
          newFocusRef = newFocusable;
        }
      } else {
        newFocusRef = newFocus;
      }

      if (newFocusRef) {
        FocusManager.instance.setNextFocus(newFocusRef, options);
      }
    }
  }

  getFocusedNode() {
    return this.tree.find(FocusManager.instance.focusedId);
  }

  getNextFocusable(direction) {
    return FocusManager.getNextFocusable(direction);
  }

  getNextFocusableForParent(direction) {
    const focusDirection = toFocusDirection(direction);
    const parentNode = this.tree.findParent(FocusManager.instance.focusedId);

    return parentNode?.component?.current?.props[focusDirection];
  }

  getSecondChildId(id) {
    const node = this.tree.find(id);

    return path(["children", 1, "id"], node);
  }

  isFocused(id: string | number) {
    return FocusManager.instance.focusedId === id;
  }

  resetFocus() {
    if (FocusManager.instance.focused?.onBlur) {
      FocusManager.instance.focused.onBlur(FocusManager.instance.focused, {});
    }

    FocusManager.instance.setFocusLocal({ current: null });
  }

  private extractDirectionFromOptions(
    options: Nullable<FocusManager.Android.CallbackOptions>
  ): Nullable<FocusManager.Android.FocusNavigationDirections> {
    if (options?.direction) {
      return options.direction;
    }

    if (options?.initialFocusDirection) {
      return options.initialFocusDirection;
    }

    if (FocusManager.instance.previousNavigationDirection) {
      return FocusManager.instance.previousNavigationDirection;
    }

    logger.warning("Failed to extract focusDirection");

    return null;
  }

  public isFocusableChildOf(
    focusable: FocusManager.TouchableReactRef | string,
    referenceFocusable: FocusManager.TouchableReactRef | string,
    options: { direct: boolean } = { direct: false }
  ): boolean {
    const focusableNode = this.tree.findNode(focusable);
    const referenceNode = this.tree.findNode(referenceFocusable);

    if (!referenceNode || !focusableNode) return false;

    if (options.direct) {
      return referenceNode.children.some(({ id }) => {
        const focusableId = isString(focusable)
          ? focusable
          : getFocusableId(focusable);

        return id === focusableId;
      });
    } else {
      return !!referenceNode.findNode(focusable);
    }
  }

  private hasFocus = (node) => {
    if (node.children?.length > 0) {
      return node.children.some((item) => this.hasFocus(item));
    } else if (this.isFocused(node?.component?.current?.props.id)) {
      return true;
    }

    return false;
  };

  isAnyDescendantFocused(id: string) {
    const node: TreeNode | null = this.tree.find(id);

    if (node) {
      return this.hasFocus(node);
    } else {
      // return false;
      throw new Error(`Group with id ${id} not found`);
    }
  }
}

export const focusManager = FocusManager.getInstance();
