import { LitElement, PropertyValues } from 'lit'
import { state, property } from 'lit/decorators.js'
import type { ApiModel } from '@api-client/core/browser.js'
import { bound } from '../../decorators/bound.js'

/**
 * The base class for the modeling editors.
 * Contains common properties and logic for all API editors.
 *
 * Each editor has to have the API `model` property, which is the model being edited.
 * Then each class has to implement its own rendering and editing logic.
 *
 * @fires change - An event dispatched when the editor value was changed.
 */
export abstract class ApiEditor extends LitElement {
  /**
   * If true, the editor is in read-only mode.
   * @attribute {boolean} readonly
   */
  @property({ type: Boolean }) accessor readonly: boolean

  /**
   * The API model being edited.
   * @attribute {object} model
   */
  @property({ type: Object }) accessor model: ApiModel | undefined

  /**
   * The ID of the node in the model graph that is being edited.
   * @attribute {string} domainKey
   */
  @property({ type: String }) accessor domainKey: string | undefined

  /**
   * The namespace key of the node in the model graph that is being edited.
   * To be used only when the edited node comes from a specific domain.
   * @attribute {string} domainNamespace
   */
  @property({ type: String }) accessor domainNamespace: string | undefined

  /**
   * If true, the editor has unsaved changes.
   * This is a general purpose property that can be used by the editor
   * to indicate that the model was changed but not saved.
   * @attribute {boolean} dirty
   */
  @state() accessor dirty: boolean

  constructor() {
    super()
    this.readonly = false
    this.dirty = false
  }

  /**
   * A common way for an element to notify a change.
   */
  notifyChange(): void {
    this.dispatchEvent(new Event('change'))
  }

  protected override willUpdate(cp: PropertyValues<this>): void {
    if (cp.has('model')) {
      const old = cp.get('model')
      if (old) {
        old.removeEventListener('change', this.handleModelChange)
      }
      this.model?.addEventListener('change', this.handleModelChange)
    }
    if (cp.has('model') || cp.has('domainKey') || cp.has('domainNamespace')) {
      this.setupModel(this.model, this.domainKey, this.domainNamespace)
    }
    super.willUpdate(cp)
  }

  @bound
  protected handleModelChange(): void {
    this.requestUpdate()
  }

  /**
   * A hook method called when the `model` or `domainKey` properties change.
   * Subclasses should implement this method to setup their internal state
   * based on the new model and domainKey.
   *
   * @param model The new API model.
   * @param domainKey The new edited node ID.
   * @param domainNamespace The new edited node namespace.
   */
  protected abstract setupModel(model?: ApiModel, domainKey?: string, domainNamespace?: string): void | Promise<void>
}
