import * as flexui from '@openflexui/core'
import { type utils as typeUtils } from './types'
import { type ComponentType, type GeneralBehavior, type TraitBehavior } from './behavior'
import { SelectorQuery } from './selector_query'
import { IntersectionObserver } from './intersection'
import { ResizeObserver } from './resize'
import { MediaQueryObserver } from './media_query'

type DataList = typeUtils.DataList
type PropertyList = typeUtils.PropertyList
type MethodList = typeUtils.MethodList
export type Empty = typeUtils.Empty
export type PropertyValues<TProperty extends PropertyList> = typeUtils.PropertyValues<TProperty>
export type AllData<
  TData extends DataList,
  TProperty extends PropertyList,
> = typeUtils.DataWithPropertyValues<TData, TProperty>

type ExportType<
  UData extends DataList,
  UProperty extends PropertyList,
  UMethod extends MethodList,
  UComponentExport,
  UExtraThisFields extends DataList,
> = [UComponentExport] extends [never]
  ? Component<UData, UProperty, UMethod, UComponentExport, UExtraThisFields>
  : UComponentExport

const filterComponentExportWithType = <
  UData extends DataList,
  UProperty extends PropertyList,
  UMethod extends MethodList,
  UComponentExport,
  UExtraThisFields extends DataList,
>(
  source: GeneralComponentCaller,
  elem: flexui.Element,
  componentType: ComponentType<UData, UProperty, UMethod, UComponentExport, UExtraThisFields>,
): ExportType<UData, UProperty, UMethod, UComponentExport, UExtraThisFields> | undefined => {
  const comp = elem.asInstanceOf(componentType._$)
  if (comp === null) return undefined
  const selectedSpace = comp.getRootBehavior().ownerSpace
  const uncheckedCaller = comp.getMethodCaller()
  const caller = uncheckedCaller instanceof ComponentCaller ? uncheckedCaller : null
  const sourceSpace = source._$.getRootBehavior().ownerSpace
  const defaultResult = selectedSpace !== sourceSpace ? null : caller
  const ret = caller?._$export?.(selectedSpace !== sourceSpace ? null : source) as unknown
  if (ret === undefined) {
    // eslint-disable-next-line @typescript-eslint/no-unsafe-return
    return defaultResult as any
  }
  // eslint-disable-next-line @typescript-eslint/no-unsafe-return
  return ret as any
}

const filterComponentExport = (
  source: ComponentCaller<any, any, any, any>,
  elem: flexui.Element,
): any => {
  if (flexui.Component.isComponent(elem)) {
    const selectedSpace = elem.getRootBehavior().ownerSpace
    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
    const uncheckedCaller = elem.getMethodCaller()
    const caller = uncheckedCaller instanceof ComponentCaller ? uncheckedCaller : null
    const sourceSpace = source._$.getRootBehavior().ownerSpace
    const defaultResult = selectedSpace !== sourceSpace ? null : caller
    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
    const ret = caller?._$export?.(selectedSpace !== sourceSpace ? null : source)
    if (ret === undefined) {
      return defaultResult as any
    }
    return ret
  }
  return undefined
}

export const determineComponentExports = (
  parent: GeneralBehavior[],
  selfExport: ((source: GeneralComponent | null) => any) | undefined,
) => {
  if (selfExport !== undefined) return selfExport
  for (let i = parent.length - 1; i >= 0; i -= 1) {
    const behavior = parent[i]!
    if (behavior._$export !== undefined) return behavior._$export
  }
  return undefined
}

export type GeneralComponent = Component<
  /* TData */ Record<string, any>,
  /* TProperty */ Record<string, any>,
  /* TMethod */ Record<string, any>,
  /* TComponentExport */ any,
  /* TExtraThisFields */ Record<string, any>
>

export type Component<
  TData extends DataList,
  TProperty extends PropertyList,
  TMethod extends MethodList,
  TComponentExport,
  TExtraThisFields extends DataList,
> = ComponentCaller<TData, TProperty, TMethod, TComponentExport> & {
  [k in keyof TMethod]: TMethod[k]
} & TExtraThisFields

type GeneralComponentCaller = ComponentCaller<
  /* TData */ Record<string, any>,
  /* TProperty */ Record<string, any>,
  /* TMethod */ Record<string, any>,
  /* TComponentExport */ any
>

export class ComponentCaller<
  TData extends DataList,
  TProperty extends PropertyList,
  TMethod extends MethodList,
  TComponentExport,
> {
  /** The corresponding `Component` */
  _$!: flexui.Component<TData, TProperty, TMethod>
  /** @internal */
  _$export?: (source: GeneralComponent | null) => TComponentExport

  /** The component path in the code space */
  get is(): string {
    return this._$.is
  }
  set is(value: string) {
    Object.defineProperty(this, 'is', {
      value,
      writable: true,
      enumerable: true,
      configurable: true,
    })
  }

  /** The `id` field in the template */
  get id(): string {
    return this._$.id
  }
  set id(value: string) {
    Object.defineProperty(this, 'id', {
      value,
      writable: true,
      enumerable: true,
      configurable: true,
    })
  }

  /** The `data-*` field in the template */
  get dataset(): { [key: string]: any } | null {
    return this._$.dataset
  }
  set dataset(value: { [key: string]: any } | null) {
    Object.defineProperty(this, 'dataset', {
      value,
      writable: true,
      enumerable: true,
      configurable: true,
    })
  }

  /** The component data and property values */
  get data(): typeUtils.Merge<AllData<TData, TProperty>> {
    return this._$.data
  }
  set data(value: AllData<TData, TProperty>) {
    Object.defineProperty(this, 'data', {
      value,
      writable: true,
      enumerable: true,
      configurable: true,
    })
  }

  /** The component data and property values (same as `data` ) */
  get properties(): typeUtils.Merge<AllData<TData, TProperty>> {
    return this._$.data
  }
  set properties(value: AllData<TData, TProperty>) {
    Object.defineProperty(this, 'properties', {
      value,
      writable: true,
      enumerable: true,
      configurable: true,
    })
  }

  /**
   * Group several data update calls
   *
   * This method is designed for hinting the backend that some updates should be handled together.
   * However, this is done automatically now,
   * so this method is just for backward compatibilities.
   */
  // eslint-disable-next-line class-methods-use-this
  groupSetData(callback: () => void) {
    flexui.safeCallback('GroupSetData Callback', callback, this, [], this._$)
  }

  /**
   * Do a classic data update
   *
   * The `callback` is called after the update applies in the backend.
   * In most cases, you SHOULD NOT wait the backend update (that might be very slow),
   * and most calls, including read calls such as `selectComponent` , simply works immediately.
   * However, when called inside observers,
   * the data update will not be applied to templates immediately
   * (it is recommanded to use `updateData` instead in observers).
   */
  setData(
    newData: Partial<typeUtils.SetDataSetter<typeUtils.DataWithPropertyValues<TData, TProperty>>>,
    callback?: () => void,
  ): void
  setData(data: Record<string, any>, callback?: () => void) {
    this._$.setData(data as any)
    if (callback) {
      flexui.triggerRender(this._$, () => {
        flexui.safeCallback('SetData Callback', callback, this, [], this._$)
      })
    }
  }

  /**
   * Schedule a classic data updates
   *
   * The data update will not be applied until next `setData` or `applyDataUpdates` call.
   * When called inside observers, the data update will be applied when observer ends.
   * All data observers will not be triggered immediately before applied.
   * Reads of the data will get the unchanged value before applied.
   */
  updateData(
    newData: Partial<typeUtils.SetDataSetter<typeUtils.DataWithPropertyValues<TData, TProperty>>>,
  ): void
  updateData(newData: Record<string, any>): void {
    this._$.updateData(newData as any)
  }

  /**
   * Schedule a data update on a single specified path
   *
   * The data update will not be applied until next `setData` or `applyDataUpdates` call.
   * All data observers will not be triggered immediately before applied.
   * Reads of the data will get the unchanged value before applied.
   */
  replaceDataOnPath<T extends (string | number)[]>(
    path: readonly [...T],
    data: typeUtils.GetFromDataPath<typeUtils.DataWithPropertyValues<TData, TProperty>, T>,
  ): void
  replaceDataOnPath(path: any, data: any) {
    this._$.replaceDataOnPath(path, data)
  }

  /**
   * Schedule an array update
   *
   * The behavior is like `Array.prototype.slice` .
   * Break the array before the `index`-th item, delete `del` items, and insert some items here.
   * If `index` is undefined, negative, or larger than the length of the array,
   * no items will be deleted and new items will be appended to the end of the array.
   * The data update will not be applied until next `setData` or `applyDataUpdates` call.
   * All data observers will not be triggered immediately before applied.
   * Reads of the data will get the unchanged value before applied.
   */
  spliceArrayDataOnPath<T extends (string | number)[]>(
    path: readonly [...T],
    index: typeUtils.GetFromDataPath<
      typeUtils.DataWithPropertyValues<TData, TProperty>,
      T
    > extends any[]
      ? number | undefined
      : never,
    del: typeUtils.GetFromDataPath<
      typeUtils.DataWithPropertyValues<TData, TProperty>,
      T
    > extends any[]
      ? number | undefined
      : never,
    inserts: typeUtils.GetFromDataPath<
      typeUtils.DataWithPropertyValues<TData, TProperty>,
      T
    > extends (infer I)[]
      ? I[]
      : never,
  ): void
  spliceArrayDataOnPath(
    path: (string | number)[],
    index: number | undefined,
    del: number | undefined,
    inserts: unknown[],
  ) {
    this._$.spliceArrayDataOnPath(path, index as any, del as any, inserts as any)
  }

  /**
   * Apply all scheduled updates immediately
   *
   * Inside observers, it is generally not .
   */
  applyDataUpdates() {
    this._$.applyDataUpdates()
  }

  /**
   * Pending all data updates in the callback, and apply updates after callback returns
   *
   * This function helps grouping several `replaceDataOnPath` or `spliceArrayDataOnPath` calls,
   * and then apply them at the end of the callback.
   * `setData` and `applyDataUpdates` calls inside the callback still apply updates immediately.
   */
  groupUpdates<T>(callback: () => T): T | undefined {
    return this._$.groupUpdates(() =>
      flexui.safeCallback('GroupUpdates Callback', callback, this, [], this._$),
    )
  }

  /**
   * Check whether the `other` behavior is a dependent behavior or a implemented trait behavior
   */
  hasBehavior(behavior: GeneralBehavior | TraitBehavior<any, any>) {
    return this._$.hasBehavior(behavior._$)
  }

  /**
   * Get the trait behavior implementation of the component
   *
   * Returns `undefined` if the specified trait behavior is not implemented.
   */
  traitBehavior<TOut extends { [key: string]: any }>(
    traitBehavior: TraitBehavior<any, TOut>,
  ): TOut | undefined {
    return this._$.traitBehavior(traitBehavior._$)
  }

  /** Trigger an event */
  triggerEvent(
    name: string,
    detail?: any,
    options?: {
      bubbles?: boolean
      composed?: boolean
      capturePhase?: boolean
    },
  ) {
    return this._$.triggerEvent(name, detail, options)
  }

  /** Create a selector query for searching element inside the component */
  createSelectorQuery(): SelectorQuery {
    return new SelectorQuery(this)
  }

  /** Create an intersection observer */
  createIntersectionObserver(options?: {
    thresholds?: number[]
    initialRatio?: number
    observeAll?: boolean
  }): IntersectionObserver {
    return new IntersectionObserver(
      this,
      options?.thresholds || [0],
      options?.initialRatio || 0,
      options?.observeAll || false,
    )
  }

  /** Create a resize observer */
  createResizeObserver(options?: { observeAll?: boolean }): ResizeObserver {
    return new ResizeObserver(this, !!options?.observeAll)
  }

  /** Create an media query observer */
  createMediaQueryObserver(): MediaQueryObserver {
    return new MediaQueryObserver(this)
  }

  // TODO support animate / applyAnimation / clearAnimation
  // TODO support setUpdatePerformanceListener

  /**
   * Query an element inside the component
   *
   * If `componentType` is provided, this method will check the selected component type.
   * If the component type does not match, `null` is returned.
   */
  selectComponent(selector: string): any
  selectComponent<
    UData extends DataList,
    UProperty extends PropertyList,
    UMethod extends MethodList,
    UComponentExport,
    UExtraThisFields extends DataList,
  >(
    selector: string,
    componentType: ComponentType<UData, UProperty, UMethod, UComponentExport, UExtraThisFields>,
  ): ExportType<UData, UProperty, UMethod, UComponentExport, UExtraThisFields> | null
  selectComponent<
    UData extends DataList,
    UProperty extends PropertyList,
    UMethod extends MethodList,
    UComponentExport,
    UExtraThisFields extends DataList,
  >(
    selector: string,
    componentType?: ComponentType<UData, UProperty, UMethod, UComponentExport, UExtraThisFields>,
  ): ExportType<UData, UProperty, UMethod, UComponentExport, UExtraThisFields> | null {
    const target = this._$.getShadowRoot()!.querySelector(selector)
    if (target === null) return null
    return componentType
      ? filterComponentExportWithType(this, target, componentType) ?? null
      : filterComponentExport(this, target) ?? null
  }

  /**
   * Query all elements inside the component
   *
   * If `componentType` is provided, this method will check the selected component type.
   * If a component type does not match, it is not returned.
   */
  selectAllComponents(selector: string): GeneralComponent[]
  selectAllComponents<
    UData extends DataList,
    UProperty extends PropertyList,
    UMethod extends MethodList,
    UComponentExport,
    UExtraThisFields extends DataList,
  >(
    selector: string,
    componentType: ComponentType<UData, UProperty, UMethod, UComponentExport, UExtraThisFields>,
  ): ExportType<UData, UProperty, UMethod, UComponentExport, UExtraThisFields>[]
  selectAllComponents<
    UData extends DataList,
    UProperty extends PropertyList,
    UMethod extends MethodList,
    UComponentExport,
    UExtraThisFields extends DataList,
  >(
    selector: string,
    componentType?: ComponentType<UData, UProperty, UMethod, UComponentExport, UExtraThisFields>,
  ): ExportType<UData, UProperty, UMethod, UComponentExport, UExtraThisFields>[] {
    const targets = this._$.getShadowRoot()!.querySelectorAll(selector)
    const ret = [] as ExportType<UData, UProperty, UMethod, UComponentExport, UExtraThisFields>[]
    targets.forEach((target) => {
      // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
      const r = componentType
        ? filterComponentExportWithType(this, target, componentType)
        : filterComponentExport(this, target)
      if (r !== undefined) ret.push(r)
    })
    return ret
  }

  /**
   * Get the owner component
   *
   * If `componentType` is provided, this method will check the selected component type.
   * If a component type does not match, `null` is returned.
   */
  selectOwnerComponent(): any
  selectOwnerComponent<
    UData extends DataList,
    UProperty extends PropertyList,
    UMethod extends MethodList,
    UComponentExport,
    UExtraThisFields extends DataList,
  >(
    componentType: ComponentType<UData, UProperty, UMethod, UComponentExport, UExtraThisFields>,
  ): ExportType<UData, UProperty, UMethod, UComponentExport, UExtraThisFields> | null
  selectOwnerComponent<
    UData extends DataList,
    UProperty extends PropertyList,
    UMethod extends MethodList,
    UComponentExport,
    UExtraThisFields extends DataList,
  >(
    componentType?: ComponentType<UData, UProperty, UMethod, UComponentExport, UExtraThisFields>,
  ): ExportType<UData, UProperty, UMethod, UComponentExport, UExtraThisFields> | null {
    const target = this._$.ownerShadowRoot?.getHostNode()
    if (target === undefined) return null
    return componentType
      ? filterComponentExportWithType(this, target, componentType) ?? null
      : filterComponentExport(this, target) ?? null
  }

  getRelationNodes(relationKey: string): GeneralComponent[] {
    return this._$.getRelationNodes(relationKey).map(
      (target) => target.getMethodCaller() as any as GeneralComponent,
    )
  }

  /** Cast the component into a general-typed one */
  general(): GeneralComponent {
    return this
  }

  /**
   * Cast the component into the specified type
   *
   * Returns `null` if the component node is not the instance of the specified component.
   */
  asInstanceOf<
    UData extends DataList,
    UProperty extends PropertyList,
    UMethod extends MethodList,
    UComponentExport,
    UExtraThisFields extends DataList,
  >(
    componentType: ComponentType<UData, UProperty, UMethod, UComponentExport, UExtraThisFields>,
  ): Component<UData, UProperty, UMethod, UComponentExport, UExtraThisFields> | null {
    const inner = this._$.asInstanceOf(componentType._$)
    if (!inner) return null
    return this as unknown as Component<
      UData,
      UProperty,
      UMethod,
      UComponentExport,
      UExtraThisFields
    >
  }
}

export class ComponentProto<
  TData extends DataList,
  TProperty extends PropertyList,
  TMethod extends MethodList,
  TComponentExport,
  TExtraThisFields extends DataList,
> {
  private proto: Component<TData, TProperty, TMethod, TComponentExport, TExtraThisFields>

  constructor(
    methods: TMethod,
    parents: GeneralBehavior[],
    componentExport?: (source: GeneralComponent | null) => TComponentExport,
  ) {
    this.proto = Object.create(ComponentCaller.prototype) as Component<
      TData,
      TProperty,
      TMethod,
      TComponentExport,
      TExtraThisFields
    >
    Object.assign(this.proto, methods)
    this.proto._$export = determineComponentExports(parents, componentExport)
  }

  derive(): Component<TData, TProperty, TMethod, TComponentExport, TExtraThisFields> {
    return Object.create(this.proto) as Component<
      TData,
      TProperty,
      TMethod,
      TComponentExport,
      TExtraThisFields
    >
  }
}