/*
 * Copyright (C) 2019 Robert Bosch GmbH Copyright (C) 2019 Robert Bosch Manufacturing Solutions GmbH, Germany. All rights reserved.
 */

import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { Column, ColumnItem, BciColumnBrowserModel } from './column-browser.model';

/**
 * Utility service for the creation and updating of the column browser's model. This service is used by
 * {@link ColumnBrowserComponent}.
 */
@Injectable()
export class ColumnBrowserService {
  /** All columns currently shown in the browser. */
  columns: Observable<Column[]>;

  private columnsSubject: BehaviorSubject<Column[]>;

  /** Current selection path. */
  private selectedPath: any[] = [];

  /** Titles of {@link ColumnBrowserService#columns all columns}. */
  private headers: string[] = [];

  constructor() {
    this.columnsSubject = new BehaviorSubject<Column[]>([]);
    this.columns = this.columnsSubject.asObservable();
  }

  /**
   * Initializes the column model for display based on the input model.
   * @param items Complete array of items.
   * @param headers
   * @param selectedPath The current selected path.
   */
  initColumns(items: BciColumnBrowserModel[], headers: string[], selectedPath: any[]): void {
    if (!items || items.length === 0) {
      this.selectedPath = [];
      this.columnsSubject.next([]);
      return;
    }

    this.headers = headers;
    this.selectedPath = selectedPath;
    this.prepareColumns(items, undefined);
    this.columnsSubject.next(this.columnsSubject.getValue());
  }

  /**
   * Removes the columns starting from the given {@param level column index}.
   * @param level From where the columns will be removed.
   */
  resetColumns(level: number): void {
    if (level < this.columnsSubject.value.length) {
      this.columnsSubject.value.splice(level);
      this.columnsSubject.next(this.columnsSubject.getValue());
    }
  }

  /**
   * Updates the complete column tree by:
   * 1. Updating the selection path starting from the {@param columnItem initial element} up to the columns root, and
   * 2. updating the column's items visibility starting from the column index of the {@param columnItem initial element}
   *    down into all remaining columns.
   *
   * @param columnItem The currently selected column.
   * @param level Current column index.
   * @param clearSelection Clears the current selection.
   */
  updateColumns(columnItem: ColumnItem, level: number, clearSelection = false): void {
    this.markSelectedPath(columnItem, level);
    this.prepareChildren(columnItem, level, clearSelection);
  }

  /**
   * Deselects all items of the column with the given index.
   * @param level Current column index
   */
  clearColumnSelection(level: number): void {
    if (level < this.columnsSubject.value.length) {
      this.columnsSubject.value[level].items.forEach((item) => {
        item.selected = false;
      });
    }
  }

  /**
   * Deselects the current {@param columnItem column item} and updates selection and visibility state to all children.
   * @param columnItem which was deselected
   * @param level column index from where to start
   */
  deselectColumnItem(columnItem: ColumnItem, level: number): void {
    let currentIndex = level;
    while (this.columnsSubject.value[currentIndex] != null) {
      this.columnsSubject.value[currentIndex].items.forEach((item) => {
        item.selected = false;
        item.visible =
          currentIndex === 0 ||
          (level === currentIndex && JSON.stringify(columnItem.parentItem) === JSON.stringify(item.parentItem));
      });
      currentIndex++;
    }
  }

  updateHeaders(headers: string[]): void {
    this.headers = headers;
    this.columnsSubject.value.forEach((column, index) => {
      column.title = this.headers[index];
    });
  }

  /**
   * Prepares the visibility of the child nodes.
   * @param columnItem Name of the current node.
   * @param level Level of the current node.
   * @param clearSelection Whether to forcefully clear the current selection.
   */
  private prepareChildren(columnItem: ColumnItem, level: number, clearSelection = false): void {
    let currentLevel = level;
    while (this.columnsSubject.value[currentLevel] != null) {
      if (this.columnsSubject.value[currentLevel + 1]) {
        this.columnsSubject.value[currentLevel + 1].items.forEach((item) => {
          item.visible = item.parentItem.item.model === columnItem.item.model;
          if (clearSelection) {
            item.selected = false;
          }
        });
      }
      currentLevel++;
    }
  }

  /** Creates and fills {@Column columns} based on {@BciColumnBrowserModel the external input}. */
  private prepareColumns(items: BciColumnBrowserModel[], parentItem?: any, level = 0): void {
    if (items.length > 0) {
      this.addEmptyColumn(level);
    }

    items.forEach((item) => {
      // JSON.stringify is used below because the object references might get invalid on this.selectedPath
      const selected =
        this.selectedPath && this.selectedPath[level]
          ? JSON.stringify(this.selectedPath[level]) === JSON.stringify(item.model)
          : false;
      const visible = level === 0 || (parentItem != null ? parentItem.visible && parentItem.selected : false);
      const columnItem = new ColumnItem(item, parentItem, item.label, selected, visible, item.children.length > 0);
      if (!this.columnsSubject.value[level].items.some((child) => child.item.model === columnItem.item.model)) {
        this.columnsSubject.value[level].items.push(columnItem);
      }

      // Create columns for the item's children
      this.prepareColumns(item.children, columnItem, level + 1);
    });
  }

  /** Adds an empty column at the given level. */
  private addEmptyColumn(level: number): void {
    if (this.columnsSubject.value[level] == null) {
      this.columnsSubject.value.push(new Column(this.headers[level], []));
    }
  }

  /**
   * Recursively sets the {@link ColumnItem#selected} state starting from the initial element until the root/first
   * column is reached.
   * @param columnItem Initial element.
   * @param level Column index of the initial element.
   */
  private markSelectedPath(columnItem: ColumnItem, level: number): void {
    this.columnsSubject.value[level].items.forEach((item) => {
      item.selected = item.item === columnItem.item;
      item.hasChildren = item.item.children.length > 0;
      if (level !== 0 && item.selected) {
        const parentItem = this.columnsSubject.value[level - 1].items.find(
          (parent) => parent.item === item.parentItem.item
        );
        if (parentItem != null) {
          this.markSelectedPath(parentItem, level - 1);
        }
      }
    });
  }
}
