/*
 * Copyright (c) 2010, 2024 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 {
  ButtonTile, ChildModelOf, EnumObject, Event, EventHandler, EventListener, EventMapOf, EventModel, EventSupport, Form, HtmlComponent, icons, InitModelOf, inspector, Menu, MenuBar, menus, ObjectOrChildModel, Outline, PageEventMap,
  PageModel, PropertyChangeEvent, scout, strings, Table, TableRow, TableRowClickEvent, TileOutlineOverview, TileOverviewForm, TreeNode, Widget
} from '../../../index';
import $ from 'jquery';

/**
 * This class is used differently in online and JS-only case. In the online case we only have instances
 * of Page in an outline. The server sets the property <code>nodeType</code> which is used to distinct
 * between pages with tables and pages with nodes in some cases. In the JS only case, Page is an abstract
 * class and is never instantiated directly, instead we always use subclasses of PageWithTable or PageWithNodes.
 * Implementations of these classes contain code which loads table data or child nodes.
 */
export class Page extends TreeNode implements PageModel {
  declare model: PageModel;
  declare eventMap: PageEventMap;
  declare self: Page;
  declare parent: Outline;
  declare childNodes: Page[];
  declare parentNode: Page;

  /**
   * This property is set by the server, see: JsonOutline#putNodeType.
   */
  nodeType: NodeType;
  compactRoot: boolean;
  detailTable: Table;
  detailTableVisible: boolean;
  detailForm: Form;
  detailFormVisible: boolean;
  detailFormVisibleByUi: boolean;
  navigateButtonsVisible: boolean;
  tableStatusVisible: boolean;
  htmlComp: HtmlComponent;
  /**
   * True to select the page linked with the selected row when the row was selected. May be useful on touch devices.
   */
  drillDownOnRowClick: boolean;
  /**
   * The icon id which is used for icons in the tile outline overview.
   */
  overviewIconId: string;
  showTileOverview: boolean;
  inheritMenusFromParentTablePage: boolean;
  row: TableRow;
  tile: ButtonTile;
  events: EventSupport;
  pageChanging: number;

  protected _tableFilterHandler: EventHandler<Event<Table>>;
  protected _tableRowClickHandler: EventHandler<TableRowClickEvent>;
  protected _detailTableModel: ChildModelOf<Table>;
  /** @internal */
  _detailFormModel: ChildModelOf<Form>;
  protected _menuOwnerMenusChangeHandler: (event: Event<MenuOwner>) => void;

  constructor() {
    super();

    this.nodeType = null;
    this.compactRoot = false;
    this.detailTable = null;
    this.detailTableVisible = true;
    this.detailForm = null;
    this.detailFormVisible = true;
    this.detailFormVisibleByUi = true;
    this.navigateButtonsVisible = true;

    this.tableStatusVisible = true;
    this.drillDownOnRowClick = false;
    this.overviewIconId = null;
    this.showTileOverview = false;
    this.inheritMenusFromParentTablePage = true;
    this.events = new EventSupport();
    this.events.registerSubTypePredicate('propertyChange', (event: PropertyChangeEvent, propertyName) => event.propertyName === propertyName);
    this.pageChanging = 0;
    this._tableFilterHandler = this._onTableFilter.bind(this);
    this._tableRowClickHandler = this._onTableRowClick.bind(this);
    this._detailTableModel = null;
    this._detailFormModel = null;
    this._menuOwnerMenusChangeHandler = this._onMenuOwnerMenusChange.bind(this);
  }

  /**
   * This enum defines a node-type. This is basically used for the Scout Classic case where we only have instances
   * of Page, but never instances of PageWithTable or PageWithNodes. The server simply sets a nodeType instead.
   */
  static NodeType = {
    NODES: 'nodes',
    TABLE: 'table'
  } as const;

  protected override _init(model: InitModelOf<this>) {
    try {
      this.setPageChanging(true);
      this._detailTableModel = Page._removePropertyIfLazyLoading(model, 'detailTable') as ChildModelOf<Table>;
      this._detailFormModel = Page._removePropertyIfLazyLoading(model, 'detailForm') as ChildModelOf<Form>;

      super._init(model);
      icons.resolveIconProperty(this, 'overviewIconId');

      // init necessary if the properties are still available (e.g. Scout classic)
      this._internalInitTable();
      this._internalInitDetailForm();
    } finally {
      this.setPageChanging(false);
    }
  }

  protected static _removePropertyIfLazyLoading(object: PageModel, name: string): any {
    let prop = object[name];
    if (typeof prop === 'string') {
      // Scout Classic: it is an object id -> do not remove it. directly create the widget. lazy loading is done on backend
      return null;
    }
    if (prop instanceof Widget) {
      // it already is a widget. directly use it.
      return null;
    }

    // otherwise: remove the property and return it
    delete object[name];
    return prop;
  }

  protected override _destroy() {
    this.trigger('destroying');
    super._destroy();
    if (this.detailTable) {
      this.detailTable.destroy();
      this.detailTable = null;
    }
    if (this.detailForm) {
      this.detailForm.destroy();
      this.detailForm = null;
    }
    this.trigger('destroy');
  }

  protected _internalInitTable() {
    let tableModel = this.detailTable;
    if (tableModel) {
      // this case is used for Scout classic
      let newDetailTable = this.getOutline()._createChild(tableModel);
      this._setDetailTable(newDetailTable);
    }
  }

  protected _internalInitDetailForm() {
    let formModel = this.detailForm;
    if (formModel) {
      let newDetailForm = this.getOutline()._createChild(formModel);
      this._setDetailForm(newDetailForm);
    }
  }

  ensureDetailTable() {
    if (this.detailTable) {
      return;
    }
    this.setDetailTable(this.createDetailTable());
  }

  /**
   * Creates the detail table
   * @returns the created table or null
   */
  createDetailTable(): Table {
    let detailTable = this._createDetailTable();
    if (!detailTable && this._detailTableModel) {
      detailTable = this.getOutline()._createChild(this._detailTableModel);
      this._detailTableModel = null; // no longer needed
    }
    return detailTable;
  }

  /**
   * Override this function to create the internal table. Default impl. returns null.
   */
  protected _createDetailTable(): Table {
    return null;
  }

  ensureDetailForm() {
    if (this.detailForm) {
      return;
    }
    this.setDetailForm(this.createDetailForm());
  }

  /**
   * Creates the detail form
   * @returns the created form or null
   */
  createDetailForm(): Form {
    let detailForm = this._createDetailForm();
    if (!detailForm && this._detailFormModel) {
      detailForm = this.getOutline()._createChild(this._detailFormModel);
      this._detailFormModel = null; // no longer needed
    }
    return detailForm;
  }

  /**
   * Override this function to return a detail form which is displayed in the outline when this page is selected.
   * The default implementation returns null.
   */
  protected _createDetailForm(): Form {
    return null;
  }

  /**
   * Override this function to initialize the internal detail form.
   * @param form the form to initialize.
   */
  protected _initDetailForm(form: Form) {
    if (form instanceof Form) {
      form.setModal(false);
      form.setClosable(false);
      form.setDisplayHint(Form.DisplayHint.VIEW);
      form.setDisplayViewId('C');
      form.setShowOnOpen(false);
      this._updateParentTablePageMenusForDetailForm();
    }
    if (form instanceof TileOverviewForm) {
      form.setPage(this);
    }
  }

  protected _updateParentTablePageMenusForDetailFormAndDetailTable() {
    this._updateParentTablePageMenusForDetailForm();
    this._updateParentTablePageMenusForDetailTable();
  }

  protected _updateParentTablePageMenusForDetailForm() {
    this._updateParentTablePageMenusForMenuOwner(this.detailForm && this.detailForm.rootGroupBox);
  }

  protected _updateParentTablePageMenusForDetailTable() {
    this._updateParentTablePageMenusForMenuOwner(this.detailTable);
  }

  protected _updateParentTablePageMenusForMenuOwner(menuOwner: MenuOwner) {
    if (!menuOwner) {
      return;
    }

    menuOwner.off('propertyChange:menus', this._menuOwnerMenusChangeHandler);

    const originalMenus = menuOwner.menus || [];
    const parentTablePageMenus = this._computeParentTablePageMenus(menuOwner);

    menuOwner.setMenus(parentTablePageMenus
      .concat(originalMenus
        .filter((menu: Menu & { __parentTablePageMenu?: boolean }) => !menu.__parentTablePageMenu)
      ));

    menuOwner.on('propertyChange:menus', this._menuOwnerMenusChangeHandler);
  }

  protected _onMenuOwnerMenusChange(event: Event<MenuOwner>) {
    this._updateParentTablePageMenusForMenuOwner(event.source);
  }

  protected _computeParentTablePageMenus(newParent: Widget): Menu[] {
    if (!this.parentNode) {
      return [];
    }

    const table = this.parentNode.detailTable;
    const row = this.row;

    if (!table || !row || table !== row.getTable()) {
      return [];
    }

    return this._filterAndCloneParentTablePageMenus(table.menus, newParent);
  }

  protected _filterAndCloneParentTablePageMenus(tablePageMenus: Menu[], newParent: Widget): Menu[] {
    return this._filterParentTablePageMenus(tablePageMenus)
      .filter(this._isMenuInheritedFromParentTablePage.bind(this))
      .map(menu => this._cloneParentTablePageMenu(menu, newParent));
  }

  protected _filterParentTablePageMenus(tablePageMenus: Menu[]): Menu[] {
    return menus.filter(tablePageMenus, Table.MenuType.SingleSelection);
  }

  protected _cloneParentTablePageMenu(menu: Menu, newParent: Widget): Menu {
    if (!menu) {
      return null;
    }

    const clone = menu.clone(
      {
        parent: newParent,
        menuTypes: [],
        __parentTablePageMenu: true
      },
      {
        delegateEventsToOriginal: ['action'],
        delegateAllPropertiesToClone: true,
        excludePropertiesToClone: ['menuTypes', 'childActions']
      });

    if (menu.childActions && menu.childActions.length) {
      clone.setChildActions(this._filterAndCloneParentTablePageMenus(menu.childActions, clone));
    }

    return clone;
  }

  protected _isMenuInheritedFromParentTablePage(menu: Menu): boolean {
    return this.inheritMenusFromParentTablePage;
  }

  /**
   * Override this function to destroy the internal (detail) form.
   * @param form the form to destroy.
   */
  protected _destroyDetailForm(form: Form) {
    if (form instanceof TileOverviewForm) {
      form.setPage(null);
    }
    if (form.owner === this.getOutline()) {
      // in Scout classic the owner is not an outline but the NullWidget.
      // Then destroy is controlled by the backend
      form.destroy();
    }
  }

  /**
   * Override this function to initialize the internal (detail) table.
   * Default impl. delegates filter events to the outline mediator.
   * @param table The table to initialize.
   */
  protected _initDetailTable(table: Table) {
    table.menuBar.setPosition(MenuBar.Position.TOP);
    table.on('filter', this._tableFilterHandler);
    if (this.drillDownOnRowClick) {
      table.on('rowClick', this._tableRowClickHandler);
      table.setMultiSelect(false);
    }
    table.setTableStatusVisible(this.tableStatusVisible);
    this._updateParentTablePageMenusForDetailTable();
  }

  /**
   * Override this function to destroy the internal (detail) table.
   * @param table the table to destroy.
   */
  protected _destroyDetailTable(table: Table) {
    table.off('filter', this._tableFilterHandler);
    table.off('rowClick', this._tableRowClickHandler);
    if (table.owner === this.getOutline()) {
      // in Scout classic the owner is not an outline but the NullWidget.
      // Then destroy is controlled by the backend
      table.destroy();
    }
  }

  /** @internal */
  override _decorate() {
    super._decorate();
    if (!this.$node) {
      return;
    }
    if (this.session.inspector) {
      inspector.applyInfo(this, this.$node);
    }
    this.$node.toggleClass('compact-root', this.compactRoot);
    this.$node.toggleClass('has-tile-overview', this.showTileOverview ||
      (this.compactRoot && this.getOutline().detailContent instanceof TileOutlineOverview));
  }

  // see Java: AbstractPage#pageActivatedNotify
  activate() {
    this.ensureDetailTable();
    this.ensureDetailForm();
  }

  // see Java: AbstractPage#pageDeactivatedNotify
  deactivate() {
    // NOP
  }

  /**
   * @returns the tree / outline / parent instance. it's all the same,
   *     but it's more intuitive to work with the 'outline' when we deal with pages.
   */
  getOutline(): Outline {
    return this.parent;
  }

  /**
   * Returns an array of pages linked with the given rows. The order of the returned pages corresponds to the
   * order of the rows. Rows that are not linked to a page are ignored.
   */
  pagesForTableRows(rows: TableRow[]): Page[] {
    return rows.map(row => row.page).filter(Boolean);
  }

  /**
   * @param form The new form
   */
  setDetailForm(form: Form) {
    if (form === this.detailForm) {
      return;
    }
    this._setDetailForm(form);
  }

  protected _setDetailForm(form: Form) {
    let oldDetailForm = this.detailForm;
    if (oldDetailForm !== form && oldDetailForm instanceof Widget) {
      // must be a widget to be destroyed. At startup in Scout Classic it might be a string (the widget id)
      this._destroyDetailForm(oldDetailForm);
    }
    this.detailForm = form;
    if (form) {
      form.one('destroy', () => {
        if (this.detailForm === form) {
          this.detailForm = null;
        }
      });
      this._initDetailForm(form);
    }
    this.triggerPropertyChange('detailForm', oldDetailForm, form);
    this.getOutline().pageChanged(this);
  }

  /**
   * @param table The new table
   */
  setDetailTable(table: Table) {
    if (table === this.detailTable) {
      return;
    }
    this._setDetailTable(table);
  }

  protected _setDetailTable(table: Table) {
    let oldDetailTable = this.detailTable;
    if (oldDetailTable !== table && oldDetailTable instanceof Widget) {
      // must be a widget to be destroyed. At startup in Scout Classic it might be a string (the widget id)
      this._destroyDetailTable(oldDetailTable);
    }
    this.detailTable = table;
    if (table) {
      table.one('destroy', () => {
        if (this.detailTable === table) {
          this.detailTable = null;
        }
      });
      this._initDetailTable(table);
    }
    this.triggerPropertyChange('detailTable', oldDetailTable, table);
    this.getOutline().pageChanged(this);
  }

  /**
   * Updates relevant properties from the pages linked with the given rows using the method updatePageFromTableRow and
   * returns the pages. Rows that are not linked to a page are ignored.
   *
   * @returns pages linked with the given rows.
   */
  updatePagesFromTableRows(rows: TableRow[]): Page[] {
    return rows
      .filter(row => !!row.page)
      .map(row => row.page.updatePageFromTableRow(row));
  }

  /**
   * Updates relevant properties (text, enabled, htmlEnabled) from the page linked with the given row.
   * Only call this method if {@link TableRow#page} is set!
   *
   * @returns page linked with the given row.
   */
  updatePageFromTableRow(row: TableRow): Page {
    let page = row.page;
    page.enabled = row.enabled;
    page.text = page.computeTextForRow(row);
    if (row.cells.length) {
      page.htmlEnabled = row.cells[0].htmlEnabled;
      page.cssClass = row.cells[0].cssClass;
    }
    return page;
  }

  /**
   * This function creates the text property of this page. The default implementation returns the texts of the summary columns of the table or
   * from the first cell of the given row. It's allowed to ignore the given row entirely, when you override this function.
   */
  computeTextForRow(row: TableRow): string {
    const summaryColumns = row.getTable().summaryColumns();
    if (summaryColumns.length) {
      return strings.join(' ', ...summaryColumns.map(summaryColumn => summaryColumn.cellText(row)));
    }
    if (row.cells.length >= 1) {
      return row.cells[0].text;
    }
    return '';
  }

  /**
   * @returns a page parameter object used to pass to newly created child pages. Sets the parent
   *     to our outline instance and adds optional other properties. Typically, you'll pass an
   *     object (entity-key or arbitrary data) to a child page.
   */
  protected _pageParam<T extends object>(paramProperties?: T): T & { parent: Outline } {
    let param = {
      parent: this.getOutline()
    };
    $.extend(param, paramProperties);
    return param as T & { parent: Outline };
  }

  reloadPage() {
    let outline = this.getOutline();
    if (outline) {
      this.loadChildren();
    }
  }

  linkWithRow(row: TableRow) {
    this.row = row;
    row.page = this;
    this.getOutline().trigger('pageRowLink', {
      page: this,
      row: row
    });
  }

  unlinkWithRow(row: TableRow) {
    delete this.row;
    delete row.page;
  }

  protected _onTableFilter(event: Event<Table>) {
    this.getOutline().mediator.onTableFilter(event, this);
  }

  protected _onTableRowClick(event: TableRowClickEvent) {
    if (!this.drillDownOnRowClick) {
      return;
    }
    if (this.leaf) {
      return;
    }
    this.getOutline().drillDown(event.row.page);
    event.source.deselectRow(event.row);
  }

  setPageChanging(changing: boolean) {
    if (changing) {
      this.pageChanging++;
      return;
    }
    if (this.pageChanging) {
      this.pageChanging--;
    }
  }

  /**
   * Triggers a property change for a single property.
   */
  triggerPropertyChange<T>(propertyName: string, oldValue: T, newValue: T): PropertyChangeEvent<T, this> {
    scout.assertParameter('propertyName', propertyName);
    return this.trigger('propertyChange', {
      propertyName: propertyName,
      oldValue: oldValue,
      newValue: newValue
    }) as PropertyChangeEvent<T, this>;
  }

  trigger<K extends string & keyof EventMapOf<Page>>(type: K, eventOrModel?: Event<Page> | EventModel<EventMapOf<Page>[K]>): EventMapOf<Page>[K] {
    let event: Event<Page>;
    if (eventOrModel instanceof Event) {
      event = eventOrModel;
    } else {
      event = new Event(eventOrModel);
    }
    event.source = this;
    this.events.trigger(type, event);
    return event;
  }

  one<K extends string & keyof EventMapOf<this>>(type: K, handler: EventHandler<EventMapOf<this>[K] & Event<this>>) {
    this.events.one(type, handler);
  }

  on<K extends string & keyof EventMapOf<this>>(type: K, handler: EventHandler<(EventMapOf<this>)[K] & Event<this>>): EventListener {
    return this.events.on(type, handler);
  }

  off<K extends string & keyof EventMapOf<this>>(type: K, handler?: EventHandler<EventMapOf<this>[K]>) {
    this.events.off(type, handler);
  }
}

export type NodeType = EnumObject<typeof Page.NodeType>;
export type MenuOwner = Widget & { menus: Menu[]; setMenus: (menus: ObjectOrChildModel<Menu>[]) => void };
