import { chain, arrayEquals } from "@solid-primitives/utils";
import {
  type Accessor,
  children,
  createComputed,
  createMemo,
  type JSX,
  onCleanup,
  untrack,
} from "solid-js";
import { isServer } from "solid-js/web";

// TODO delete in next major version
export type { ResolvedChildren, ResolvedJSXElement } from "solid-js/types/reactive/signal.js";

/**
 * Type for the `ref` prop
 */
export type Ref<T> = T | ((el: T) => void) | undefined;

/**
 * Component properties with types for `ref` prop
 * ```ts
 * {
 *    ref?: T | ((el: T) => void);
 * }
 * ```
 */
export interface RefProps<T> {
  ref?: Ref<T>;
}

/**
 * Utility for chaining multiple `ref` assignments with `props.ref` forwarding.
 * @param refs list of ref setters. Can be a `props.ref` prop for ref forwarding or a setter to a local variable (`el => ref = el`).
 * @example
 * ```tsx
 * interface ButtonProps {
 *    ref?: Ref<HTMLButtonElement>
 * }
 * function Button (props: ButtonProps) {
 *    let ref: HTMLButtonElement | undefined
 *    onMount(() => {
 *        // use the local ref
 *    })
 *    return <button ref={mergeRefs(props.ref, el => ref = el)} />
 * }
 *
 * // in consumer's component:
 * let ref: HTMLButtonElement | undefined
 * <Button ref={ref} />
 * ```
 */
export function mergeRefs<T>(...refs: Ref<T>[]): (el: T) => void {
  return chain(refs as ((el: T) => void)[]);
}

/**
 * Default predicate used in `resolveElements()` and `resolveFirst()` to filter Elements.
 *
 * On the client it uses `instanceof Element` check, on the server it checks for the object with `t` property. (generated by compiling JSX)
 */
export const defaultElementPredicate: (item: JSX.Element | Element) => item is Element = isServer
  ? (item): item is Element => item != null && typeof item === "object" && "t" in item
  : (item): item is Element => item instanceof Element;

/**
 * Utility for resolving recursively nested JSX children to a single element or an array of elements using a predicate.
 *
 * It does **not** create a computation - should be wrapped in one to repeat the resolution on changes.
 *
 * @param value JSX children
 * @param predicate predicate to filter elements
 * @returns single element or an array of elements or `null` if no elements were found
 */
export function getResolvedElements<T extends object>(
  value: JSX.Element,
  predicate: (item: JSX.Element | T) => item is T,
): T | T[] | null {
  if (predicate(value)) return value;
  if (typeof value === "function" && !(value as () => JSX.Element).length)
    return getResolvedElements((value as () => JSX.Element)(), predicate);
  if (Array.isArray(value)) {
    const results: T[] = [];
    for (const item of value) {
      const result = getResolvedElements(item, predicate);
      if (result)
        Array.isArray(result) ? results.push.apply(results, result) : results.push(result);
    }
    return results.length ? results : null;
  }
  return null;
}

export type ResolveChildrenReturn<T extends object> = Accessor<T | T[] | null> & {
  toArray: () => T[];
};

/**
 * Utility for resolving recursively nested JSX children to a single element or an array of elements using a predicate.
 *
 * @param fn Accessor of JSX children
 * @param predicate predicate to filter elements.
 * ```ts
 * // default predicate
 * (item: JSX.Element): item is Element => item instanceof Element
 * ```
 * @param serverPredicate predicate to filter elements on server. {@link defaultElementPredicate}
 * @returns Signal of a single element or an array of elements or `null` if no elements were found
 * ```ts
 * Accessor<T | T[] | null> & { toArray: () => T[] }
 * ```
 * @example
 * ```tsx
 * function Button(props: ParentProps) {
 *   const children = resolveElements(() => props.children)
 *   return <For each={children.toArray()}>
 *    {child => <div>{child.localName}: {child}</div>}
 *  </For>
 * }
 * ```
 */
export function resolveElements(fn: Accessor<JSX.Element>): ResolveChildrenReturn<Element>;
export function resolveElements<T extends object & JSX.Element>(
  fn: Accessor<JSX.Element>,
  predicate: (item: JSX.Element) => item is T,
  serverPredicate?: (item: JSX.Element) => item is T,
): ResolveChildrenReturn<T>;
export function resolveElements<T extends object>(
  fn: Accessor<JSX.Element>,
  predicate: (item: JSX.Element | T) => item is T,
  serverPredicate?: (item: JSX.Element | T) => item is T,
): ResolveChildrenReturn<T>;
export function resolveElements(
  fn: Accessor<JSX.Element>,
  predicate = defaultElementPredicate,
  serverPredicate = defaultElementPredicate,
): ResolveChildrenReturn<Element> {
  const children = createMemo(fn);
  const memo = createMemo(() =>
    getResolvedElements(children(), isServer ? serverPredicate : predicate),
  ) as ResolveChildrenReturn<Element>;
  memo.toArray = () => {
    const value = memo();
    return Array.isArray(value) ? value : value ? [value] : [];
  };
  return memo;
}

/**
 * Utility for resolving recursively nested JSX children in search of the first element that matches a predicate.
 *
 * It does **not** create a computation - should be wrapped in one to repeat the resolution on changes.
 *
 * @param value JSX children
 * @param predicate predicate to filter elements
 * @returns single found element or `null` if no elements were found
 */
export function getFirstChild<T extends object>(
  value: JSX.Element,
  predicate: (item: JSX.Element | T) => item is T,
): T | null {
  if (predicate(value)) return value;
  if (typeof value === "function" && !(value as () => JSX.Element).length)
    return getFirstChild((value as () => JSX.Element)(), predicate);
  if (Array.isArray(value)) {
    for (const item of value) {
      const result = getFirstChild(item, predicate);
      if (result) return result;
    }
  }
  return null;
}

/**
 * Utility for resolving recursively nested JSX children in search of the first element that matches a predicate.
 * @param fn Accessor of JSX children
 * @param predicate predicate to filter elements.
 * ```ts
 * // default predicate
 * (item: JSX.Element): item is Element => item instanceof Element
 * ```
 * @param serverPredicate predicate to filter elements on server. {@link defaultElementPredicate}
 * @returns Signal of a single found element or `null` if no elements were found
 * ```ts
 * Accessor<T | null>
 * ```
 * @example
 * ```tsx
 * function Button(props: ParentProps) {
 *  const child = resolveFirst(() => props.children)
 *  return <div>{child()?.localName}: {child()}</div>
 * }
 * ```
 */
export function resolveFirst(fn: Accessor<JSX.Element>): Accessor<Element | null>;
export function resolveFirst<T extends object & JSX.Element>(
  fn: Accessor<JSX.Element>,
  predicate: (item: JSX.Element) => item is T,
  serverPredicate?: (item: JSX.Element) => item is T,
): Accessor<T | null>;
export function resolveFirst<T extends object>(
  fn: Accessor<JSX.Element>,
  predicate: (item: JSX.Element | T) => item is T,
  serverPredicate?: (item: JSX.Element | T) => item is T,
): Accessor<T | null>;
export function resolveFirst(
  fn: Accessor<JSX.Element>,
  predicate = defaultElementPredicate,
  serverPredicate = defaultElementPredicate,
): Accessor<any | null> {
  const children = createMemo(fn);
  return createMemo(() => getFirstChild(children(), isServer ? serverPredicate : predicate));
}

/**
 * Get up-to-date references of the multiple children elements.
 * @param ref Getter of current array of elements
 * @see https://github.com/solidjs-community/solid-primitives/tree/main/packages/refs#Refs
 * @example
 * ```tsx
 * const [refs, setRefs] = createSignal<Element[]>([]);
 * <Refs ref={setRefs}>
 *   {props.children}
 * </Refs>
 * ```
 */
export function Refs(props: { ref: Ref<Element[]>; children: JSX.Element }): JSX.Element {
  if (isServer) {
    return props.children;
  }

  const cb = props.ref as (els: Element[]) => void,
    resolved = children(() => props.children);

  let prev: Element[] = [];

  createComputed(() => {
    const els = resolved.toArray().filter(defaultElementPredicate);
    if (!arrayEquals(prev, els)) untrack(() => cb(els));
    prev = els;
  }, []);
  onCleanup(() => prev.length && cb([]));

  return resolved as unknown as JSX.Element;
}

/**
 * Get up-to-date reference to a single child element.
 * @param ref Getter of current element *(or `undefined` if not mounted)*
 * @see https://github.com/solidjs-community/solid-primitives/tree/main/packages/refs#Ref
 * @example
 * ```tsx
 * const [ref, setRef] = createSignal<Element | undefined>();
 * <Ref ref={setRef}>
 *   {props.children}
 * </Ref>
 * ```
 */
export function Ref(props: { ref: Ref<Element | undefined>; children: JSX.Element }): JSX.Element {
  if (isServer) {
    return props.children;
  }

  const cb = props.ref as (el: Element | undefined) => void,
    resolved = children(() => props.children);

  let prev: Element | undefined;

  createComputed(() => {
    const el = resolved.toArray().find(defaultElementPredicate);
    if (el !== prev) untrack(() => cb(el));
    prev = el;
  });

  onCleanup(() => prev && cb(undefined));

  return resolved as unknown as JSX.Element;
}
