import type { FetchResponse } from "../runtime/fetch"
import { createBinding, disposeBinding } from "../core/signals"

export interface UINode {
  readonly type: string
  /** Author-given semantic name (a stable selector for tests + AI review feedback). */
  readonly name?: string
  style: Style<this, any>
}

export type UINodeChild = UINode | null | undefined | false

export type Color = number | string | null

export type BaseStyle = {
  opacity?: number | `${number}`,
  transform?: string | null,
  backgroundColor?: Color | null,
  bgColor?: Color | null,
  overflow?: "visible" | "hidden",
  boxSizing?: "border-box" | "content-box",
  /**
   * Author-given semantic name — a stable selector for tests and for marking elements in the AI
   * code-review feedback loop. NOT a style: it's extracted at construction (never sent to the layout
   * engine) and surfaced on the node + in the renderer's serialized output. Available on every element.
   */
  name?: string,
}

export type DrawableStyle = {
  backgroundImage?: FetchResponse | File | string | null,
  bgGradient?: string,
  backgroundGradient?: string,
  bgImage?: FetchResponse  | File | string | null,
  backgroundSize?: "cover" | "contain" | "tile",
  bgSize?: "cover" | "contain" | "tile",
  border?: string | number | null,
  borderWidth?: number,
  borderColor?: Color,

  borderLeft?: string | number | null,
  borderLeftWidth?: number,
  borderLeftColor?: Color,
  borderTop?: string | number | null,
  borderTopWidth?: number,
  borderTopColor?: Color,
  borderRight?: string | number | null,
  borderRightWidth?: number,
  borderRightColor?: Color,
  borderBottom?: string | number | null,
  borderBottomWidth?: number,
  borderBottomColor?: Color,

  borderRadius?: UIValue | string,
  borderTopLeftRadius?: UIValue,
  borderTopRightRadius?: UIValue,
  borderBottomLeftRadius?: UIValue,
  borderBottomRightRadius?: UIValue,

  pointerEvents?: "all" | "none",
}

export type ContainerStyle = {
  flexDirection?: "row" | "column",
  justifyContent?: "flex-start" | "center" | "flex-end" | "space-between" | "space-evenly",
  alignItems?: "flex-start" | "center" | "flex-end" | "stretch",
  gap?: UIValue,
  flexWrap?: "nowrap" | "wrap" | "wrap-reverse"
}

export type ScrollStyle = {
  scrollX?: "auto" | "all" | "none",
  scrollY?: "auto" | "all" | "none"
}

type UIValue = number | 
  `${number}px` | 
  `${number}vw` | 
  `${number}vh` | 
  `${number}vmin` | 
  `${number}vmax` | 
  `${number}em` | 
  `calc(${string})` |
  `min(${string})` |
  `max(${string})` |
  `${string}%` |
  `clamp(${string})`

export type PaddingStyle = {
  paddingLeft?: UIValue | "safe-left" | "safe-left-comfort",
  paddingTop?: UIValue | "safe-top" | "safe-top-comfort",
  paddingRight?: UIValue | "safe-right" | "safe-right-comfort",
  paddingBottom?: UIValue | "safe-bottom" | "safe-bottom-comfort",
  padding?: UIValue | "safe-all" | "safe-all-comfort",
  paddingHorizontal?: UIValue,
  paddingVertical?: UIValue,

  pl?: UIValue | "safe-left" | "safe-left-comfort",
  pt?: UIValue | "safe-top" | "safe-top-comfort",
  pr?: UIValue | "safe-right" | "safe-right-comfort",
  pb?: UIValue | "safe-bottom" | "safe-bottom-comfort",
  p?: UIValue | "safe-all" | "safe-all-comfort",
  px?: UIValue,
  py?: UIValue,
}



export type MarginStyle = {
  marginLeft?: UIValue | "auto" | "safe-left" | "safe-left-comfort",
  marginTop?: UIValue | "auto" | "safe-top" | "safe-top-comfort",
  marginRight?: UIValue | "auto" | "safe-right" | "safe-right-comfort",
  marginBottom?: UIValue | "auto" | "safe-bottom" | "safe-bottom-comfort",
  margin?: UIValue | "auto" | "safe-all" | "safe-all-comfort",
  marginHorizontal?: UIValue | "auto",
  marginVertical?: UIValue | "auto",

  ml?: UIValue | "auto" | "safe-left" | "safe-left-comfort",
  mt?: UIValue | "auto" | "safe-top" | "safe-top-comfort",
  mr?: UIValue | "auto" | "safe-right" | "safe-right-comfort",
  mb?: UIValue | "auto" | "safe-bottom" | "safe-bottom-comfort",
  m?: UIValue | "auto" | "safe-all" | "safe-all-comfort",
  mx?: UIValue | "auto",
  my?: UIValue | "auto",
}

export type PositionStyle = {
  top?: UIValue | `${number}%` | 'safe-top' | 'safe-top-comfort',
  left?: UIValue | `${number}%` | 'safe-left' | 'safe-left-comfort',
  bottom?: UIValue | `${number}%` | 'safe-bottom' | 'safe-bottom-comfort',
  right?: UIValue | `${number}%` | 'safe-right' | 'safe-right-comfort',
  width?: UIValue | `${number}%` | "auto",
  height?: UIValue | `${number}%` | "auto",
  maxWidth?: UIValue | `${number}%` | "auto",
  maxHeight?: UIValue | `${number}%` | "auto",
  minWidth?: UIValue | `${number}%` | "auto",
  minHeight?: UIValue | `${number}%` | "auto",
}

export type ElementStyle = PaddingStyle & MarginStyle & BaseStyle & PositionStyle & {
  position?: "absolute" | "static" | "relative",
  aspectRatio?: number,
  flex?: number | string,
  flexGrow?: number,
  flexShrink?: number,
  flexBase?: UIValue | `${number}%` | "auto",
  alignSelf?: "flex-start" | "center" | "flex-end" | "stretch",
  display?: "none" | "flex"
}

export type TextStyle =  {
  textAlign?: "start" | "center" | "end" | "left" | "right"
  fontFamily?: string,
  fontSize?: UIValue,
  lineHeight?: UIValue | "normal",
  fontWeight?: number | `${number}` | "normal" | "bold",
  fontStyle?: "normal" | "italic",
  color?: Color,
  textDecoration?: "underline" | "line-through" | "none",
  letterSpacing?: UIValue
}

// Pulls function-valued props out of `style` (mutating it — they must never reach the host bridge)
// and returns them. A static value under the same key overrides (disposes) an existing binding.
function extractBindings(el: any, style: any): [string, () => any][] | null {
  let fns: [string, () => any][] | null = null
  for (const key of Object.keys(style)) {
    const value = style[key]
    if (typeof value === "function") {
      (fns ??= []).push([key, value])
      delete style[key]
    } else if (el._fx !== undefined) {
      disposeBinding(el, key)
    }
  }
  return fns
}

// One reactive binding per style key: re-runs when a signal the function read changes, writing
// through the style proxy (so a mounted node hits _creatorUI.updateStyle, an unmounted one just
// keeps _style current for the next mount).
function applyBindings(el: any, fns: [string, () => any][]) {
  for (const [ key, fn ] of fns) {
    createBinding(el, key, () => { el.style[key] = fn() })
  }
}

// Не надо переписывать эту функцию 10 раз, обрати внимание, что у тебя тут возвращается this
function styleFunction(this: any, newStyle: any) {
  // `name` is a JS-node selector (read by hosts off el.name), not a layout property — extract it
  // exactly like the constructor so `.style({ name })` is equivalent to the first-arg style. Left
  // in the style it would never become el.name (breaking name-based lookups) and would leak into
  // the layout engine.
  if (newStyle && typeof newStyle.name === "string" && newStyle.name.length > 0) {
    this.name = newStyle.name
    delete newStyle.name
  }
  const fns = extractBindings(this, newStyle)
  Object.assign(this._style, newStyle)
  if (this._id !== 0) {
    _creatorUI.mergeStyle(this._id, newStyle)
  }
  if (fns) applyBindings(this, fns)
  return this
}

const handler: ProxyHandler<any> = {
  get(target, key, receiver) {
    if (key === "_raw") {
      return target._el._style
    }
    return target._el._style[key]
  },
  set(target, key: string, value: string | number) {
    if (typeof value === "function") {
      // Reactive binding: `el.style.prop = () => sig.value`. Note a later plain write does NOT
      // dispose the binding (this is the hot path) — override statically via `.style({ prop })`.
      const el = target._el
      const fn = value as unknown as () => any
      createBinding(el, key, () => { el.style[key] = fn() })
      return true
    }
    if (target._el._id !== 0) {
      _creatorUI.updateStyle(target._el._id, key, value)
    }
    target._el._style[key] = value
    return true
  },
  getOwnPropertyDescriptor(target, p) {
    return Object.getOwnPropertyDescriptor(target._el._style, p)
  },
  ownKeys(target) {    
    return Object.keys(target._el._style)
  }
}

function Style() {
  
}

// Reactive style input: every primitive-valued prop also accepts a `() => value` binding that
// re-applies when a signal it read changes. Nested state blocks (onPressed, onLandscape, …) stay
// static — bindings are extracted at the top level only.
export type Reactive<T> = { [K in keyof T]: NonNullable<T[K]> extends object ? T[K] : T[K] | (() => T[K]) }

// User-defined style classes: any `$`-prefixed key in .style() declares a state block (like
// onPressed, but with any name), toggled from code via setClass/toggleClass. `duration`/`delay`
// make the swap transition.
export type ClassStyles<T> = { [key: `$${string}`]: T & { duration?: number, delay?: number } }

export type StyleFn <R, T extends object> = ((this: R, style: Reactive<T> & { onLandscape?: T, onPortait?: T } & ClassStyles<DrawableStyle & BaseStyle>) => R)
export type Style <R, T extends object> = StyleFn<R,T> & T & { onLandscape?: T, onPortait?: T } & ClassStyles<DrawableStyle & BaseStyle>

export type AnimateStyle<R, T extends object> = ((this: R, style: T & { duration?: number, delay?: number, layer?: number, commit?: boolean }) => R)

export type AppearStyle <R, T> = (arg: { from: T, duration?: number, delay?: number }) => R
export type DisappearStyle <R, T> = (arg: { to: T, duration?: number, delay?: number }) => R

export type OnLayoutCallback = (layout: { left: number, top: number, width: number, height: number }) => void

export class Element<T extends string> {
  readonly type: T
  /** Author-given semantic name (LeCodes `name`) — see BaseStyle.name. Extracted from the options
   *  object so it never reaches the layout engine; the renderer reads it off the node. */
  readonly name?: string
  protected _style: any

  protected _id: number = 0

  protected _styleProxy: any
  protected _appear: any
  protected _disappear: any
  /** @internal Reactive bindings owned by this element (style key / "#text" → effect). The element
   *  is the effect's only strong owner, so discarding a subtree lets its bindings collect with it. */
  _fx?: Map<string, any>

  constructor(type: T, style: any) {
    this.type = type
    if (style && typeof style.name === "string" && style.name.length > 0) {
      this.name = style.name
      delete style.name
    }
    this._style = style
    if (style) {
      const fns = extractBindings(this, style)
      if (fns) applyBindings(this, fns)   // sync first run — _style holds concrete values pre-mount
    }
    // Creation-site capture for the editor preview's element inspector: the host sets the flag
    // before running the bundle, and the stack (mapped through the bundle's source map) points at
    // the user code that created this element. Off everywhere else — zero cost when unset.
    if ((globalThis as any).__lecodesCaptureSites) {
      (this as any)._site = new Error("lecodes-site")
    }
  }

  get style(): any {
    if (this._styleProxy) return this._styleProxy
    const obj = styleFunction.bind(this)
    // @ts-ignore
    obj._el = this

    const proxy = new Proxy(obj, handler)
    this._styleProxy = proxy
    return proxy
  }

  animateTo(style: any): this{
    _creatorUI.animateTo(this._id, style)
    if (style.commit !== false) {
      Object.assign(this._style, style)
    }
    return this
  }
  animateFrom(style: any): this{
    _creatorUI.animateFrom(this._id, style)
    return this
  }

  /** @internal Active style-class names (stored without the `$`). Kept on the element — not just in
   *  the layout engine — so hosts re-apply them when a detached node is re-mounted (screens
   *  re-assign _id on every open). */
  protected _classes?: Set<string>

  /** Activate/deactivate a `$`-class declared in .style() (accepts the name with or without `$`). */
  setClass(name: string, enabled: boolean): this {
    if (name[0] === "$") name = name.slice(1)
    if (enabled) {
      (this._classes ??= new Set()).add(name)
    } else {
      this._classes?.delete(name)
    }
    if (this._id !== 0) {
      _creatorUI.setClass(this._id, name, enabled)
    }
    return this
  }

  toggleClass(name: string): this {
    return this.setClass(name, !this.hasClass(name))
  }

  hasClass(name: string): boolean {
    return this._classes?.has(name[0] === "$" ? name.slice(1) : name) ?? false
  }

  /** Reactive class binding: keeps the `$`-class active exactly while `fn()` is true — re-runs
   *  when a signal `fn` read changes. Re-binding the same name replaces the previous binding. */
  bindClass(name: string, fn: () => boolean): this {
    if (name[0] === "$") name = name.slice(1)
    createBinding(this, "#class:" + name, () => { this.setClass(name, !!fn()) })
    return this
  }

  protected ll?: OnLayoutCallback[]
  onLayout(onLayout: OnLayoutCallback): this { 
    if (this.ll) {
      this.ll.push(onLayout)
    } else {
      this.ll = [ onLayout ]
    }
    return this
  }
}

// ---- reactive children --------------------------------------------------------------------------

export type ChildrenFn = () => UINodeChild[]

// The container whose children binding is currently executing. __uiMap uses it as the cache owner;
// nested bindings save/restore it, so each level caches on its own element.
let currentMapOwner: any = null

/**
 * Compiler-emitted memoized map for reactive children. The chisel `reactiveUi` pass rewrites
 * `list.map(fn)` inside a children binding to `__uiMap(list, fn, "<file>:<offset>")`, so unchanged
 * items keep their element identity across re-runs (the reconciler then leaves them mounted).
 * Cache: per (binding owner, call-site slot), keyed by item reference; duplicate items consume
 * cached nodes in order. Outside a children binding, for non-array inputs, or for non-element
 * results it behaves exactly like Array.prototype.map — a wrong rewrite can't change behavior.
 */
export function __uiMap(list: any, render: (item: any, index: number) => any, slot: string): any[] {
  if (currentMapOwner === null || !Array.isArray(list)) {
    return list?.map(render) ?? []
  }
  const slots: Map<string, Map<any, any[]>> = currentMapOwner._mapCache ??= new Map()
  const prev = slots.get(slot)
  const next = new Map<any, any[]>()
  const out: any[] = []
  for (let i = 0; i < list.length; i++) {
    const item = list[i]
    const stack = prev?.get(item)
    let node = stack !== undefined && stack.length > 0 ? stack.shift() : undefined
    if (node === undefined) {
      node = render(item, i)
      if (!(node instanceof Element)) {
        out.push(node)   // not a UI element — plain map semantics, never cached
        continue
      }
    }
    let arr = next.get(item)
    if (arr === undefined) next.set(item, arr = [])
    arr.push(node)
    out.push(node)
  }
  // entries left in `prev` (items gone from the list) are dropped here; their nodes and bindings
  // become unreachable and collect
  slots.set(slot, next)
  return out
}

export class ContainerElement<T extends string> extends Element<T> {
  children: any[] = []

  constructor(type: T, style: any, children: any[] | ChildrenFn) {
    super(type, style)
    if (typeof children === "function") {
      this._bindChildren(children)
    } else {
      this.children = children
    }
  }

  /** @internal Reactive children: re-runs when a signal read inside `fn` changes and reconciles
   *  the result against the mounted children with minimal insert/remove ops. */
  _bindChildren(fn: ChildrenFn) {
    createBinding(this, "#children", () => {
      const prevOwner = currentMapOwner
      currentMapOwner = this
      let next: UINodeChild[]
      try {
        next = fn()
      } finally {
        currentMapOwner = prevOwner
      }
      this._reconcile(Array.isArray(next) ? next : [])
    })
  }

  /** @internal Identity diff of the current children against `next`: departed nodes are removed,
   *  new ones inserted at their position, kept nodes stay mounted (so their host state survives).
   *  A moved node is remove+insert — it remounts. Falsy entries are compacted away. */
  _reconcile(next: UINodeChild[]) {
    const target: any[] = []
    for (const c of next) if (c) target.push(c)

    if (this._id === 0) {
      this.children.length = 0
      this.children.push(...target)
      return
    }

    // compact holes left by a previous static children array (falsy entries were never mounted)
    for (let i = this.children.length - 1; i >= 0; i--) {
      if (!this.children[i]) this.children.splice(i, 1)
    }

    const targetSet = new Set(target)
    const departed = this.children.filter(c => !targetSet.has(c))
    if (departed.length > 0) this.remove(...departed)

    // this.children now mirrors the mounted order; walk the target and fix mismatches in place
    for (let i = 0; i < target.length; i++) {
      if (this.children[i] === target[i]) continue
      const node = target[i]
      if (this.children.includes(node, i + 1)) {
        this.remove(node)   // moved forward — remount at its new position
      }
      this.insert(i, node)
    }
  }

  append(...nodes: UINodeChild[]): this {
    if (this._id === 0) {
      this.children.push(...nodes)
      return this
    }
    let index = 0
    for (let child of this.children) {
      if (child) {
        index++
      }
    }
    for (let node of nodes) {
      if (!node) continue
      _creatorUI.insertNode(index, this._id, node) 
      index++
    }
    this.children.push(...nodes)
    return this
  }
  insert(index: number, ...nodes: UINodeChild[]): this {
    if (this._id === 0) {
      this.children.splice(index, 0, ...nodes)
      return this
    }

    let _index = 0
    for (let i = 0; i < index; i++) {
      if (this.children[i]) {
        _index++
      }
    }
    for (let node of nodes) {
      if (!node) continue
      _creatorUI.insertNode(_index, this._id, node) 
      _index++
    }
    this.children.splice(index, 0, ...nodes)
    return this
  }
  remove(...nodesToDelete: UINode[]): this {
    const set = new Set(nodesToDelete)

    for (let i = 0; i < this.children.length; i++) {
      if (set.has(this.children[i])) {
        set.delete(this.children[i])
        this.children.splice(i, 1)
        i--

        if (set.size === 0) {
          break
        }
      }
    }

    if (this._id === 0) {
      return this
    }
    for (let node of nodesToDelete) {
      _creatorUI.removeNode(this._id, node)
    }
    return this
  }
  
  setContent(children: UINodeChild[] | ChildrenFn): this {
    if (typeof children === "function") {
      this._bindChildren(children)   // createBinding replaces a previous "#children" binding
      return this
    }
    disposeBinding(this, "#children")  // static content overrides a reactive binding
    if (this._id !== 0) {
      _creatorUI.setContent(this._id, this.children, children)
    }
    this.children.length = 0
    this.children.push(...children)
    return this
  }
}