// src/framework/shadowlisp/SlDomRenderer.ts
//
// Vision 48: ShadowLisp DOM Renderer
//
// Alan Kay (Smalltalk creator, Turing Award 2003):
//   "The UI IS the program rendered as DOM. S-expressions become elements.
//    Live programming means the DOM reflects the code in real-time."
//
// Rich Hickey (Clojure creator):
//   "Hiccup showed that vectors render to HTML naturally.
//    ShadowLisp shows that DOM elements ARE S-expressions."
//
// This renders ShadowLisp S-expressions to live DOM elements.

import type { SExpr } from '../../lisp/SExpr';

/**
 * HTML element names that SlDomRenderer recognizes as element forms.
 */
const HTML_ELEMENTS = new Set([
  // Document structure
  'html', 'head', 'body', 'main', 'header', 'footer', 'nav', 'aside', 'article', 'section',
  // Content sectioning
  'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'blockquote', 'pre', 'code',
  // Text content
  'div', 'span', 'a', 'em', 'strong', 'small', 'mark', 'del', 'ins', 'sub', 'sup',
  // Lists
  'ul', 'ol', 'li', 'dl', 'dt', 'dd',
  // Tables
  'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td', 'caption',
  // Forms
  'form', 'input', 'textarea', 'select', 'option', 'optgroup', 'button', 'label', 'fieldset', 'legend',
  // Media
  'img', 'video', 'audio', 'source', 'canvas', 'svg', 'iframe',
  // Interactive
  'details', 'summary', 'dialog', 'menu',
  // Other
  'br', 'hr', 'template', 'slot'
]);

/**
 * SVG element names.
 */
const SVG_ELEMENTS = new Set([
  'svg', 'g', 'path', 'circle', 'rect', 'line', 'polyline', 'polygon',
  'text', 'tspan', 'defs', 'use', 'symbol', 'clipPath', 'mask',
  'linearGradient', 'radialGradient', 'stop', 'filter', 'feGaussianBlur',
  'animate', 'animateTransform'
]);

/**
 * Event handler attribute prefixes.
 */
const EVENT_ATTRIBUTES = new Set([
  'onclick', 'ondblclick', 'onmousedown', 'onmouseup', 'onmouseover', 'onmouseout',
  'onmousemove', 'onmouseenter', 'onmouseleave', 'onkeydown', 'onkeyup', 'onkeypress',
  'onfocus', 'onblur', 'onchange', 'oninput', 'onsubmit', 'onreset', 'onscroll',
  'onload', 'onerror', 'ondragstart', 'ondrag', 'ondragend', 'ondrop'
]);

/**
 * Render context for tracking bindings and state.
 */
export interface IRenderContext {
  /** Look up a value in the current environment */
  lookup: (name: string) => unknown;
  /** Subscribe to state changes for reactive updates */
  subscribe?: (path: string, callback: (value: unknown) => void) => () => void;
  /** Emit an event to the actor */
  emit?: (event: string, payload?: unknown) => void;
  /** Send a command to the actor */
  send?: (command: string, payload?: unknown) => void;
}

/**
 * Binding info for reactive updates.
 */
interface IBinding {
  element: Element;
  attribute?: string;
  path: string;
  unsubscribe?: () => void;
}

/**
 * SlDomRenderer - Render S-expressions to DOM elements.
 *
 * Converts ShadowLisp expressions like:
 *   (div :class "container" (span "Hello") (button :onclick handler "Click"))
 *
 * Into live DOM:
 *   <div class="container"><span>Hello</span><button>Click</button></div>
 *
 * @example
 * ```typescript
 * const renderer = new SlDomRenderer(context);
 * const element = renderer.render(['div', ':class', 'foo', 'Hello']);
 * document.body.appendChild(element);
 * ```
 */
export class SlDomRenderer {
  private readonly context: IRenderContext;
  private readonly bindings: IBinding[] = [];

  constructor(context: IRenderContext) {
    this.context = context;
  }

  private _infoKernel(message: string, ...args: unknown[]): void {
    // eslint-disable-next-line no-console -- kernel ShadowLisp DOM renderer diagnostics have no actor context
    console.log(message, ...args);
  }

  /**
   * Check if a symbol is an HTML element name.
   */
  static isElementName(name: string): boolean {
    return HTML_ELEMENTS.has(name.toLowerCase()) || SVG_ELEMENTS.has(name);
  }

  /**
   * Render an S-expression to DOM.
   *
   * @param sexp - S-expression to render
   * @returns DOM Element or Text node
   */
  render(sexp: SExpr): Node {
    // Null/undefined → empty text
    if (sexp === null || sexp === undefined) {
      return document.createTextNode('');
    }

    // Number → text node
    if (typeof sexp === 'number') {
      return document.createTextNode(String(sexp));
    }

    // Boolean → text node
    if (typeof sexp === 'boolean') {
      return document.createTextNode(String(sexp));
    }

    // String → text node (strip quotes if present)
    if (typeof sexp === 'string') {
      const text = sexp.startsWith('"') && sexp.endsWith('"')
        ? sexp.slice(1, -1)
        : sexp;
      return document.createTextNode(text);
    }

    // Array → element or special form
    if (Array.isArray(sexp)) {
      return this._renderForm(sexp);
    }

    // Fallback
    return document.createTextNode(String(sexp));
  }

  /**
   * Render a list form.
   */
  private _renderForm(form: SExpr[]): Node {
    if (form.length === 0) {
      return document.createTextNode('');
    }

    const [head, ...rest] = form;

    // Head must be a string (element name or special form)
    if (typeof head !== 'string') {
      // Render each item and join as fragment
      const fragment = document.createDocumentFragment();
      for (const item of form) {
        fragment.appendChild(this.render(item));
      }
      return fragment;
    }

    // Strip quotes from head if present
    const op = head.startsWith('"') && head.endsWith('"')
      ? head.slice(1, -1)
      : head;

    // Special forms
    switch (op) {
      case 'bind':
        return this._renderBind(rest);

      case 'if':
        return this._renderIf(rest);

      case 'for':
      case 'map':
        return this._renderFor(rest);

      case 'fragment':
        return this._renderFragment(rest);

      case 'text':
        return this._renderText(rest);

      default:
        // Check if it's an HTML element
        if (SlDomRenderer.isElementName(op)) {
          return this._renderElement(op, rest);
        }

        // Unknown form - try to render as text
        return document.createTextNode(`(${op} ...)`);
    }
  }

  /**
   * Render an HTML element.
   */
  private _renderElement(tagName: string, args: SExpr[]): Element {
    // Create element (handle SVG namespace)
    const isSvg = SVG_ELEMENTS.has(tagName);
    const element = isSvg
      ? document.createElementNS('http://www.w3.org/2000/svg', tagName)
      : document.createElement(tagName);

    // Parse attributes and children
    let i = 0;
    while (i < args.length) {
      const arg = args[i];

      // Keyword attribute (starts with :)
      if (typeof arg === 'string' && arg.startsWith(':')) {
        const attrName = arg.slice(1); // Remove :
        const attrValue = args[i + 1];
        this._setAttribute(element, attrName, attrValue);
        i += 2;
        continue;
      }

      // Child element or text
      const child = this.render(arg);
      element.appendChild(child);
      i++;
    }

    return element;
  }

  /**
   * Set an attribute on an element.
   */
  private _setAttribute(element: Element, name: string, value: SExpr): void {
    // Handle special attributes
    const lowerName = name.toLowerCase();

    // Event handlers
    if (EVENT_ATTRIBUTES.has(lowerName) || lowerName.startsWith('on')) {
      this._setEventHandler(element, lowerName, value);
      return;
    }

    // Style object
    if (lowerName === 'style' && Array.isArray(value)) {
      this._setStyle(element as HTMLElement, value);
      return;
    }

    // Class list
    if (lowerName === 'class' || lowerName === 'classname') {
      const classes = this._evaluateValue(value);
      if (Array.isArray(classes)) {
        element.className = classes.filter(Boolean).join(' ');
      } else {
        element.className = String(classes ?? '');
      }
      return;
    }

    // Boolean attributes
    if (typeof value === 'boolean') {
      if (value) {
        element.setAttribute(name, '');
      } else {
        element.removeAttribute(name);
      }
      return;
    }

    // Bind expression
    if (Array.isArray(value) && value[0] === 'bind') {
      this._bindAttribute(element, name, value.slice(1));
      return;
    }

    // Regular attribute
    const evaluated = this._evaluateValue(value);
    element.setAttribute(name, String(evaluated ?? ''));
  }

  /**
   * Set an event handler.
   */
  private _setEventHandler(element: Element, eventName: string, handler: SExpr): void {
    const event = eventName.replace(/^on/, '');

    // Handler could be:
    // - A function from context
    // - A lambda expression
    // - A send/emit form

    const callback = (e: Event) => {
      if (typeof handler === 'string') {
        // Look up handler in context
        const fn = this.context.lookup(handler);
        if (typeof fn === 'function') {
          fn(e);
        }
      } else if (Array.isArray(handler)) {
        const [op, ...args] = handler;
        if (op === 'send' && this.context.send) {
          const command = this._evaluateValue(args[0]) as string;
          const payload = args[1] ? this._evaluateValue(args[1]) : undefined;
          this.context.send(command, payload);
        } else if (op === 'emit' && this.context.emit) {
          const eventName = this._evaluateValue(args[0]) as string;
          const payload = args[1] ? this._evaluateValue(args[1]) : undefined;
          this.context.emit(eventName, payload);
        } else if (op === 'lambda') {
          // Evaluate lambda with event as argument
          // For now, just log
          this._infoKernel('[SlDomRenderer] Lambda handler invoked', args);
        }
      }
    };

    element.addEventListener(event, callback);
  }

  /**
   * Set style from S-expression.
   */
  private _setStyle(element: HTMLElement, styleExpr: SExpr[]): void {
    // Style is (hash "property" value ...)
    if (styleExpr[0] === 'hash') {
      for (let i = 1; i < styleExpr.length; i += 2) {
        const prop = this._evaluateValue(styleExpr[i]) as string;
        const val = this._evaluateValue(styleExpr[i + 1]);
        element.style.setProperty(
          prop.replace(/([A-Z])/g, '-$1').toLowerCase(),
          String(val)
        );
      }
    }
  }

  /**
   * Render a bind expression for reactive content.
   */
  private _renderBind(args: SExpr[]): Node {
    // Path should be a symbol name, NOT evaluated
    let path: string;
    const pathArg = args[0];
    if (typeof pathArg === 'string') {
      // Strip quotes if present (it's a symbol, not a string literal)
      path = pathArg.startsWith('"') && pathArg.endsWith('"')
        ? pathArg.slice(1, -1)
        : pathArg;
    } else {
      path = String(pathArg);
    }

    // Get initial value
    const value = this.context.lookup(path);
    const textNode = document.createTextNode(String(value ?? ''));

    // Subscribe to changes if possible
    if (this.context.subscribe) {
      const unsubscribe = this.context.subscribe(path, (newValue) => {
        textNode.textContent = String(newValue ?? '');
      });

      this.bindings.push({
        element: textNode as unknown as Element,
        path,
        unsubscribe
      });
    }

    return textNode;
  }

  /**
   * Bind an attribute to reactive state.
   */
  private _bindAttribute(element: Element, attrName: string, args: SExpr[]): void {
    const path = this._evaluateValue(args[0]) as string;

    // Set initial value
    const value = this.context.lookup(path);
    element.setAttribute(attrName, String(value ?? ''));

    // Subscribe to changes
    if (this.context.subscribe) {
      const unsubscribe = this.context.subscribe(path, (newValue) => {
        element.setAttribute(attrName, String(newValue ?? ''));
      });

      this.bindings.push({
        element,
        attribute: attrName,
        path,
        unsubscribe
      });
    }
  }

  /**
   * Render conditional (if test then else).
   */
  private _renderIf(args: SExpr[]): Node {
    const [test, thenBranch, elseBranch] = args;
    const testValue = this._evaluateValue(test);

    if (testValue) {
      return this.render(thenBranch);
    } else if (elseBranch !== undefined) {
      return this.render(elseBranch);
    }

    return document.createTextNode('');
  }

  /**
   * Render a for/map loop.
   */
  private _renderFor(args: SExpr[]): Node {
    // (for items (lambda (item) (div item)))
    const [collection, mapper] = args;
    const items = this._evaluateValue(collection) as unknown[];

    const fragment = document.createDocumentFragment();

    if (Array.isArray(items) && Array.isArray(mapper) && mapper[0] === 'lambda') {
      const [, params, body] = mapper;
      const paramName = Array.isArray(params) ? params[0] : params;

      for (const item of items) {
        // Create a temporary context with the item bound
        const itemContext: IRenderContext = {
          ...this.context,
          lookup: (name) => {
            if (name === paramName) return item;
            return this.context.lookup(name);
          }
        };

        const itemRenderer = new SlDomRenderer(itemContext);
        fragment.appendChild(itemRenderer.render(body as SExpr));
      }
    }

    return fragment;
  }

  /**
   * Render a fragment (multiple children without wrapper).
   */
  private _renderFragment(children: SExpr[]): Node {
    const fragment = document.createDocumentFragment();
    for (const child of children) {
      fragment.appendChild(this.render(child));
    }
    return fragment;
  }

  /**
   * Render explicit text content.
   */
  private _renderText(args: SExpr[]): Node {
    const text = args.map(arg => this._evaluateValue(arg)).join('');
    return document.createTextNode(text);
  }

  /**
   * Evaluate a value, looking up symbols in context.
   */
  private _evaluateValue(sexp: SExpr): unknown {
    if (sexp === null || sexp === undefined) return null;
    if (typeof sexp === 'number') return sexp;
    if (typeof sexp === 'boolean') return sexp;

    if (typeof sexp === 'string') {
      // Quoted string - strip quotes
      if (sexp.startsWith('"') && sexp.endsWith('"')) {
        return sexp.slice(1, -1);
      }
      // Keyword - return as-is
      if (sexp.startsWith(':')) {
        return sexp;
      }
      // Symbol - look up in context
      return this.context.lookup(sexp);
    }

    if (Array.isArray(sexp)) {
      const [op, ...args] = sexp;

      // Hash/object literal
      if (op === 'hash') {
        const obj: Record<string, unknown> = {};
        for (let i = 0; i < args.length; i += 2) {
          const key = this._evaluateValue(args[i]) as string;
          const val = this._evaluateValue(args[i + 1]);
          obj[key] = val;
        }
        return obj;
      }

      // List literal
      if (op === 'list') {
        return args.map(arg => this._evaluateValue(arg));
      }

      // Other forms - return as-is for now
      return sexp;
    }

    return sexp;
  }

  /**
   * Dispose all bindings.
   */
  dispose(): void {
    for (const binding of this.bindings) {
      binding.unsubscribe?.();
    }
    this.bindings.length = 0;
  }
}
