import { noChange } from 'lit'
import { directive, PartInfo } from 'lit/directive.js'
import { AsyncDirective } from 'lit/async-directive.js'

class ObservableDirective extends AsyncDirective {
  private value?: EventTarget
  private path?: string
  private changeEvent?: string
  private unsubscribe?: () => void
  private lastValue?: unknown

  constructor(_partInfo: PartInfo) {
    super(_partInfo)
    this.listener = this.listener.bind(this)
  }

  get(path: string): unknown | undefined {
    if (!path) {
      return undefined
    }
    const segments = path.split('.')
    let current: unknown = this.value
    for (const segment of segments) {
      if (current === null || current === undefined || typeof current !== 'object') {
        return undefined
      }
      if (Array.isArray(current)) {
        const index = parseInt(segment, 10)
        if (isNaN(index) || index < 0 || index >= current.length) {
          return undefined
        }
        current = current[index]
      } else {
        current = (current as Record<string, unknown>)[segment]
      }
    }
    return current
  }

  /**
   * @param target The target object containing the data to render.
   * @param path The path to the property to render.
   * @param defaultValue The default value to use if the property is not found.
   * @param changeEvent The event type to listen for changes (default is 'change').
   */
  render(target: EventTarget, path: string, defaultValue?: unknown, changeEvent?: 'string') {
    if (this.value !== target) {
      this.value = target
      this.path = path
      this.changeEvent = changeEvent
      this.unsubscribe?.()
      this.unsubscribe = undefined
      if (this.isConnected) {
        this.subscribe()
      }
    }
    let value = this.get(path)
    if (value === undefined) {
      value = defaultValue
    }
    if (value !== this.lastValue) {
      this.lastValue = value
      return value
    }
    return noChange
  }

  subscribe(): void {
    const { value, path, changeEvent = 'change' } = this
    if (value && path) {
      value.addEventListener(changeEvent, this.listener)
      this.unsubscribe = () => {
        value.removeEventListener(changeEvent, this.listener)
      }
    }
  }

  protected override disconnected() {
    this.unsubscribe?.()
    this.unsubscribe = undefined
  }

  protected override reconnected() {
    this.subscribe()
  }

  protected listener(): void {
    const value = this.get(this.path as string)
    this.setValue(value)
  }
}

/**
 * A Lit directive that connects a `LiveData` property to a LitElement property.
 * When the `LiveData` property changes, the LitElement re-renders.
 *
 * @param liveData - The `LiveData` instance.
 * @param path - The dot-notation path to the property to observe (e.g., "user.name").
 * @returns The current value of the `LiveData` property.
 *
 * @example
 * ```typescript
 * import { LitElement, html } from 'lit'
 * import { customElement } from 'lit/decorators.js'
 *
 * class ObservableData extends EventTarget {
 *   // ...
 * }
 *
 * @customElement('my-element')
 * export class MyElement extends LitElement {
 *   // Create an observable instance
 *   userData = new ObservableData({ name: 'Initial Name' });
 *
 *   override render() {
 *     return html`
 *       <div>User Name: ${observable(this.userData, 'name', 'change')}</div>
 *     `;
 *   }
 * }
 * ```
 */
export const observable = directive(ObservableDirective)
