/*
 * Copyright (c) 2010, 2025 BSI Business Systems Integration AG
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 */
import {BaseDoEntity, Constructor, ObjectFactory, strings} from '../index';

/**
 * Holds all data object class mappings.
 *
 * Data object classes having an @typeName decorator do register themselves automatically into this registry.
 * The code to do that is automatically created and injected at compile time.
 * See DataObjectTransformer#_createDoInventoryAddStatement.
 * Therefore, the API of this class must match the code generated by this transformer.
 * If this class name, the method name 'get' (static accessor) or 'add' (to register a data object) are changed, the transformer must be adapted as well!
 */
export class DataObjectInventory {

  protected static _INSTANCE: DataObjectInventory = null;
  protected _constructorByTypeName = new Map<string, Constructor<BaseDoEntity>>();
  protected _typeNameByObjectType = new Map<string, string>();
  protected _objectTypeByTypeName = new Map<string, string>();

  protected constructor() {
  }

  /**
   * Adds a new dataobject to the registry.
   * @param doClass The dataobject class to register.
   * @param typeName Optional typeName (`_type` attribute) of this dataobject. E.g. `myNamespace.MyEntity`.
   * If omitted, it will be detected from the given class by creating a new instance and reading the `_type` attribute.
   * So this attribute must be set either by a `@typeName()` decorator on the class or as part of its constructor.
   * @param objectType Optional object type of the dataobject. E.g. `myNamespace.MyEntityDo`.
   * If omitted, it will be read from the `ObjectFactory`. So the constructor must have been registered to the `ObjectFactory` already.
   * @returns true if the class could be completely registered. false otherwise (e.g. if the typeName or objectType is unknown).
   */
  add(doClass: Constructor<BaseDoEntity>, typeName?: string, objectType?: string): boolean {
    if (!doClass) {
      return false;
    }
    typeName = typeName || this._readTypeName(doClass);
    if (!typeName) {
      return false;
    }

    if (this._constructorByTypeName.has(typeName)) {
      throw new Error(`There is already a constructor registered for type name '${typeName}'.`);
    }
    this._constructorByTypeName.set(typeName, doClass);
    objectType = objectType || ObjectFactory.get().getObjectType(doClass);
    if (!objectType) {
      return false;
    }

    objectType = strings.removePrefix(objectType, 'scout.'); // scout elements are in the map without namespace.
    this._typeNameByObjectType.set(objectType, typeName);
    this._objectTypeByTypeName.set(typeName, objectType);
    return true;
  }

  /**
   * Removes the dataobject given.
   * @param item The dataobject class or the typeName (_type) of the dataobject to remove.
   */
  remove(item: Constructor<BaseDoEntity> | string) {
    if (typeof item === 'string') {
      this._removeByTypeName(item);
    } else {
      this._removeByClass(item);
    }
  }

  protected _removeByClass(doClass: Constructor<BaseDoEntity>) {
    if (!doClass) {
      return;
    }
    for (const [typeName, doConstructor] of this._constructorByTypeName) {
      if (doConstructor === doClass) {
        this._removeByTypeName(typeName);
      }
    }
  }

  protected _removeByTypeName(typeName: string) {
    if (!typeName) {
      return;
    }
    this._constructorByTypeName.delete(typeName);
    const objectType = this._objectTypeByTypeName.get(typeName);
    if (objectType) {
      this._typeNameByObjectType.delete(objectType);
    }
    this._objectTypeByTypeName.delete(typeName);
  }

  protected _readTypeName(DoClass: Constructor<BaseDoEntity>): string {
    return new DoClass()._type;
  }

  /**
   * @returns All dataobject classes known to the registry.
   */
  getKnownDataObjectClasses(): IterableIterator<Constructor<BaseDoEntity>> {
    return this._constructorByTypeName.values();
  }

  /**
   * @param typeNameOrObjectType The typeName (_type like 'scout.Topic') or objectType (like 'scout.TopicDo') for which the dataobject class should be returned.
   * @returns the dataobject class for given typeName (_type) or objectType.
   */
  toConstructor(typeNameOrObjectType: string): Constructor<BaseDoEntity> {
    return this._constructorByTypeName.get(typeNameOrObjectType) || this._constructorByTypeName.get(this.toTypeName(typeNameOrObjectType));
  }

  /**
   * @param objectType The objectType for which the typeName (_type) should be returned. E.g. 'scout.CodeDo' or 'myapp.MySpecialDo'.
   * @returns the dataobject typeName (_type) for given objectType. E.g. returns 'scout.Topic' for 'scout.TopicDo'. Or 'myApp.MySpecial' for 'myApp.MySpecialDo'. This is the inverse operation of {@link toObjectType}.
   */
  toTypeName(objectType: string): string {
    objectType = strings.removePrefix(objectType, 'scout.'); // scout elements are in the map without namespace.
    return this._typeNameByObjectType.get(objectType);
  }

  /**
   * @param typeName The typeName (_type) for which the objectType should be returned.
   * @returns the dataobject objectType for given typeName (_type). E.g. returns 'TopicDo' for 'scout.Topic'. Or 'myApp.MySpecialDo' for 'myApp.MySpecial'. This is the inverse operation of {@link toTypeName}.
   */
  toObjectType(typeName: string): string {
    return this._objectTypeByTypeName.get(typeName);
  }

  /**
   * @returns the DataObjectInventory instance.
   */
  static get(): DataObjectInventory {
    if (!DataObjectInventory._INSTANCE) {
      // Do not create using scout.create as this registry instance is already used very early during source code parsing.
      // At this time no object factory is available yet.
      DataObjectInventory._INSTANCE = new DataObjectInventory();
    }
    return DataObjectInventory._INSTANCE;
  }
}

// proactively register this class on the window object to ensure it can be used by dataobjects to register themselves.
// See DataObjectTransformer#_createDoInventoryAddStatement.
window['scout'] = window['scout'] || {};
window['scout'].DataObjectInventory = DataObjectInventory;
