import { ThingKind } from './kinds.js'
import { ModelValidationOptions } from './types.js'
import { observed } from '../decorators/observed.js'

/**
 * An interface describing a base metadata of a thing.
 */
export declare interface IThing {
  /**
   * The data kind. The application ignores the input with an unknown `kind`, unless
   * it can be determined from the context.
   */
  kind?: typeof ThingKind
  /**
   * The name of the thing.
   */
  name?: string
  /**
   * Optional value to overwrite the `name` in the UI.
   * The primary descriptive field is the `name`. The display name is only used in the presentation of the data.
   */
  displayName?: string
  /**
   * The description of the thing.
   */
  description?: string
  /**
   * The version number of the thing.
   */
  version?: string
}

export class Thing extends EventTarget {
  kind = ThingKind
  /**
   * The name of the thing.
   */
  @observed() accessor name: string | undefined
  /**
   * Optional value to overwrite the `name` in the UI.
   * The primary descriptive field is the `name`. The display name is only used in the presentation of the data.
   */
  @observed() accessor displayName: string | undefined
  /**
   * The description of the thing.
   */
  @observed() accessor description: string | undefined
  /**
   * The version number of the thing.
   */
  @observed() accessor version: string | undefined

  /**
   * Returns one in this order:
   * - displayName
   * - name
   * - 'Unnamed object'
   *
   * @deprecated Use `getLabel()` instead.
   */
  get renderLabel(): string {
    return this.displayName || this.name || 'Unnamed object'
  }

  /**
   * Creates a basic description from a name.
   */
  static fromName(name: string): Thing {
    const thing = new Thing({
      name,
      kind: ThingKind,
    })
    return thing
  }

  /**
   * Creates a new thing instance.
   *
   * @param input The thing definition used to restore the state.
   */
  static fromJSON(input: Partial<IThing> = {}, defaults: Partial<IThing> = {}): Thing {
    const result = new Thing()
    const { name, displayName, description, version } = input || {}
    if (typeof name === 'string') {
      result.name = name
    } else if (typeof defaults.name === 'string') {
      result.name = defaults.name
    }
    if (typeof displayName === 'string') {
      result.displayName = displayName
    } else if (typeof defaults.displayName === 'string') {
      result.displayName = defaults.displayName
    }
    if (description) {
      result.description = description
    } else if (typeof defaults.description === 'string') {
      result.description = defaults.description
    }
    if (version) {
      result.version = version
    } else if (typeof defaults.version === 'string') {
      result.version = defaults.version
    }
    return result
  }

  /**
   * @param input The thing definition used to restore the state.
   */
  constructor(input?: string | IThing) {
    super()
    let init: IThing
    if (typeof input === 'string') {
      init = JSON.parse(input)
    } else if (typeof input === 'object') {
      init = input
    } else {
      init = {
        kind: ThingKind,
      }
    }
    this.new(init)
  }

  /**
   * Creates a new thing clearing anything that is so far defined.
   *
   * Note, this throws an error when the server is not a thing.
   */
  new(init: IThing): void {
    if (!Thing.isThing(init)) {
      throw new Error(`Not a thing.`)
    }
    const { description, name, version, displayName } = init
    this.kind = ThingKind
    this.name = name
    this.displayName = displayName
    this.description = description
    this.version = version
  }

  notifyChange(): void {
    this.dispatchEvent(new Event('change'))
  }

  static validate(input: unknown, options: ModelValidationOptions = {}): boolean {
    const { strict } = options
    if (input && typeof input === 'object') {
      const typed = input as IThing
      if (strict && typed.kind !== ThingKind) {
        return false
      }
      return true
    }
    return false
  }

  /**
   * Checks whether the input is a definition of a server.
   */
  static isThing(input: unknown): boolean {
    const typed = input as IThing
    if (input && typed.kind === ThingKind) {
      return true
    }
    return false
  }

  toJSON(): IThing {
    const result: IThing = {
      kind: ThingKind,
    }
    if (typeof this.name === 'string') {
      result.name = this.name
    }
    if (typeof this.displayName === 'string') {
      result.displayName = this.displayName
    }
    if (this.description) {
      result.description = this.description
    }
    if (this.version) {
      result.version = this.version
    }
    return result
  }

  /**
   * Returns one in this order:
   * - `this.displayName`
   * - `this.name`
   * - `defaultName`
   * - 'Unnamed object'
   *
   * @param defaultName The default name to use when no other is available.
   */
  getLabel(defaultName?: string): string {
    return this.displayName || this.name || defaultName || 'Unnamed object'
  }
}
