import * as flexui from '@openflexui/core'
import type { GeneralComponent } from './component'

/**
 * Capture native setTimeout at module load (before entry_guard protect).
 * Runtime global setTimeout is timer-policy wrapped and denies business timers;
 * selector_query is framework code and must not go through that gate.
 */
const engineSetTimeout: typeof setTimeout = globalThis.setTimeout

export type SelectorQueryFields = {
  id?: boolean
  dataset?: boolean
  mark?: boolean
  rect?: boolean
  size?: boolean
  scrollOffset?: boolean
  properties?: string[]
  context?: boolean
  // TODO support computedStyle
}

export type SelectorQueryResult<T extends SelectorQueryFields> = {
  id: T['id'] extends true ? string : undefined
  dataset: T['dataset'] extends true ? { [k: string]: any } : undefined
  mark: T['mark'] extends true ? { [k: string]: any } : undefined
  left: T['rect'] extends true ? number : undefined
  top: T['rect'] extends true ? number : undefined
  right: T['rect'] extends true ? number : undefined
  bottom: T['rect'] extends true ? number : undefined
  width: T['size'] extends true ? number : undefined
  height: T['size'] extends true ? number : undefined
  scrollLeft: T['scrollOffset'] extends true ? number : undefined
  scrollTop: T['scrollOffset'] extends true ? number : undefined
  scrollWidth: T['scrollOffset'] extends true ? number : undefined
  scrollHeight: T['scrollOffset'] extends true ? number : undefined
} & {
  [K in T['properties'] extends (infer I extends string)[] ? I : never]: any
}

export type BoundingClientRect = {
  id: string
  dataset: { [k: string]: any }
  left: number
  top: number
  right: number
  bottom: number
  width: number
  height: number
}

export type ScrollOffset = {
  id: string
  dataset: { [k: string]: any }
  scrollLeft: number
  scrollTop: number
  scrollWidth: number
  scrollHeight: number
}

export type ContextResult = {
  context: unknown
}

const joinAsync = <T>(inits: ((cb: (ret: T) => void) => void)[], cb: (rets: T[]) => void) => {
  let initDone = false
  let jobCount = inits.length
  const rets = new Array<T>(jobCount)
  inits.forEach((init, index) => {
    init((ret) => {
      rets[index] = ret
      jobCount -= 1
      if (jobCount === 0 && initDone) {
        cb(rets)
      }
    })
  })
  initDone = true
  if (jobCount === 0) {
    engineSetTimeout(() => {
      cb(rets)
    }, 0)
  }
}

class NodesRef<S extends boolean> {
  private _$sq: SelectorQuery
  private _$comp: GeneralComponent
  private _$sel: string | null
  private _$single: S

  constructor(
    selectorQuery: SelectorQuery,
    component: GeneralComponent,
    selector: string | null,
    selectSingle: S,
  ) {
    this._$sq = selectorQuery
    this._$comp = component
    this._$sel = selector
    this._$single = selectSingle
  }

  fields<T extends SelectorQueryFields>(
    fields: T,
    cb: (res: S extends true ? SelectorQueryResult<T> : SelectorQueryResult<T>[]) => void = () => {
      /* empty */
    },
  ) {
    this._$sq._$push(this._$sel, this._$comp, this._$single, fields, cb)
    return this._$sq
  }

  boundingClientRect(
    cb: (res: S extends true ? BoundingClientRect : BoundingClientRect[]) => void = () => {
      /* empty */
    },
  ) {
    this._$sq._$push(
      this._$sel,
      this._$comp,
      this._$single,
      {
        id: true,
        dataset: true,
        rect: true,
        size: true,
      },
      cb,
    )
    return this._$sq
  }

  scrollOffset(
    cb: (res: S extends true ? ScrollOffset : ScrollOffset[]) => void = () => {
      /* empty */
    },
  ) {
    this._$sq._$push(
      this._$sel,
      this._$comp,
      this._$single,
      {
        id: true,
        dataset: true,
        scrollOffset: true,
      },
      cb,
    )
    return this._$sq
  }

  context(
    cb: (res: S extends true ? ContextResult : any[]) => void = () => {
      /* empty */
    },
  ) {
    this._$sq._$push(this._$sel, this._$comp, this._$single, { context: true }, cb)
    return this._$sq
  }
}

// main class
export class SelectorQuery {
  private _$comp: GeneralComponent
  /** @internal */
  _$queue: {
    component: GeneralComponent
    selector: string | null
    single: boolean
    fields: SelectorQueryFields
    cb: (res: any) => void
  }[]

  /** @internal */
  constructor(component: GeneralComponent) {
    this._$comp = component
    this._$queue = []
  }

  /** Change where the node should be selected from */
  in(component: GeneralComponent) {
    this._$comp = component
    return this
  }

  /** Select the first node that matches the selector */
  select(selector: string) {
    return new NodesRef(this, this._$comp, selector, true)
  }

  /** Select all nodes that match the selector */
  selectAll(selector: string) {
    return new NodesRef(this, this._$comp, selector, false)
  }

  /**
   * Select the viewport of the component
   *
   * Only the size and the scroll position is meaningful when the viewport is selected.
   */
  selectViewport() {
    return new NodesRef(this, this._$comp, null, true)
  }

  /** @internal */
  _$push(
    selector: string | null,
    component: GeneralComponent,
    single: boolean,
    fields: SelectorQueryFields,
    cb: (res: any) => void,
  ) {
    this._$queue.push({
      component,
      selector,
      single,
      fields,
      cb,
    })
  }

  /**
   * Execute all queries
   *
   * The `cb` is called after all queries done.
   */
  exec(
    cb: (resList: any[]) => void = () => {
      /* empty */
    },
  ) {
    const q = this._$queue
    joinAsync<any>(
      q.map((req) => (done) => {
        const { component, selector, single, fields, cb } = req
        if (selector === null) {
          // for the viewport selection, return available fields
          const res = {} as { [k: string]: any }
          joinAsync<undefined>(
            [
              (done) => {
                const ctx = component._$.getBackendContext()
                const width = ctx ? ctx.getWindowWidth() : 0
                const height = ctx ? ctx.getWindowHeight() : 0
                if (fields.id) res.id = ''
                if (fields.dataset) res.dataset = {}
                if (fields.mark) res.mark = {}
                if (fields.rect) {
                  res.left = 0
                  res.top = 0
                  res.right = width
                  res.bottom = height
                }
                if (fields.size) {
                  res.width = width
                  res.height = height
                }
                if (fields.scrollOffset) {
                  let rootComp = component._$.general()
                  while (rootComp.ownerShadowRoot) {
                    rootComp = rootComp.ownerShadowRoot.getHostNode()
                  }
                  rootComp.getBoundingClientRect(({ left, top, width, height }) => {
                    res.scrollLeft = left
                    res.scrollTop = top
                    res.scrollWidth = width
                    res.scrollHeight = height
                    done(undefined)
                  })
                } else {
                  done(undefined)
                }
              },
            ],
            () => {
              cb(res)
              done(res)
            },
          )
        } else {
          // for common node selection, return specified fields
          const constructSingleRes = (elem: flexui.Element, cb: (res: any) => void) => {
            const res = {} as { [k: string]: any }
            if (fields.id) res.id = elem.id
            if (fields.dataset) res.dataset = elem.dataset
            if (fields.mark) res.mark = elem.collectMarks()
            joinAsync<undefined>(
              [
                (done) => {
                  if (fields.rect || fields.size) {
                    elem.getBoundingClientRect((rect) => {
                      if (fields.rect) {
                        res.left = rect.left
                        res.top = rect.top
                        res.right = rect.left + rect.width
                        res.bottom = rect.top + rect.height
                      }
                      if (fields.size) {
                        res.width = rect.width
                        res.height = rect.height
                      }
                      done(undefined)
                    })
                  } else {
                    done(undefined)
                  }
                },
                (done) => {
                  if (fields.scrollOffset) {
                    elem.getScrollOffset((rect) => {
                      res.scrollLeft = rect.scrollLeft
                      res.scrollTop = rect.scrollTop
                      res.scrollWidth = rect.scrollWidth
                      res.scrollHeight = rect.scrollHeight
                      done(undefined)
                    })
                  } else {
                    done(undefined)
                  }
                },
                (done) => {
                  if (fields.context) {
                    elem.getContext((ctx) => {
                      res.context = ctx
                      done(undefined)
                    })
                  } else {
                    done(undefined)
                  }
                },
              ],
              () => {
                if (fields.properties) {
                  if (flexui.Component.isComponent(elem)) {
                    fields.properties.forEach((propName) => {
                      if (flexui.Component.hasProperty(elem, propName)) {
                        const ds = elem.getComponentOptions().propertyPassingDeepCopy
                        const v = (elem.data as { [x: string]: unknown })[propName]
                        if (ds === flexui.DeepCopyKind.None) {
                          res[propName] = v
                        } else if (ds === flexui.DeepCopyKind.Simple) {
                          res[propName] = flexui.dataUtils.simpleDeepCopy(v)
                        } else if (ds === flexui.DeepCopyKind.SimpleWithRecursion) {
                          res[propName] = flexui.dataUtils.deepCopy(v, true)
                        }
                      }
                    })
                  }
                }
                cb(res)
              },
            )
          }
          if (single) {
            // select single node
            const node = component._$.getShadowRoot()!.querySelector(selector)
            if (node) {
              constructSingleRes(node, (res) => {
                cb(res)
                done(res)
              })
            } else {
              engineSetTimeout(() => {
                cb(null)
                done(null)
              }, 0)
            }
          } else {
            // select multiple nodes
            const nodes = component._$.getShadowRoot()!.querySelectorAll(selector)
            joinAsync<any>(
              nodes.map((node) => (done) => {
                constructSingleRes(node, (res) => {
                  done(res)
                })
              }),
              (resList) => {
                cb(resList)
                done(resList)
              },
            )
          }
        }
      }),
      cb,
    )
  }
}