/* eslint-disable @eslint-react/purity */
/* eslint-disable @eslint-react/no-unnecessary-use-prefix */
import type { ReactElement } from 'react';

import { DEBUG, DEBUG_MORE } from '../../config';
import { logUnequalProps } from '../../util/areShallowEqual';
import { incrementOverlayCounter } from '../../util/debugOverlay';
import { orderBy } from '../../util/iteratees';
import safeExec from '../../util/safeExec';
import { throttleWith } from '../../util/schedulers';
import { createSignal, isSignal, type Signal } from '../../util/signals';
import { requestMeasure, requestMutation } from '../fasterdom/fasterdom';
import { getIsBlockingAnimating } from './heavyAnimation';

export { getIsHeavyAnimating, beginHeavyAnimation, onFullyIdle } from './heavyAnimation';

export type Props = AnyLiteral;
export type FC<P extends Props = any> = (props: P) => any;
export type FC_withDebug =
  FC
  & { DEBUG_contentComponentName?: string };

export enum VirtualType {
  Empty,
  Text,
  Tag,
  Component,
  Fragment,
}

interface VirtualElementEmpty {
  type: VirtualType.Empty;
  target?: Node;
}

interface VirtualElementText {
  type: VirtualType.Text;
  target?: Node;
  value: string;
}

export interface VirtualElementTag {
  type: VirtualType.Tag;
  target?: HTMLElement | SVGElement;
  tag: string;
  props: Props;
  children: VirtualElementChildren;
}

export interface VirtualElementComponent {
  type: VirtualType.Component;
  componentInstance: ComponentInstance;
  props: Props;
  children: VirtualElementChildren;
}

export interface VirtualElementFragment {
  type: VirtualType.Fragment;
  children: VirtualElementChildren;
  placeholderTarget?: Comment;
}

export type StateHookSetter<T> = (newValue: ((current: T) => T) | T) => void;

export interface RefObject<T = unknown> {
  current: T;
  onChange?: NoneToVoidFunction;
}

export type ElementRef<T = HTMLElement> = RefObject<T | undefined>;

export enum MountState {
  Mounting,
  Mounted,
  Unmounted,
}

interface ComponentInstance {
  id: number;
  $element: VirtualElementComponent;
  Component: FC;
  name: string;
  props: Props;
  renderedValue?: any;
  lastMountNamespace?: string;
  mountState: MountState;
  context?: Record<string, Signal<unknown>>;
  hooks?: {
    state?: {
      cursor: number;
      byCursor: {
        value: any;
        nextValue: any;
        setter: StateHookSetter<any>;
      }[];
    };
    effects?: {
      cursor: number;
      byCursor: {
        dependencies?: readonly unknown[];
        schedule?: NoneToVoidFunction;
        cleanup?: NoneToVoidFunction;
        releaseSignals?: NoneToVoidFunction;
      }[];
    };
    memos?: {
      cursor: number;
      byCursor: {
        value: any;
        dependencies: readonly unknown[];
      }[];
    };
    refs?: {
      cursor: number;
      byCursor: RefObject[];
    };
  };
  prepareForFrame?: () => void;
  forceUpdate?: () => void;
  onUpdate?: () => void;
}

export type VirtualElement =
  VirtualElementEmpty
  | VirtualElementText
  | VirtualElementTag
  | VirtualElementComponent
  | VirtualElementFragment;
export type VirtualElementParent =
  VirtualElementTag
  | VirtualElementComponent
  | VirtualElementFragment;
export type VirtualElementChildren = VirtualElement[];
export type VirtualElementReal = Exclude<VirtualElement, VirtualElementComponent | VirtualElementFragment>;

// Compatibility with JSX types
export type TeactNode =
  ReactElement
  | string
  | number
  | boolean
  | undefined
  | TeactNode[];

type Effect = () => (NoneToVoidFunction | void);
type EffectCleanup = NoneToVoidFunction;

export type Context<T> = {
  defaultValue?: T;
  contextId: string;
  Provider: FC<{ value: T; children: TeactNode }>;
};

const Fragment = Symbol('Fragment') as unknown as FC<{ children: TeactNode }>;

const DEBUG_RENDER_THRESHOLD = 7;
const DEBUG_EFFECT_THRESHOLD = 7;
const DEBUG_SILENT_RENDERS_FOR = new Set(['TeactMemoWrapper', 'TeactNContainer', 'Button', 'ListItem', 'MenuItem']);
const MAX_EFFECT_CURSORS_PER_INSTANCE = 10000;

let contextCounter = 0;

let lastComponentId = 0;
let renderingInstance: ComponentInstance;

export function isParentElement($element: VirtualElement): $element is VirtualElementParent {
  return (
    $element.type === VirtualType.Tag
    || $element.type === VirtualType.Component
    || $element.type === VirtualType.Fragment
  );
}

function cloneElement($element: VirtualElement): VirtualElement {
  switch ($element.type) {
    case VirtualType.Empty:
      return { type: VirtualType.Empty };
    case VirtualType.Text:
      return { type: VirtualType.Text, value: $element.value };
    case VirtualType.Tag:
      return {
        type: VirtualType.Tag,
        tag: $element.tag,
        props: $element.props,
        children: $element.children.map(cloneElement),
      };
    case VirtualType.Component: {
      const { componentInstance } = $element;
      return createComponentInstance(componentInstance.Component, componentInstance.props, []);
    }
    case VirtualType.Fragment:
      return {
        type: VirtualType.Fragment,
        children: $element.children.map(cloneElement),
      };
  }
}

// Check if an element has been used (mounted/rendered) and needs cloning for reuse
export function isElementUsed($element: VirtualElement): boolean {
  switch ($element.type) {
    case VirtualType.Component:
      return $element.componentInstance.mountState !== MountState.Unmounted;
    case VirtualType.Fragment:
      return $element.placeholderTarget !== undefined || $element.children.some(isElementUsed);
    case VirtualType.Tag:
    case VirtualType.Text:
    case VirtualType.Empty:
      return $element.target !== undefined;
  }
}

function createElement(
  source: string | FC | typeof Fragment,
  props: Props,
  ...children: any[]
): VirtualElementParent | VirtualElementChildren {
  if (source === Fragment) {
    return buildFragmentElement(children);
  } else if (typeof source === 'function') {
    return createComponentInstance(source, props || {}, children);
  } else {
    return buildTagElement(source, props || {}, children);
  }
}

function buildFragmentElement(children: any[]): VirtualElementFragment {
  return {
    type: VirtualType.Fragment,
    children: buildChildren(children, true),
  };
}

function createComponentInstance(Component: FC, props: Props, children: any[]): VirtualElementComponent {
  if (children?.length) {
    props.children = children.length === 1 ? children[0] : children;
  }

  const componentInstance: ComponentInstance = {
    id: -1,
    $element: undefined as unknown as VirtualElementComponent,
    Component,
    name: Component.name,
    props,
    mountState: MountState.Unmounted,
  };

  componentInstance.$element = buildComponentElement(componentInstance);

  return componentInstance.$element;
}

function buildComponentElement(
  componentInstance: ComponentInstance,
  children?: VirtualElementChildren,
): VirtualElementComponent {
  return {
    type: VirtualType.Component,
    componentInstance,
    props: componentInstance.props,
    children: children ? buildChildren(children, true) : [],
  };
}

function buildTagElement(tag: string, props: Props, children: any[]): VirtualElementTag {
  return {
    type: VirtualType.Tag,
    tag,
    props,
    children: buildChildren(children),
  };
}

function buildChildren(children: any[], noEmpty = false): VirtualElement[] {
  const cleanChildren = dropEmptyTail(children, noEmpty);
  const newChildren = [];

  for (let i = 0, l = cleanChildren.length; i < l; i++) {
    const child = cleanChildren[i];
    if (Array.isArray(child)) {
      newChildren.push(...buildChildren(child, noEmpty));
    } else {
      newChildren.push(buildChildElement(child));
    }
  }

  return newChildren;
}

// We only need placeholders in the middle of collection (to ensure other elements order).
function dropEmptyTail(children: any[], noEmpty = false) {
  let i = children.length - 1;

  for (; i >= 0; i--) {
    if (!isEmptyPlaceholder(children[i])) {
      break;
    }
  }

  if (i === children.length - 1) {
    return children;
  }

  if (i === -1 && noEmpty) {
    return children.slice(0, 1);
  }

  return children.slice(0, i + 1);
}

function isEmptyPlaceholder(child: any) {
  return !child && child !== 0;
}

function buildChildElement(child: any): VirtualElement {
  if (isEmptyPlaceholder(child)) {
    return { type: VirtualType.Empty };
  } else if (isParentElement(child)) {
    return child;
  } else {
    return {
      type: VirtualType.Text,
      value: String(child),
    };
  }
}

const DEBUG_components: AnyLiteral = { TOTAL: { name: 'TOTAL', renders: 0 } };
const DEBUG_memos: Record<string, { key: string; calls: number; misses: number; hitRate: number }> = {};
const DEBUG_MEMOS_CALLS_THRESHOLD = 20;

if (DEBUG) {
  document.addEventListener('dblclick', () => {
    // eslint-disable-next-line no-console
    console.warn('COMPONENTS', orderBy(
      Object
        .values(DEBUG_components)
        .map(({ avgRenderTime, ...state }) => {
          return { ...state, ...(avgRenderTime !== undefined && { avgRenderTime: Number(avgRenderTime.toFixed(2)) }) };
        }),
      'renders',
      'desc',
    ));

    // eslint-disable-next-line no-console
    console.warn('MEMOS', orderBy(
      Object
        .values(DEBUG_memos)
        .filter(({ calls }) => calls >= DEBUG_MEMOS_CALLS_THRESHOLD)
        .map((state) => ({ ...state, hitRate: Number(state.hitRate.toFixed(2)) })),
      'hitRate',
      'asc',
    ));
  });
}

let instancesPendingUpdate = new Set<ComponentInstance>();
let idsToExcludeFromUpdate = new Set<number>();
let pendingEffects = new Map<number, Effect>();
let pendingCleanups = new Map<number, EffectCleanup>();
let pendingLayoutEffects = new Map<number, Effect>();
let pendingLayoutCleanups = new Map<number, EffectCleanup>();
let areImmediateEffectsCaptured = false;

/*
  Effect call order:
  - Regular call order inside component
  - Child to parent
  - Parent to child on unmount
  - Sibling behavior is not defined
*/
function runEffectsMap<T extends NoneToVoidFunction>(map: Map<number, T>) {
  Array.from(map.entries())
    .sort(([aKey], [bKey]) => {
      const idA = Math.floor(aKey / MAX_EFFECT_CURSORS_PER_INSTANCE);
      const idB = Math.floor(bKey / MAX_EFFECT_CURSORS_PER_INSTANCE);

      const idDiff = idB - idA;
      if (idDiff !== 0) {
        return idDiff;
      }

      const cursorA = aKey % MAX_EFFECT_CURSORS_PER_INSTANCE;
      const cursorB = bKey % MAX_EFFECT_CURSORS_PER_INSTANCE;

      return cursorA - cursorB;
    }).forEach(([_, cb]) => void cb());
}

/*
  Order:
  - component effect cleanups
  - component effects
  - measure tasks
  - mutation tasks
  - component updates
  - component layout effect cleanups
  - component layout effects
  - forced layout measure tasks
  - forced layout mutation tasks
 */

const runUpdatePassOnRaf = throttleWith(requestMeasure, () => {
  if (getIsBlockingAnimating()) {
    getIsBlockingAnimating.once(runUpdatePassOnRaf);
    return;
  }

  const runImmediateEffects = captureImmediateEffects();

  idsToExcludeFromUpdate = new Set();
  const instancesToUpdate = Array
    .from(instancesPendingUpdate)
    .sort((a, b) => a.id - b.id);
  instancesPendingUpdate = new Set();

  const currentCleanups = pendingCleanups;
  pendingCleanups = new Map();
  runEffectsMap(currentCleanups);

  const currentEffects = pendingEffects;
  pendingEffects = new Map();
  runEffectsMap(currentEffects);

  requestMutation(() => {
    instancesToUpdate.forEach(prepareComponentForFrame);
    instancesToUpdate.forEach((instance) => {
      if (idsToExcludeFromUpdate.has(instance.id)) {
        return;
      }

      forceUpdateComponent(instance);
    });

    runImmediateEffects?.();
  });
});

export function captureImmediateEffects() {
  if (areImmediateEffectsCaptured) {
    return undefined;
  }

  areImmediateEffectsCaptured = true;
  return runCapturedImmediateEffects;
}

function runCapturedImmediateEffects() {
  const currentLayoutCleanups = pendingLayoutCleanups;
  pendingLayoutCleanups = new Map();
  runEffectsMap(currentLayoutCleanups);

  const currentLayoutEffects = pendingLayoutEffects;
  pendingLayoutEffects = new Map();
  runEffectsMap(currentLayoutEffects);

  areImmediateEffectsCaptured = false;
}

export function renderComponent(componentInstance: ComponentInstance) {
  idsToExcludeFromUpdate.add(componentInstance.id);

  const { Component, props } = componentInstance;
  let newRenderedValue: unknown;

  safeExec(() => {
    renderingInstance = componentInstance;
    if (componentInstance.hooks) {
      if (componentInstance.hooks.state) {
        componentInstance.hooks.state.cursor = 0;
      }
      if (componentInstance.hooks.effects) {
        componentInstance.hooks.effects.cursor = 0;
      }
      if (componentInstance.hooks.memos) {
        componentInstance.hooks.memos.cursor = 0;
      }
      if (componentInstance.hooks.refs) {
        componentInstance.hooks.refs.cursor = 0;
      }
    }

    let DEBUG_startAt: number | undefined;
    if (DEBUG) {
      const componentName = DEBUG_resolveComponentName(Component);
      if (!DEBUG_components[componentName]) {
        DEBUG_components[componentName] = {
          name: componentName,
          renders: 0,
          avgRenderTime: 0,
        };
      }

      if (DEBUG_MORE) {
        if (!DEBUG_SILENT_RENDERS_FOR.has(componentName)) {
          // eslint-disable-next-line no-console
          console.log(`[Teact] Render ${componentName}`);
        }
      }

      DEBUG_startAt = performance.now();
    }

    newRenderedValue = Component(props);

    if (DEBUG) {
      const duration = performance.now() - DEBUG_startAt!;
      const componentName = DEBUG_resolveComponentName(Component);
      if (duration > DEBUG_RENDER_THRESHOLD) {
        // eslint-disable-next-line no-console
        console.warn(`[Teact] Slow component render: ${componentName}, ${Math.round(duration)} ms`);
      }

      const { renders, avgRenderTime } = DEBUG_components[componentName];
      DEBUG_components[componentName].avgRenderTime = (avgRenderTime * renders + duration) / (renders + 1);
      DEBUG_components[componentName].renders++;
      DEBUG_components.TOTAL.renders++;

      if (DEBUG_MORE) {
        incrementOverlayCounter(`${componentName} renders`);
        incrementOverlayCounter(`${componentName} duration`, duration);
      }
    }
  }, {
    rescue: () => {
      // eslint-disable-next-line no-console
      console.error(`[Teact] Error while rendering component ${componentInstance.name}`, componentInstance);

      newRenderedValue = componentInstance.renderedValue;
    },
  });

  if (componentInstance.mountState === MountState.Mounted && newRenderedValue === componentInstance.renderedValue) {
    return componentInstance.$element;
  }

  componentInstance.renderedValue = newRenderedValue;
  const children = Array.isArray(newRenderedValue) ? newRenderedValue : [newRenderedValue];
  componentInstance.$element = buildComponentElement(componentInstance, children);

  return componentInstance.$element;
}

export function hasElementChanged($old: VirtualElement, $new: VirtualElement) {
  if (typeof $old !== typeof $new) {
    return true;
  } else if ($old.type !== $new.type) {
    return true;
  } else if ($old.type === VirtualType.Text && $new.type === VirtualType.Text) {
    return $old.value !== $new.value;
  } else if ($old.type === VirtualType.Tag && $new.type === VirtualType.Tag) {
    return ($old.tag !== $new.tag) || ($old.props.key !== $new.props.key);
  } else if ($old.type === VirtualType.Component && $new.type === VirtualType.Component) {
    return (
      $old.componentInstance.Component !== $new.componentInstance.Component
    ) || (
      $old.props.key !== $new.props.key
    );
  }

  return false;
}

export function mountComponent(componentInstance: ComponentInstance) {
  componentInstance.id = ++lastComponentId;
  componentInstance.mountState = MountState.Mounting;
  renderComponent(componentInstance);
  componentInstance.mountState = MountState.Mounted;
  return componentInstance.$element;
}

export function unmountComponent(componentInstance: ComponentInstance) {
  if (componentInstance.mountState === MountState.Unmounted) {
    return;
  }

  idsToExcludeFromUpdate.add(componentInstance.id);

  if (componentInstance.hooks?.effects) {
    for (const effect of componentInstance.hooks.effects.byCursor) {
      if (effect.cleanup) {
        safeExec(effect.cleanup);
      }

      effect.cleanup = undefined;
      effect.releaseSignals?.();
    }
  }

  componentInstance.mountState = MountState.Unmounted;

  helpGc(componentInstance);
}

// We need to remove all references to DOM objects. We also clean all other references, just in case
function helpGc(componentInstance: ComponentInstance) {
  const {
    effects, state, memos, refs,
  } = componentInstance.hooks || {};

  if (effects) {
    for (const hook of effects.byCursor) {
      hook.schedule = undefined;
      hook.cleanup = undefined;
      hook.releaseSignals = undefined;
      hook.dependencies = undefined;
    }
  }

  if (state) {
    for (const hook of state.byCursor) {
      hook.value = undefined;
      hook.nextValue = undefined;

      hook.setter = undefined as any;
    }
  }

  if (memos) {
    for (const hook of memos.byCursor) {
      hook.value = undefined;

      hook.dependencies = undefined as any;
    }
  }

  if (refs) {
    for (const hook of refs.byCursor) {
      hook.current = undefined;
      hook.onChange = undefined;
    }
  }

  componentInstance.hooks = undefined;

  componentInstance.$element = undefined as any;
  componentInstance.renderedValue = undefined;
  componentInstance.onUpdate = undefined;
}

function prepareComponentForFrame(componentInstance: ComponentInstance) {
  if (componentInstance.mountState === MountState.Unmounted) {
    return;
  }

  if (componentInstance.hooks?.state) {
    for (const hook of componentInstance.hooks.state.byCursor) {
      hook.value = hook.nextValue;
    }
  }
}

function forceUpdateComponent(componentInstance: ComponentInstance) {
  if (componentInstance.mountState === MountState.Unmounted || !componentInstance.onUpdate) {
    return;
  }

  const currentElement = componentInstance.$element;

  renderComponent(componentInstance);

  if (componentInstance.$element !== currentElement) {
    componentInstance.onUpdate();
  }
}

export function useState<T>(): [T | undefined, StateHookSetter<T | undefined>];
export function useState<T>(initial: T | (() => T), debugKey?: string): [T, StateHookSetter<T>];
export function useState<T>(initial?: T | (() => T), debugKey?: string): [T, StateHookSetter<T>] {
  if (!renderingInstance.hooks) {
    renderingInstance.hooks = {};
  }
  if (!renderingInstance.hooks.state) {
    renderingInstance.hooks.state = { cursor: 0, byCursor: [] };
  }

  const { cursor, byCursor } = renderingInstance.hooks.state;
  const componentInstance = renderingInstance;

  if (byCursor[cursor] === undefined) {
    const initValue = typeof initial === 'function' ? (initial as () => T)() : initial;
    byCursor[cursor] = {
      value: initValue,
      nextValue: initValue,
      setter: (newValue: ((current: T) => T) | T) => {
        if (componentInstance.mountState === MountState.Unmounted) {
          return;
        }

        if (typeof newValue === 'function') {
          newValue = (newValue as (current: T) => T)(byCursor[cursor].nextValue);
        }

        if (byCursor[cursor].nextValue === newValue) {
          return;
        }

        byCursor[cursor].nextValue = newValue;

        instancesPendingUpdate.add(componentInstance);
        runUpdatePassOnRaf();

        if (DEBUG_MORE) {
          // eslint-disable-next-line no-console
          console.log(
            '[Teact.useState]',
            DEBUG_resolveComponentName(componentInstance.Component),
            `State update at cursor #${cursor}${debugKey ? ` (${debugKey})` : ''}, next value: `,
            byCursor[cursor].nextValue,
          );
        }
      },
    };
  }

  renderingInstance.hooks.state.cursor++;

  return [
    byCursor[cursor].value,
    byCursor[cursor].setter,
  ];
}

function useEffectBase(
  isLayout: boolean,
  effect: Effect,
  dependencies?: readonly unknown[],
  debugKey?: string,
) {
  if (!renderingInstance.hooks) {
    renderingInstance.hooks = {};
  }

  if (!renderingInstance.hooks.effects) {
    renderingInstance.hooks.effects = { cursor: 0, byCursor: [] };
  }

  const { cursor, byCursor } = renderingInstance.hooks.effects;
  const effectConfig = byCursor[cursor];
  const componentInstance = renderingInstance;

  function schedule() {
    scheduleEffect(componentInstance, cursor, effect, isLayout);
  }

  if (dependencies && effectConfig?.dependencies) {
    if (dependencies.some((dependency, i) => dependency !== effectConfig.dependencies![i])) {
      if (DEBUG && debugKey) {
        const causedBy = dependencies.reduce<string[]>((res, newValue, i) => {
          const prevValue = effectConfig.dependencies![i];
          if (newValue !== prevValue) {
            res.push(`${i}: ${String(prevValue)} => ${String(newValue)}`);
          }

          return res;
        }, []);

        // eslint-disable-next-line no-console
        console.log(`[Teact] Effect "${debugKey}" caused by dependencies.`, causedBy.join(', '));
      }

      schedule();
    }
  } else {
    if (debugKey) {
      // eslint-disable-next-line no-console
      console.log(`[Teact] Effect "${debugKey}" caused by missing dependencies.`);
    }

    schedule();
  }

  function setupSignals() {
    const cleanups = dependencies?.filter(isSignal).map((signal, i) => signal.subscribe(() => {
      if (debugKey) {
        // eslint-disable-next-line no-console
        console.log(`[Teact] Effect "${debugKey}" caused by signal #${i} new value:`, signal());
      }

      byCursor[cursor].schedule!();
    }));

    if (!cleanups?.length) {
      return undefined;
    }

    return () => {
      for (const cleanup of cleanups) {
        cleanup();
      }
    };
  }

  if (effectConfig) effectConfig.schedule = undefined; // Help GC

  byCursor[cursor] = {
    ...effectConfig,
    dependencies,
    schedule,
  };

  if (!effectConfig) {
    byCursor[cursor].releaseSignals = setupSignals();
  }

  renderingInstance.hooks.effects.cursor++;

  if (DEBUG && cursor >= MAX_EFFECT_CURSORS_PER_INSTANCE) {
    // eslint-disable-next-line no-console
    console.warn(`[Teact] Effect cursor #${cursor} in ${componentInstance.name} is out of bounds. How?`);
  }
}

function scheduleEffect(
  componentInstance: ComponentInstance,
  cursor: number,
  effect: Effect,
  isLayout: boolean,
) {
  const { byCursor } = componentInstance.hooks!.effects!;
  const cleanup = byCursor[cursor]?.cleanup;
  const cleanupsContainer = isLayout ? pendingLayoutCleanups : pendingCleanups;
  const effectsContainer = isLayout ? pendingLayoutEffects : pendingEffects;
  const effectId = componentInstance.id * MAX_EFFECT_CURSORS_PER_INSTANCE + cursor;

  if (cleanup) {
    const runEffectCleanup = () => safeExec(() => {
      if (componentInstance.mountState === MountState.Unmounted) {
        return;
      }

      let DEBUG_startAt: number | undefined;
      if (DEBUG) {
        DEBUG_startAt = performance.now();
      }

      cleanup();

      if (DEBUG) {
        const duration = performance.now() - DEBUG_startAt!;
        const componentName = DEBUG_resolveComponentName(componentInstance.Component);
        if (duration > DEBUG_EFFECT_THRESHOLD) {
          // eslint-disable-next-line no-console
          console.warn(
            `[Teact] Slow cleanup at effect cursor #${cursor}: ${componentName}, ${Math.round(duration)} ms`,
          );
        }
      }
    }, {
      rescue: () => {
        // eslint-disable-next-line no-console
        console.error(`[Teact] Error in effect cleanup at cursor #${cursor} in ${componentInstance.name}`,
          componentInstance);
      },
      always: () => {
        byCursor[cursor].cleanup = undefined;
      },
    });

    cleanupsContainer.set(effectId, runEffectCleanup);
  }

  const runEffect = () => safeExec(() => {
    if (componentInstance.mountState === MountState.Unmounted) {
      return;
    }

    let DEBUG_startAt: number | undefined;
    if (DEBUG) {
      DEBUG_startAt = performance.now();
    }

    const result = effect();
    if (typeof result === 'function') {
      byCursor[cursor].cleanup = result;
    }

    if (DEBUG) {
      const duration = performance.now() - DEBUG_startAt!;
      const componentName = DEBUG_resolveComponentName(componentInstance.Component);
      if (duration > DEBUG_EFFECT_THRESHOLD) {
        // eslint-disable-next-line no-console
        console.warn(`[Teact] Slow effect at cursor #${cursor}: ${componentName}, ${Math.round(duration)} ms`);
      }
    }
  }, {
    rescue: () => {
      // eslint-disable-next-line no-console
      console.error(`[Teact] Error in effect at cursor #${cursor} in ${componentInstance.name}`, componentInstance);
    },
  });

  effectsContainer.set(effectId, runEffect);

  runUpdatePassOnRaf();
}

export function useEffect(effect: Effect, dependencies?: readonly unknown[], debugKey?: string) {
  return useEffectBase(false, effect, dependencies, debugKey);
}

export function useLayoutEffect(effect: Effect, dependencies?: readonly unknown[], debugKey?: string) {
  return useEffectBase(true, effect, dependencies, debugKey);
}

export function useUnmountCleanup(cleanup: NoneToVoidFunction) {
  if (!renderingInstance.hooks) {
    renderingInstance.hooks = {};
  }

  if (!renderingInstance.hooks.effects) {
    renderingInstance.hooks.effects = { cursor: 0, byCursor: [] };
  }

  const { cursor, byCursor } = renderingInstance.hooks.effects;

  if (!byCursor[cursor]) {
    byCursor[cursor] = {
      cleanup,
    };
  }

  renderingInstance.hooks.effects.cursor++;
}

export function useMemo<T>(
  resolver: () => T,
  dependencies: readonly unknown[],
  debugKey?: string,
  debugHitRateKey?: string,
): T {
  if (!renderingInstance.hooks) {
    renderingInstance.hooks = {};
  }
  if (!renderingInstance.hooks.memos) {
    renderingInstance.hooks.memos = { cursor: 0, byCursor: [] };
  }

  const { cursor, byCursor } = renderingInstance.hooks.memos;
  let { value } = byCursor[cursor] || {};

  let DEBUG_state: typeof DEBUG_memos[string] | undefined;
  if (DEBUG && debugHitRateKey) {
    const instanceKey = `${debugHitRateKey}#${renderingInstance.id}`;

    DEBUG_state = DEBUG_memos[instanceKey];
    if (!DEBUG_state) {
      DEBUG_state = {
        key: instanceKey, calls: 0, misses: 0, hitRate: 0,
      };
      DEBUG_memos[instanceKey] = DEBUG_state;
    }

    DEBUG_state.calls++;
    DEBUG_state.hitRate = (DEBUG_state.calls - DEBUG_state.misses) / DEBUG_state.calls;
  }

  if (
    byCursor[cursor] === undefined
    || dependencies.length !== byCursor[cursor].dependencies.length
    || dependencies.some((dependency, i) => dependency !== byCursor[cursor].dependencies[i])
  ) {
    if (DEBUG) {
      if (debugKey) {
        const msg = `[Teact.useMemo] ${renderingInstance.name} (${debugKey}): Update is caused by:`;
        if (!byCursor[cursor]) {
          // eslint-disable-next-line no-console
          console.log(`${msg} [first render]`);
        } else {
          logUnequalProps(byCursor[cursor].dependencies, dependencies, msg, debugKey);
        }
      }

      if (DEBUG_state) {
        DEBUG_state.misses++;
        DEBUG_state.hitRate = (DEBUG_state.calls - DEBUG_state.misses) / DEBUG_state.calls;

        if (
          DEBUG_state.calls % 10 === 0
          && DEBUG_state.calls >= DEBUG_MEMOS_CALLS_THRESHOLD
          && DEBUG_state.hitRate < 0.25
        ) {
          // eslint-disable-next-line no-console
          console.warn(

            `[Teact] ${DEBUG_state.key}: Hit rate is ${DEBUG_state.hitRate.toFixed(2)} for ${DEBUG_state.calls} calls`,
          );
        }
      }
    }

    value = resolver();
  }

  byCursor[cursor] = {
    value,
    dependencies,
  };

  renderingInstance.hooks.memos.cursor++;

  return value;
}

export function useCallback<F extends AnyFunction>(newCallback: F, dependencies: unknown[], debugKey?: string): F {
  // eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
  return useMemo(() => newCallback, dependencies, debugKey);
}

export function useRef<T>(initial: T): RefObject<T>;
export function useRef<T>(): RefObject<T | undefined>; // TT way (empty is `undefined`)
export function useRef<T>(initial?: T) {
  if (!renderingInstance.hooks) {
    renderingInstance.hooks = {};
  }
  if (!renderingInstance.hooks.refs) {
    renderingInstance.hooks.refs = { cursor: 0, byCursor: [] };
  }

  const { cursor, byCursor } = renderingInstance.hooks.refs;
  if (!byCursor[cursor]) {
    byCursor[cursor] = {
      current: initial,
    };
  }

  renderingInstance.hooks.refs.cursor++;

  return byCursor[cursor];
}

export function createContext<T>(defaultValue?: T): Context<T> {
  const contextId = String(contextCounter++);

  function TeactContextProvider(props: { value: T; children: TeactNode }) {
    const [getValue, setValue] = useSignal(props.value ?? defaultValue);
    // Create a new object to avoid mutations in the parent context
    renderingInstance.context = { ...renderingInstance.context };

    renderingInstance.context[contextId] = getValue;
    setValue(props.value);
    return props.children;
  }

  TeactContextProvider.DEBUG_contentComponentName = contextId;

  const context = {
    defaultValue,
    contextId,
    Provider: TeactContextProvider,
  };

  return context;
}

export function useContextSignal<T>(context: Context<T>) {
  const [getDefaultValue] = useSignal(context.defaultValue);

  return renderingInstance.context?.[context.contextId] as Signal<T> || getDefaultValue;
}

export function useSignal<T>(initial?: T) {
  const signalRef = useRef<ReturnType<typeof createSignal<T>>>();
  signalRef.current ??= createSignal<T>(initial);
  return signalRef.current;
}

export function memo<T extends FC_withDebug>(Component: T, debugKey?: string) {
  function TeactMemoWrapper(props: Props) {
    return useMemo(
      () => createElement(Component, props),
      // eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
      Object.values(props),
      debugKey,
      DEBUG_MORE ? DEBUG_resolveComponentName(renderingInstance.Component) : undefined,
    );
  }

  TeactMemoWrapper.DEBUG_contentComponentName = DEBUG_resolveComponentName(Component);

  return TeactMemoWrapper as T;
}

export function DEBUG_resolveComponentName(Component: FC_withDebug) {
  const { name, DEBUG_contentComponentName } = Component;

  if (name === 'TeactNContainer') {
    return `container>${DEBUG_contentComponentName}`;
  }

  if (name === 'TeactMemoWrapper') {
    return `memo>${DEBUG_contentComponentName}`;
  }

  if (name === 'TeactContextProvider') {
    return `context>id${DEBUG_contentComponentName}`;
  }

  return name + (DEBUG_contentComponentName ? `>${DEBUG_contentComponentName}` : '');
}

export default {
  createElement,
  cloneElement,
  Fragment,
};

export { createElement, cloneElement, Fragment };
