import { Misc } from "../misc.js";
import { RollTypeComp } from "./roll-type-comp.mjs";
import { RollTypeTache } from "./roll-type-tache.mjs";
import { RollTypeAttaque } from "./roll-type-attaque.mjs";
import { RollTypeDefense } from "./roll-type-defense.mjs";
import { RollTypeMeditation } from "./roll-type-meditation.mjs";
import { RollTypeSort } from "./roll-type-sort.mjs";
import { RollTypeOeuvre } from "./roll-type-oeuvre.mjs";
import { RollTypeJeu } from "./roll-type-jeu.mjs";

import { RollPartAction } from "./roll-part-action.mjs";
import { RollPartActor } from "./roll-part-actor.mjs";
import { RollPartAppelMoral } from "./roll-part-appelmoral.mjs";
import { RollPartAstrologique } from "./roll-part-astrologique.mjs";
import { PART_CARAC, RollPartCarac } from "./roll-part-carac.mjs";
import { RollPartCoeur } from "./roll-part-coeur.mjs";
import { PART_COMP, RollPartComp } from "./roll-part-comp.mjs";
import { RollPartConditions } from "./roll-part-conditions.mjs";
import { RollPartDiff } from "./roll-part-diff.mjs";
import { RollPartEncTotal } from "./roll-part-enctotal.mjs";
import { RollPartEtat } from "./roll-part-etat.mjs";
import { RollPartEthylisme } from "./roll-part-ethylisme.mjs";
import { RollPartMalusArmure } from "./roll-part-malusarmure.mjs";
import { RollPartMeditation } from "./roll-part-meditation.mjs";
import { RollPartMoral } from "./roll-part-moral.mjs";
import { RollPartOpponent } from "./roll-part-opponent.mjs";
import { RollPartSurEnc } from "./roll-part-surenc.mjs";
import { PART_TRICHER, RollPartTricher } from "./roll-part-tricher.mjs";
import { RollPartTache } from "./roll-part-tache.mjs";
import { RollPartOeuvre } from "./roll-part-oeuvre.mjs";
import { RollPartSort } from "./roll-part-sort.mjs";
import { RollBasicParts } from "./roll-basic-parts.mjs";
import { RollPartRollMode } from "./roll-part-rollmode.mjs";
import { RollPartJeu } from "./roll-part-jeu.mjs";
import { RollPartSign } from "./roll-part-sign.mjs";
import { RollPartAttaque } from "./roll-part-attaque.mjs";
import { RollPartDefense } from "./roll-part-defense.mjs";
import { RollDialogAdapter } from "./roll-dialog-adapter.mjs";
import { ROLLDIALOG_SECTION } from "./roll-part.mjs";
import { ROLL_TYPE_COMP } from "./roll-constants.mjs";
import ChatRollResult from "./chat-roll-result.mjs";
import { renderTemplate } from "../constants.js";
import { RollTypeCuisine } from "./roll-type-cuisine.mjs";
import { RollPartCuisine } from "./roll-part-cuisine.mjs";
import { ActorImpacts } from "../technical/actor-impacts.mjs";
import { RollPartEmpoignade } from "./roll-part-empoignade.mjs";
import { RollPartEmpoignadeTaille } from "./roll-part-empoignade-taille.mjs";
import { RollPartEcailles } from "./roll-part-ecailles.mjs";
import { RollPartResistance } from "./roll-part-resistance.mjs";
import { RollTypePossession } from "./roll-type-possession.mjs";
import { RollPartPossession } from "./roll-part-possession.mjs";
import { RollPartApprecier } from "./roll-part-apprecier.mjs";
import { RollTypeAppelChance } from "./roll-type-appel-chance.mjs";
import { RollTypeReveResistance } from "./roll-type-resistance.mjs";


const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api

export const ALL_ROLL_TYPES = [
  new RollTypeComp(),
  new RollTypeTache(),
  new RollTypeAttaque(),
  new RollTypePossession(),
  new RollTypeDefense(),
  new RollTypeSort(),
  new RollTypeMeditation(),
  new RollTypeCuisine(),
  new RollTypeOeuvre(),
  new RollTypeJeu(),
  new RollTypeAppelChance(),
  new RollTypeReveResistance(),
  // new RollTypeFixedCarac ??
]

const ROLL_PARTS = [
  new RollPartActor(),
  new RollPartAction(),
  new RollPartOpponent(),
  new RollPartResistance(),
  new RollPartCarac(),
  new RollPartComp(),

  new RollPartApprecier(),
  new RollPartAttaque(),
  new RollPartPossession(),
  new RollPartDefense(),
  new RollPartMeditation(),
  new RollPartSort(),
  new RollPartTache(),
  new RollPartCuisine(),
  new RollPartOeuvre(),
  new RollPartJeu(),

  new RollPartDiff(),
  new RollPartConditions(),
  new RollPartEtat(),
  new RollPartSign(),
  new RollPartEncTotal(),
  new RollPartSurEnc(),
  new RollPartEthylisme(),
  new RollPartEcailles(),
  new RollPartMalusArmure(),
  new RollPartEmpoignadeTaille(),
  new RollPartEmpoignade(),
  new RollPartAppelMoral(),
  new RollPartMoral(),
  new RollPartCoeur(),
  new RollPartAstrologique(),
  new RollPartTricher(),
  new RollPartRollMode(),
]

/**
 * Extend the base Dialog entity to select roll parameters
 * @extends {Dialog}
 * # Principes
 * - une seule fenêtre de dialogue (classe RollDialog)
 * - plusieurs "types"s de fonctionnement (classe RollType)
 * - gestion uniforme des modificateurs (classe RollPart)
 * - un objet rollData contient les informations liées à un jet de dés
 * - un rollData doit pouvoir être "réduit" pour fournir les informations significatives
 *   d'un jet de dés
 * - un rollData réduit doit pouvoir être complété pour afficher la même fenêtre
 * - un rollData réduit sera utilisé pour piloter l'ouverture de la fenêtre
 *
 * - TODO: une classe de base RollChatMessage gerera les messages correspondant aux résultats du dés
 * - TODO: réfléchir aux messages supplémentaires gérés par PasseArme ?
 *
 * ## Types de fonctionnement - RollType
 * 
 * Un type de fonctionnement (RollType) détermine quelles parties (RollPart) de la
 * fenêtre RollDialog sont actives, mais aussi quels sont les effets du jet.
 * 
 * - chaque type de fonctionnement peut impacter les RollPart utilisés, les données
 *   attendues et ajoutées au rollData.
 * - chaque type de fonctionnement peut définir le template de ChatMessage correspondant
 * - Le type de fonctionnement détermine aussi quelles sont les effets du jet:
 *    - quelle ChatMessage afficher dans le tchat?
 *    - en cas d'attaque/de défense, quelles sont les suites à donner?
 *    - en cas de lancement de sort, réduire les points de rêve
 *    - en cas de méditation, créer le signe draconique
 *    - en cas de tâche, ajuster les points de tâche
 *
 *
 * ## Modificateurs - RollPart
 * - Chaque modificateur a:
 *    - un code (comp, carac, diff, ...)
 *    - une partie dédiée pour sauvegarder son contexte
 * - le contexte d'un RollPart est stocké dans le rollData de la fenêtre RollDialog, 
 *   dans des parties dédiés:
 *    - `rollData.refs[code]` pour les données de référentiel (liste de compétences, ...)
 *    - `rollData.current[code]` pour les informations d'état courante (la compétence sélectionnée, ...)
 *    - `rollData.selected[code]` pour les informations à sauvegarder, et utilisées pour paramétrer l'ouverture
 * - Chaque RollPart gère ses données dans cet espace dédié.
 * - Chaque RollPart a un sous-template dédié, et indique où il doit s'afficher dans le RollDialog
 * - Chaque RollPart peut enregistrer ses propres events handlers pour mettre à jour son contexte (et généralement réafficher le RollDialo)
 * - Chaque RollPart fournit les informations contextuelles associées au jet
 * - TODO: chaque RollPart peut fournir un sous-template pour le ChatMessage correspondant au résultat du dé.
 *
 * ## boucle de rétroaction
 * Lors de l'affichage, chaque RollPart peut fournir un filtre pour les autres RollParts.
 * Ce filtre sert principalement à filtrer les caractéristiques/compétense.
 *
 * Une fois ce filtrage effectué, chaque RollPart va pouvoir modifier sa partie du contexte
 * de la fenêtre, permettant à son template hbs d'avoir les donnéers à afficher.
 *
 * Enfin, lors de l'affichage (vu que les contrêles sont réaffichés), il peut 
 * enregistrer les listeners appropriés.
 *
 * ## Utilisation des informations sélectionnées
 *
 * Le rollData est la structure de stockage, et sert à préparer le jet de dé.
 * Le résultat du jet est stocké dans le noeud `rollData.rolled` (comme pour
 * la première version de jets de dés)
 *
 *
 * # TODO
 * - intégration pour un jet (oeuvres / tâches / méditation / compétence)
 * - RdDRollResult V2 (affichage avec templates basés sur roll-dialog)
 * - Extraction de jet résumé (pour appel chance)
 * - gestion significative
 * - Attaque
 * - Défense
 * - intégration rdd-combat
 * - combat rencontres
 *
 */
/* -------------------------------------------- */
export default class RollDialog extends HandlebarsApplicationMixin(ApplicationV2)
{
  static onCloseDoNothing() { }

  static onRollDoneDoNothing(dialog, roll) {
    if (roll.type.retry) {
      dialog.close()
    }
    else {
      dialog.render()
    }
  }

  static onRollDoneClose(dialog, roll) {
    dialog.close()
  }

  static init() { }

  static onReady() {

    foundry.applications.handlebars.loadTemplates({
      'roll-section': 'systems/foundryvtt-reve-de-dragon/templates/roll/roll-section.hbs',
      'roll-type': 'systems/foundryvtt-reve-de-dragon/templates/roll/roll-type.hbs',
      'roll-table': 'systems/foundryvtt-reve-de-dragon/templates/roll/roll-table.hbs',
      'roll-ajustements': 'systems/foundryvtt-reve-de-dragon/templates/roll/roll-ajustements.hbs',
      'roll-chances': 'systems/foundryvtt-reve-de-dragon/templates/roll/roll-chances.hbs',
      'roll-button': 'systems/foundryvtt-reve-de-dragon/templates/roll/roll-button.hbs',
    })

    ChatRollResult.onReady()

    foundry.applications.handlebars.loadTemplates(ALL_ROLL_TYPES.map(m => m.chatResultTemplate))
    foundry.applications.handlebars.loadTemplates(ROLL_PARTS.map(p => p.template))
    ROLL_PARTS.forEach(p => p.onReady(ROLL_PARTS))

    Handlebars.registerHelper('roll-centered-array', (base, show) => {
      show = Math.abs(show)
      const start = base - show
      return [...Array(2 * show + 1).keys()].map(it => start + it)
    })

    Handlebars.registerHelper('roll-list-item-value', (list, key, path = undefined) => {
      const selected = list.find(p => p.key == key)
      if (selected && path && path != '') {
        return foundry.utils.getProperty(selected, path)
      }
      return selected
    })

    Handlebars.registerHelper('roll-part-context', (rollData, code) => {
      const rollPart = ROLL_PARTS.find(it => it.code == code)
      if (rollPart == undefined) {
        return {}
      }
      return {
        code: code,
        name: rollPart.name,
        template: rollPart.template,
        rollData: rollData,
        refs: rollPart.getRefs(rollData),
        current: rollPart.getCurrent(rollData)
      }
    })
  }


  static async create(rollData, rollOptions = {}) {
    rollData = RollDialog.$prepareRollData(rollData)
    return Misc.doIfOwner(rollData.active.actor,
      actor => {
        const rollDialog = new RollDialog(rollData, rollOptions)
        rollDialog.render(true)
        return rollDialog
      }
    )
  }

  static get PARTS() {
    return { form: { template: 'systems/foundryvtt-reve-de-dragon/templates/roll/roll-dialog.hbs', } }
  }

  static get DEFAULT_OPTIONS() {
    const default_options = {
      tag: "form",
      form: {
        handler: RollDialog.handler,
        submitOnChange: false,
        closeOnSubmit: false
      },
      position: {
        width: 600,
        height: "auto",
      },
    }
    return default_options
  }

  static async handler(event, form, formData) {
    // rien pour l'instant
  }

  static getAllowedParts(rollData) {
    return rollData.type?.allowed
      ? ROLL_PARTS.filter(p => RollDialog.$isIntersecting(rollData.type.allowed, p.rollTypes))
      : ROLL_PARTS
  }

  /** pre-configure les paramètres des différentes parties de la fenêtre (par exemple, prépare les listes de caractéristiques/compétences */
  static $prepareRollData(rollData) {
    rollData.current = rollData.current ?? {}
    rollData.selected = rollData.selected ?? {}
    rollData.type = rollData.type ?? {}
    rollData.type.retry = rollData.type.retry ?? false
    RollBasicParts.restore(rollData)

    const potential = ALL_ROLL_TYPES.find(m => m.code == rollData.type.current)?.code
    const allowed = rollData.type.retry && potential
      ? [potential]
      : (rollData.type.allowed ?? ALL_ROLL_TYPES.filter(m => m.isAllowed(rollData) && m.visible(rollData)).map(m => m.code) ?? [ROLL_TYPE_COMP])
    const rollType = allowed.find(c => c == rollData.type.current) ?? allowed[0]
    const allowedRollParts = RollDialog.getAllowedParts(rollData)

    rollData.type.allowed = allowed
    rollData.type.current = rollType
    ALL_ROLL_TYPES.find(m => m.code == rollType).setRollDataType(rollData)

    rollData.refs = foundry.utils.mergeObject(rollData.refs ?? {}, Object.fromEntries(allowedRollParts.map(p => [p.code, {}])));
    rollData.options = rollData.options ?? { rollMode: game.settings.get("core", "messageMode") }

    allowedRollParts.forEach(p => RollDialog.$initializeRollPart(rollData, p.code))
    allowedRollParts
      .filter(p => p.isValid(rollData))
      .forEach(p => {
        p.restore(rollData)
        p.loadRefs(rollData)
        p.prepareContext(rollData)
      })
    return rollData
  }

  static $initializeRollPart(rollData, code) {
    rollData.refs[code] = rollData.refs[code] ?? {}
    rollData.current[code] = rollData.current[code] ?? {}
    rollData.selected[code] = rollData.selected[code] ?? {}
  }

  static $isIntersecting(allowed, included) { return included ? included.find(it => allowed.includes(it)) : true }

  static saveParts(rollData, impacts = undefined) {
    if (rollData == undefined) {
      return undefined
    }

    function saveBasics(from, to) {
      if (from) {
        to.ids = from.ids
        to.passeArme = from.passeArme
        to.rolled = from.rolled
        to.particuliere = from.particuliere
        to.result = from.result
        to.done = from.done ?? {}
        to.dmg = from.dmg
        if (from.attackerRoll) {
          to.attackerRoll = {}
          saveBasics(from.attackerRoll, to.attackerRoll)
        }
        if (from.current) {
          // stockage de current
          RollDialog.getAllowedParts(from)
            .filter(p => p.isActive(from))
            .forEach(p => p.storeClean(from, target))
        }
      }
    }

    const target = RollBasicParts.initFrom(rollData)
    saveBasics(rollData, target)
    if (impacts) {
      target.reverse = {
        active: impacts.active?.reverseImpacts(),
        opponent: impacts.opponent?.reverseImpacts()
      }
    }
    return target

  }

  constructor(rollData, rollOptions) {
    super()

    this.hooks = []
    this.rollData = rollData
    this.rollOptions = foundry.utils.mergeObject(rollOptions, {
      callbacks: [
        async r => await r.active.actor.ajoutExperience(r),
        async r => await r.active.actor.appliquerAppelMoral(r),
        ...(rollOptions.callbacks ?? [])
      ],
      onRollDone: rollOptions.onRollDone ?? RollDialog.onRollDoneDoNothing,
      onClose: rollOptions.onClose ?? RollDialog.onCloseDoNothing
    })
    this.chatRollResult = new ChatRollResult()
    this.rollParts = RollDialog.getAllowedParts(rollData)

    this.selectType()
    this.registerHooks(rollData)
  }

  registerHooks(rollData) {
    this.rollParts.filter(p => p.isValid(rollData))
      .forEach(p => p.getHooks(this).forEach(h => {
        const hook = h.hook;
        const id = Hooks.on(hook, h.fn)
        this.hooks.push({ hook, id })
      }))
  }

  unregisterHooks() {
    this.hooks.forEach(h => Hooks.off(h.hook, h.id))
  }


  selectType() {
    const selectedType = this.getSelectedType()
    selectedType.prepare(this.rollData)
    selectedType.setRollDataType(this.rollData)
    selectedType.onSelect(this.rollData)
    this.rollData.type.label = selectedType.title(this.rollData)

    this.rollParts.find(it => it.code == PART_CARAC).filterCaracs(this.rollData)
    this.rollParts.find(it => it.code == PART_COMP).filterComps(this.rollData)
  }

  getActiveParts() {
    return this.rollParts.filter(p => p.isActive(this.rollData))
  }

  rollTitle(rollData) {
    const title = rollData.label ?? ROLL_PARTS
      .filter(it => it.section == ROLLDIALOG_SECTION.ACTION)
      .filter(it => it.isActive(rollData))
      .map(it => it.title(rollData))
      .filter(it => it != undefined)
      .reduce(Misc.joining(' '));
    if (this.rollOptions.title) {
      return `${this.rollOptions.title}: ${title}`
    }
    return title
  }

  async _onRender(context, options) {
    this.window.title.innerText = this.rollTitle(this.rollData)
    const buttonRoll = this.element.querySelector(`button[name="roll-dialog-button"]`)
    buttonRoll?.addEventListener(
      "click", e => {
        e.preventDefault()
        this.roll()
      }
    )
    const buttonsType = this.element.querySelectorAll(`button[name="roll-type"]`)
    buttonsType?.forEach(it => it.addEventListener(
      "click", e => {
        e.preventDefault()
        this.rollData.type.current = e.currentTarget.dataset.type
        this.selectType()
        this.render()
      }
    ))

    Promise.all(
      this.getActiveParts().map(async p => await p._onRender(this, context, options))
    )
  }

  static getAjustements(rollData) {
    return RollDialog.getAllowedParts(rollData)
      .filter(p => p.isActive(rollData))
      .map(p => p.getAjustements(rollData))
      .reduce((a, b) => a.concat(b))
      .sort((a, b) => a.value == undefined ? 1 : b.value == undefined ? -1 : 0)
  }

  async buildHTMLTable(carac, diff) {
    return await renderTemplate('roll-table', { carac, diff })
  }

  async _prepareContext() {
    const rollData = this.rollData

    const types = ALL_ROLL_TYPES.filter(m => m.isAllowed(rollData) && m.visible(rollData))
      .map(m => m.toTypeData(rollData))
    RollBasicParts.loadSurprises(rollData, this.getSelectedType().code)
    rollData.type.label = this.getSelectedType()?.title(rollData)
    
    this.getActiveParts().forEach(p => p.applyExternalImpacts(this.getActiveParts(), rollData))
    this.setSpecialComp(this.getActiveParts());
    this.getActiveParts().forEach(p => p.prepareContext(rollData))

    RollDialog.calculAjustement(rollData)

    const templates = this.getActiveParts().map(p => p.toTemplateData())
    const context = await super._prepareContext()
    return foundry.utils.mergeObject(
      {
        types: types,
        templates: templates,
        rollData: rollData,
      }, context)
  }

  setSpecialComp(visibleRollParts) {
    const specialComp = visibleRollParts.map(p => p.getSpecialComp(this.rollData))
      .reduce((a, b) => a.concat(b))
    if (specialComp.length > 0) {
      const rollPartComp = this.getActiveParts().find(it => it.code == PART_COMP)
      rollPartComp?.setSpecialComp(this.rollData, specialComp)
    }
  }

  static calculAjustement(rollData) {
    rollData.ajustements = RollDialog.getAjustements(rollData)
    rollData.current.totaldiff = rollData.ajustements
      .filter(a => a.value != undefined)
      .map(a => a.value)
      .reduce(Misc.sum(), 0)
  }

  getSelectedType() {
    return ALL_ROLL_TYPES.find(m => m.code == this.rollData.type.current)
  }

  async close(options) {
    if (this.rollOptions.onClose) {
      this.rollOptions.onClose()
    }
    this.unregisterHooks()
    return await super.close(options)
  }

  async roll() {

    const roll = RollDialog.saveParts(this.rollData)
    RollDialog.loadRollData(roll)
    const selectedRollType = this.getSelectedType(roll);
    selectedRollType.onSelect(roll)
    roll.current.resultat = this.rollData.current[PART_TRICHER]?.resultat ?? -1
    roll.choix = {}
    roll.rolled = await RollDialogAdapter.rollDice(roll, this.rollTitle(roll))

    const impacts = new ActorImpacts(roll.active)

    roll.result = selectedRollType.getResult(roll, impacts)

    console.info('RollDialog.roll:', roll)

    const callbacks = [
      ...selectedRollType.callbacks(this.rollOptions),
      ...this.rollOptions.callbacks,
    ]

    for (let callback of callbacks) {
      await callback(roll)
    }

    await impacts.applyImpacts()
    selectedRollType.onApplyImpacts(roll, impacts)
    await this.chatRollResult.display(roll, impacts)
    this.rollOptions.onRollDone(this, roll)
  }

  static loadRollData(roll) {
    RollDialog.$prepareRollData(roll)
    if (roll.attackerRoll) {
      RollDialog.$prepareRollData(roll.attackerRoll)
    }
    RollDialog.calculAjustement(roll)
    roll.v2 = true
    return roll
  }

  async defaultCallback(roll, rolled) {
    await roll.active.actor.ajoutExperience(roll)
    await roll.active.actor.appliquerAppelMoral(roll)
  }

}
