// dtp-display-engine.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0

'use strict';

const DTP_COMPONENT = { name: 'Display Engine', slug: 'display-engine' };

import UIkit from 'uikit';
import DtpLog from './dtp-log.js';

export default class DtpDisplayEngine {

  constructor (app) {
    this.app = app;
    this.processors = { };
    this.log = new DtpLog(DTP_COMPONENT);
  }

  /**
   * Register a named Display List processor plugin. This lets client objects
   * define their own Display List processor classes and resiter them. Server
   * code can then execute actions on named processors in the client.
   * @param {*} name The name of the DL processor being registered.
   * @param {*} processor The custom DL processor being registered.
   */
  registerProcessor (name, processor) {
    this.processors[name] = processor;
  }

  /**
   * Unregisters a named Display List processing plugin.
   * @param {String} name The name of the DL processor to be unregistered.
   */
  unregisterProcessor (name) {
    if (!this.processors[name]) {
      return;
    }
    delete this.processors[name];
  }

  /**
   * Executes a Display List to implement view changes requested by the server.
   * These can arrive from an HTTP request, via socket.io, or generated in code
   * and passed in.
   * @param {DtpDisplayList} displayList 
   */
  async executeDisplayList (displayList) {
    try {
      displayList.commands.forEach((command) => {
        const processor = command.processor ? this.processors[command.processor] : this;
        this.log.debug(command.action, 'action', command);
        processor[command.action](displayList, command);
      });
    } catch (error) {
      this.log.error('executeDisplayList', 'failed to apply DisplayEngine updates', { error });
      UIkit.modal.alert(`Failed to apply updates: ${error.message}`);
    }
  }

  /*
   * action: addElement
   * selector: Specifies the container element to insertAdjacentHTML
   * where: 'beforebegin', 'afterbegin', 'beforeend', 'afterend'
   * html: the HTML content to insert at the container as specified
   */
  async addElement (displayList, command) {
    const container = document.querySelector(command.selector);
    if (!container) {
      console.debug('displayList.addElement has failed', { command });
      return;
    }
    container.insertAdjacentHTML(command.params.where, command.params.html);
  }

  async setTextContent (displayList, command) {
    const elements = document.querySelectorAll(command.selector);
    if (!elements || (elements.length === 0)) {
      this.log.error('setTextContent', 'failed to find target elements', { command });
      return;
    }
    elements.forEach((element) => {
      element.textContent = command.params.text;
    });
  }

  async setInputValue (displayList, command) {
    const elements = document.querySelectorAll(command.selector);
    if (!elements || (elements.length === 0)) {
      this.log.error('setInputValue', 'failed to find target elements', { command });
      return;
    }
    elements.forEach((element) => {
      element.value = command.params.value;
    });
  }

  /*
   * action: replaceElement
   * selector: Specifies the element to be replaced
   * html: replaces the whole specified element
   */
  async replaceElement (displayList, command) {
    const element = document.querySelector(command.selector);
    if (!element) {
      console.debug('displayList.replaceElement has failed to find requested element', { command });
      return;
    }
    element.outerHTML = command.params.html;
  }

  /*
   * action: removeElement
   * selector: Specifies the element(s) to be removed
   */
  async removeElement (displayList, command) {
    const elements = document.querySelectorAll(command.selector);
    if (!elements || !elements.length) {
      console.debug('displayList.removeElement has failed', { command });
      return;
    }
    elements.forEach((element) => {
      element.parentElement.removeChild(element);
    });
  }

  /*
   * action: setAttribute
   * selector: Specifies the element(s) for which an attribute's value should be set
   * name: the name of the attribute to be set
   * value: the value to be set on the named attribute
   */
  async setAttribute (displayList, command) {
    const elements = document.querySelectorAll(command.selector);
    if (!elements || !elements.length) {
      console.debug('displayList.setAttribute has failed', { command });
      return;
    }
    elements.forEach((element) => {
      element.setAttribute(command.params.name, command.params.value);
    });
  }

  /*
   * action: removeAttribute
   * selector: specifies the element(s) from which an attribute is to be removed
   * name: the name of the attribute to be removed
   */
  async removeAttribute (displayList, command) {
    const elements = document.querySelectorAll(command.selector);
    if (!elements || !elements.length) {
      console.debug('displayList.removeAttribute has failed', { command });
      return;
    }
    elements.forEach((element) => {
      element.removeAttribute(command.params.name);
    });
  }

  /*
   * action: addClass
   * selector: Specifies the element(s) for which style class(es) should be added
   * name: the class name to add to the classList
   */
  async addClass (displayList, command) {
    const elements = document.querySelectorAll(command.selector);
    if (!elements || !elements.length) {
      console.debug('displayList.addClass has failed', { command });
      return;
    }
    elements.forEach((element) => {
      element.classList.add(command.params.add);
    });
  }

  /*
   * action: removeClass
   * selector: Specifies the element(s) for which style class(es) should be removed
   * name: the class name to remove from the classList
   */
  async removeClass (displayList, command) {
    const elements = document.querySelectorAll(command.selector);
    if (!elements || !elements.length) {
      console.debug('displayList.removeClass has failed', { command });
      return;
    }
    elements.forEach((element) => {
      element.classList.remove(command.params.remove);
    });
  }

  /*
   * action: replaceClass
   * selector: Specifies the elements for which class replacement should occur
   * remove: the class name to remove from the classList
   * add: the class name to add to the classList
   */
  async replaceClass (displayList, command) {
    const elements = document.querySelectorAll(command.selector);
    if (!elements || !elements.length) {
      console.debug('displayList.replaceClass has failed', { command });
      return;
    }
    elements.forEach((element) => {
      element.classList.remove(command.params.remove);
      element.classList.add(command.params.add);
    });
  }

  async showNotification (displayList, command) {
    UIkit.notification(command.params);
  }

  async showModal (displayList, command) {
    this.app.currentModal = UIkit.modal.dialog(command.params.html);
  }

  async closeModal ( ) {
    if (!this.app.currentModal) {
      return;
    }
    this.app.currentModal.hide();
    delete this.app.currentModal;
  }

  async navigateTo (displayList, command) {
    window.location = command.params.href;
  }

  async reload ( ) {
    window.location.reload();
  }
}