import { WrapperObject } from '../../../../mel_metapage/js/lib/BaseObjects/WrapperObject.js';
import {
  BnumMessage,
  eMessageType,
} from '../../../../mel_metapage/js/lib/classes/bnum_message.js';
import { MelEnumerable } from '../../../../mel_metapage/js/lib/classes/enum.js';
import { FramesManager } from '../../../../mel_metapage/js/lib/classes/frame_manager.js';
import { EMPTY_STRING } from '../../../../mel_metapage/js/lib/constants/constants.js';
import { isNullOrUndefined } from '../../../../mel_metapage/js/lib/mel.js';
import { BnumEvent } from '../../../../mel_metapage/js/lib/mel_events.js';
import { MelObject } from '../../../../mel_metapage/js/lib/mel_object.js';
import { NavBarManager } from './navbar.generator.js';
import { WorkspaceModuleBlock } from '../WebComponents/workspace_module_block.js';

export { WorkspaceObject, CurrentWorkspaceData };

/**
 * Contient les classes et fonctionnalités éssentiels pour une application lié à un espace.
 * @module Workspace/Object
 * @local OnActionReceivedCallback
 * @local MethodCallback
 * @local WorkspaceObject
 * @local CurrentWorkspaceData
 * @local WorkspaceObjectData
 * @local WorkspaceUser
 * @local WorkspaceUsers
 */

/**
 * @callback OnActionReceivedCallback
 * @param {WorkspaceObjectData} data
 * @returns {void}
 */

/**
 * @callback MethodCallback
 * @return {void}
 */

/**
 * @class
 * @classdesc Contient les données et fonctions utiles pour les classes/modules qui implémentent les espaces de travail
 * @extends MelObject
 * @abstract
 * @hideconstructor
 */
class WorkspaceObject extends MelObject {
  constructor() {
    super();
  }

  /**
   * Contient le code de la fonction
   * @virtual
   * @override
   */
  main() {
    super.main();

    /**
     * Appelé lorsqu'une frame de l'espace envoie des données à l'espace via {@link WorkspaceObject.SendToWorkspace}
     * @type {BnumEvent<OnActionReceivedCallback>}
     * @event
     * @frommodule Workspace/Object {@linkto OnActionReceivedCallback}
     */
    this.onactionreceived = new BnumEvent();

    this.rcmail().addEventListener('workspace.object.call', (obj) => {
      this.onactionreceived.call(obj);
    });
  }

  /**
   * Indique au module qu'il a fini son chargement
   */
  loadModule() {
    this.main.loaded = true;
  }

  /**
   * Etat du module si il a fini de chargé ou non.
   * @type {boolean}
   * @readonly
   */
  get loaded() {
    return this.main.loaded ?? false;
  }

  /**
   * Données de l'espace en cours
   * @type {CurrentWorkspaceData}
   * @readonly
   */
  get workspace() {
    return workspaceData.Instance;
  }

  /**
   * Récupère le module d'un espace via son id.
   * @param {string} id Id du module à récupérer
   * @returns {WorkspaceModuleBlock}
   * @protected
   */
  _p_module_block(id) {
    return document.querySelector(`#${id}`);
  }

  /**
   * Permet un changement de frame de l'espace de travail
   * @param {string} page Tâche
   * @param {Object} [param1={}] Arguments qui permettra à la frame de changer de page au besoin
   * @param {?string} [param1.action=null] Action de la nouvelle frame. `null` par défaut
   * @param {Object<string, *>} [param1.newArgs={}] Arguments de l'url. Object vide par défaut.
   * @returns {Promise<void>}
   * @async
   */
  async switch_workspace_page(page, { action = null, newArgs = {} } = {}) {
    NavBarManager.currentNavBar.select(page, { background: true });

    if (!newArgs) newArgs = {};

    if (action) newArgs['_action'] = action;

    if (!Object.keys(newArgs).length) newArgs = null;

    await NavBarManager.SwitchPage(page, {
      workspace: this.workspace,
      manualConfig: newArgs,
    });
  }

  /**
   * Effectue un appel api lié à la tâche des espaces de travail
   * @param {string} action Action que l'on souhaite "taper".
   * @param {Object<string, string>} [params={}] Paramètres de l'url
   * @param {string} [type='GET'] Type d'appel
   * @returns {Promise<?any>}
   * @async
   */
  async http_workspace_call(action, params = {}, type = 'GET') {
    params ??= {};
    params._uid = this.workspace.uid;

    let data = null;
    let errored = false;
    await this.http_internal_call({
      task: 'workspace',
      on_success: (sData) => {
        data = sData;
      },
      on_error: (...args) => {
        errored = args;
      },
      params,
      action,
      type,
    });

    if (errored) throw errored;

    return data;
  }

  /**
   * Effectue un appel api "POST" lié à la tâche des espaces de travail
   * @param {string} action Action que l'on souhaite "taper".
   * @param {Object<string, string>} [params={}] Paramètres de l'url
   * @returns {Promise<?any>}
   */
  async http_workspace_post(action, params = {}) {
    return await this.http_workspace_call(action, params, 'POST');
  }

  /**
   * Récupère un paramètre d'un espace de travail
   * @param {string} key Clé du paramètre
   * @param {Object<string, string>} [params={}] Paramètres de l'url
   * @returns {Promise<?any>}
   */
  async http_workspace_param_post(key, params = {}) {
    params ??= {};
    params._key = key;
    return await this.http_workspace_post('param', params);
  }

  /**
   *
   * @param {Object} [param0={}]
   * @returns {WorkspaceModuleBlock}
   */
  createModuleElement({
    title = null,
    button = null,
    buttonText = null,
    buttonIcon = null,
  } = {}) {
    let node = document.createElement('bnum-workspace-module');

    if (title) node.setAttribute('data-title', title);

    if (button) {
      node.setAttribute('data-button', button);

      if (buttonText) node.setAttribute('data-button-text', buttonText);

      if (buttonIcon) node.setAttribute('data-button-icon', buttonIcon);
    }

    return node;
  }

  /**
   * Change l'état d'un module
   * @param {string} task Module à changer d'état
   * @param {boolean} state Nouvel état
   * @param {WorkspaceModuleBlock} container Container du module
   * @return {Promise<void>}
   * @async
   */
  async switchState(task, state, container) {
    if (state) this.hideBlock(container);
    else this.showBlock(container);

    await this.http_workspace_post('update_module_visibility', {
      _key: task,
      _state: state,
    });

    BnumMessage.DisplayMessage(
      'Visibilitée changée avec succès',
      eMessageType.Confirmation,
    );
  }

  /**
   * Vérifie si un des modules de l'espace est désactivé ou non
   * @param {string} task Module à vérifier
   * @returns {boolean}
   */
  isDisabled(task) {
    return [true, 'true'].includes(
      this.get_env('workspace_modules_visibility')?.[task],
    );
  }

  /**
   * Cache le block et aggrandit les autres
   * @param {WorkspaceModuleBlock} module
   * @returns {this} Chaînage
   */
  hideBlock(module) {
    module.parentElement.style.display = 'none';
    const children = MelEnumerable.from(
      module.parentElement.parentElement.children,
    ).where((x) => x.style.display !== 'none');

    for (const element of children) {
      element.setAttribute(
        'data-initial-classes',
        element.getAttribute('class'),
      );
      element.classList.remove(...element.classList.values());

      element.classList.add(`col-${12 / children.count()}`);
    }

    return this;
  }

  /**
   * Affiche un block et affiche correctement les autres blocks
   * @param {WorkspaceModuleBlock} module
   * @returns {this} Chaînage
   */
  showBlock(module) {
    module.parentElement.style.display = EMPTY_STRING;
    const children = MelEnumerable.from(
      module.parentElement.parentElement.children,
    ).where((x) => x.style.display !== 'none');

    //Il n'y a pas d'éléments cachés
    if (
      children.count() === module.parentElement.parentElement.children.length
    ) {
      for (const element of children.where((x) =>
        x.hasAttribute('data-initial-classes'),
      )) {
        element.setAttribute(
          'class',
          element.getAttribute('data-initial-classes'),
        );
        element.removeAttribute('data-initial-classes');
      }
    } else {
      //Il reste des éléments cachés
      for (const element of children) {
        if (!element.hasAttribute('data-initial-classes')) {
          element.setAttribute(
            'data-initial-classes',
            element.getAttribute('class'),
          );
        }
        element.classList.remove(...element.classList.values());

        element.classList.add(`col-${12 / children.count()}`);
      }
    }

    return this;
  }

  /**
   * Ajoute un callback lors du refresh de la frame
   * @param {MethodCallback} callback
   * @return {this} Chaînage
   * @override
   */
  on_refresh(callback) {
    this.rcmail().addEventListener('mel_metapage_refresh', callback);
    return this;
  }

  /**
   * Récupère les données de l'espace en cours
   * @returns {CurrentWorkspaceData}
   * @static
   */
  static GetWorkspaceData() {
    return workspaceData.Instance;
  }

  /**
   * Envoie des données à la frame parente
   * @param {string} key Clé qui permettra de récupérer les données dans la frame parente
   * @param {*} data Données à envoyer
   * @param {Object} [options={}]
   * @param {?string} [options.task=null] Ne pas utiliser.
   * @returns {boolean} Si des messages ont été envoyé à une frame parente, alors `true` sera renvoyé.
   * @static
   * @todo Rendre la tâche utile
   */
  static SendToParent(key, data, { task = null } = {}) {
    let rtn = false;

    if (parent !== window && !!parent) {
      parent.postMessage({ key, data, task });
      rtn = true;
    }

    return rtn;
  }

  /**
   * Envoie des données à la frame des espaces de travail
   * @param {string} key
   * @param {*} data Données à envoyer
   * @param {Object} [options={}]
   * @param {?string} [options.task=null] Ne pas utiliser.
   * @returns {boolean} Si des messages ont été envoyé, alors `true` sera renvoyé.
   * @static
   * @todo Rendre la tâche utile
   */
  static SendToWorkspace(key, data, { task = null } = {}) {
    const frame = FramesManager.Instance.get_frame('workspace', {
      jquery: false,
    });

    if (frame) {
      frame.contentWindow.postMessage({ key, data, task });
      return true;
    }

    return false;
  }
}

/**
 * @class
 * @classdesc Objet qui contient les données envoyé par une frame enfante qui implémente un {@link WorkspaceObject}
 * @package
 */
class WorkspaceObjectData {
  #workspace = null;
  #data = null;
  #context = null;
  #key = null;
  /**
   * Assigne les arguments aux variable privés
   * @param {CurrentWorkspaceData} workspace Données de l'espace
   * @param {string} key Clé de la données
   * @param {Object} [options={}]
   * @param {?any} [options.data=null] Données envoyer par la frame enfant
   * @param {Window | undefined} [options.context=window] Context de la frame qui a envoyer les données
   */
  constructor(workspace, key, { data = null, context = window } = {}) {
    this.#workspace = workspace;
    this.#data = data;
    this.#context = context;
    this.#key = key;
  }

  /**
   * Clé des données
   * @type {string}
   * @readonly
   */
  get key() {
    return this.#key;
  }

  /**
   * Données de l'espace
   * @type {CurrentWorkspaceData}
   * @readonly
   */
  get workspace() {
    return this.#workspace;
  }

  /**
   * Données envoyé par la frame enfant
   * @type {?any}
   * @readonly
   */
  get data() {
    return this.#data;
  }

  /**
   * Context de la frame enfant
   * @type {Window | undefined}
   * @readonly
   * @deprecated Ne fonctionne pas
   */
  get context() {
    return this.#context;
  }

  /**
   * Fonctions d'aide
   * @type {EmptyMelObject}
   * @readonly
   */
  get helper() {
    return MelObject.Empty();
  }
}

/**
 * @class
 * @classdesc Données d'un utilisateur
 * @package
 */
class WorkspaceUser {
  #email = null;
  #name = null;
  #fullname = null;
  #external = false;
  /**
   * Fractionne à partir d'une donnée brute
   * @param {{email:string, name:string, fullname:string, is_external:boolean}} user Utilisateur brute à convertir en données structurée
   */
  constructor(user) {
    this.#email = user.email;
    this.#name = user.name;
    this.#fullname = user.fullname;
    this.#external = user.is_external;
  }

  /**
   * Email de l'utilisateur
   * @type {string}
   * @readonly
   */
  get email() {
    return this.#email;
  }

  /**
   * Nom de l'utilisateur
   * @type {string}
   * @readonly
   */
  get name() {
    return this.#name;
  }

  /**
   * Nom complet de l'utilisateur
   * @type {string}
   * @readonly
   */
  get fullname() {
    return this.#fullname;
  }

  /**
   * Si il s'agit d'un utilisateur externe ou non
   * @type {boolean}
   * @readonly
   */
  get external() {
    return this.#external;
  }
}

/**
 * @class
 * @classdesc Liste des utilisateurs d'un espace.
 * @package
 */
class WorkspaceUsers {
  #users = {};
  /**
   * Récupère la liste des utilisateurs
   * @param  {...WorkspaceUser} users
   * @frommoduleparam  Workspace/Object users {@linkto WorkspaceUser}
   */
  constructor(...users) {
    for (const element of users) {
      this.#users[element.email] = element;
    }
  }

  /**
   * Liste des adresses emails des utilisateurs
   * @type {string[]}
   * @readonly
   */
  get emails() {
    return Object.keys(this.#users);
  }

  /**
   * Récupère les utilisateurs sous la forme d'un tableau
   * @returns {WorkspaceUser[]}
   */
  toArray() {
    return [...this];
  }

  /**
   * Convertit la liste des utilisateurs en énumerable
   * @returns {MelEnumerable}
   * @frommodulereturn MelLinq
   */
  toEnumerable() {
    return new MelEnumerable(this.generator.bind(this));
  }

  /**
   * Récupère Un utilisateur à partir de son email
   * @param {string} email
   * @returns {WorkspaceUser}
   */
  get(email) {
    return this.#users[email];
  }

  /**
   * Enumère les utilisateurs
   * @yields {WorkspaceUser}
   */
  *generator() {
    for (const key of Object.keys(this.#users)) {
      yield this.#users[key];
    }
  }

  /**
   * Enumère les utilisateurs
   * @yields {WorkspaceUser}
   */
  *[Symbol.iterator]() {
    yield* this.generator();
  }
}

/**
 * @class
 * @classdesc Données de l'espace
 * @package
 */
class CurrentWorkspaceData {
  #users = null;
  #workspace_created = null;
  /**
   * Constructeur de la classe.
   *
   * Parse "current_workspace_services_actives" en objet si c'est une chaîne de charactères.
   */
  constructor() {
    if (typeof rcmail.env.current_workspace_services_actives === 'string')
      rcmail.env.current_workspace_services_actives = JSON.parse(
        rcmail.env.current_workspace_services_actives,
      );
  }

  /**
   * Id de l'espace de travail
   * @type {string}
   * @readonly
   */
  get uid() {
    return rcmail.env.current_workspace_uid;
  }

  /**
   * Titre de l'espace
   * @type {string}
   * @readonly
   */
  get title() {
    return rcmail.env.current_workspace_title;
  }

  /**
   * Utilisateurs de l'espace
   * @type {WorkspaceUsers}
   * @readonly
   */
  get users() {
    if (!this.#users) {
      this.#users = new WorkspaceUsers(
        ...MelEnumerable.from(rcmail.env.current_workspace_users).select(
          (x) => new WorkspaceUser(x.value),
        ),
      );
    }
    return this.#users;
  }

  /**
   * Si l'espace est public ou non
   * @type {boolean}
   * @readonly
   */
  get isPublic() {
    return rcmail.env.current_workspace_is_public;
  }

  /**
   * Couleur de l'espace
   * @type {string}
   * @readonly
   */
  get color() {
    return rcmail.env.current_workspace_color;
  }

  /**
   * Liste des services de l'espace
   * @type {Object<string, string | Object<string, any> | boolean | number>}
   * @readonly
   */
  get services() {
    return rcmail.env.current_workspace_services_actives;
  }

  /**
   * Date de création de l'espace
   * @type {external:moment}
   * @readonly
   */
  get created() {
    if (!this.#workspace_created)
      this.#workspace_created = moment(rcmail.env.current_workspace_created);

    return this.#workspace_created;
  }

  /**
   * Récupère l'information si une application est chargée ou non
   * @param {string} service
   * @returns {boolean}
   */
  app_loaded(service) {
    return (
      this.services[service] &&
      (rcmail.env.current_workspace_services_enabled?.[service] !== false ||
        isNullOrUndefined(
          rcmail.env.current_workspace_services_enabled?.[service],
        ))
    );
  }

  reloadUsers() {
    this.#users = null;
    return this;
  }
}

/**
 * Singleton des données de l'espace pour éviter de recréer un objet à chaque fois.
 * @type {WrapperObject<CurrentWorkspaceData>}
 * @constant
 * @package
 * @frommodule Workspace/Object {@linkto CurrentWorkspaceData}
 */
const workspaceData = new WrapperObject(CurrentWorkspaceData);

/**
 * Propriété de l'objet "window" à ajouter pour indiquer que le listener "message" à déjà été ajouter.
 * @type {string}
 * @constant
 * @package
 */
const window_prop_data = 'wspobjlsn';
if (!window[window_prop_data]) {
  window.addEventListener(
    'message',
    (event) => {
      if (true) {
        rcmail.triggerEvent(
          'workspace.object.call',
          new WorkspaceObjectData(
            WorkspaceObject.GetWorkspaceData(),
            event.data.key,
            {
              data: event.data.data,
              context: event.data.task
                ? FramesManager.Instance.get_frame(event.data.task, {
                    jquery: false,
                  })?.contentWindow
                : undefined,
            },
          ),
        );
      }
    },
    false,
  );

  window[window_prop_data] = true;
}