/************************************************************************************/
import "./xregexp-all.js";
import { SystemCompendiums } from "../settings/system-compendiums.js";
import { RdDBaseActorReve } from "../actor/base-actor-reve.js";
import { Grammar } from "../grammar.js";
import { Misc } from "../misc.js";
import { ENTITE_INCARNE, ENTITE_NONINCARNE } from "../constants.js";
import { RdDItemTete } from "../documents/_module.mjs";
import { ITEM_TYPES } from "../constants.js";

const WHITESPACES = "\\s+"
const NUMERIC = "[\\+\\-]?\\d+"
const NUMERIC_VALUE = "(?<value>" + NUMERIC + ")"

const XREGEXP_COMP_CREATURE = WHITESPACES + "(?<carac>\\d+)"
  + WHITESPACES + NUMERIC_VALUE
  + "(" + WHITESPACES + "(?<init>\\d+)?\\s+?(?<dommages>[\\+\\-]?\\d+)?" + ")?"

// Skill parser depending on the type of actor
const compParser = {
  personnage: "(\\s+\\((?<special>[^\\)]+)\\))?(,\\s*\\p{Letter}+)*(\\s+(?<malus>avec armure))?" + WHITESPACES + NUMERIC_VALUE,
  creature: XREGEXP_COMP_CREATURE,
  entite: XREGEXP_COMP_CREATURE
}

const MANIEMENTS = {
  'de lancer': (weapon) => { return { name: weapon.system.lancer, categorie: 'lancer' } },
  'de jet': (weapon) => { return { name: weapon.system.lancer, categorie: 'lancer' } },
  'à une main': (weapon) => { return { name: weapon.system.competence, categorie: 'melee' } },
  'à deux mains': (weapon) => { return { name: weapon.system.competence.replace("à 1 main", "à 2 mains"), categorie: 'melee' } },
  'mêlée': (weapon) => { return { name: weapon.system.competence, categorie: 'melee' } },
}
const XREGEXP_WEAPON_MANIEMENT = "(?<maniement>(" + Misc.join(Object.keys(MANIEMENTS), '|') + "))"

const XREGEXP_SORT_VOIE = "(?<voies>[OHNT](\\/[OHNT])*)"
const XREGEXP_SORT_NAME = "(?<name>[^\\(]+)"
const XREGEXP_SORT_CASE = "(?<coord>([A-Za-zÀ-ÖØ-öø-ÿ\\s\\-]+|[A-M]\\d{1,2}))"

const XREGEXP_SORT = "(" + XREGEXP_SORT_VOIE
  + WHITESPACES + XREGEXP_SORT_NAME
  + WHITESPACES + "\\(" + XREGEXP_SORT_CASE + "\\)"
  + WHITESPACES + "R(?<diff>([\\-\\d]+|(\\w|\\s)+))"
  + WHITESPACES + "r(?<reve>(\\d+(\\+)?|\\s\\w+))"
  + "(" + WHITESPACES + "\\+(?<bonus>\\d+)\\s?%" + WHITESPACES + "en" + WHITESPACES + "(?<bonuscase>[A-M]\\d{1,2})" + ")?"
  + ")"

const XREGEXP_SORTRESERVE_CASE = "(?<coord>[A-M]\\d{1,2})";

const XREGEXP_SORT_RESERVE = XREGEXP_SORTRESERVE_CASE
  + WHITESPACES + XREGEXP_SORT_NAME
  + WHITESPACES + "(\\((?<description>[^\\)]+)\\))?"

// Main class for parsing a stat block
export class RdDStatBlockParser {

  static openInputDialog() {
    new foundry.applications.api.DialogV2({
      title: "Import de stats de PNJ/Créatures",
      content: `
        <div>
          <p>Coller le texte de la stat ici</p>
          <textarea id="statBlock"></textarea>
        </div>
      `,
      buttons: [
        {
          action: "ok", label: "OK",
          callback: async (event, button, dialog) => {
            let statBlock = dialog.element.querySelector("#statBlock").value;
            await RdDStatBlockParser.parseStatBlock(statBlock);
            dialog.close();
          }
        },
        { action: "cancel", label: "Cancel" }
      ]
    }).render({ force: true });
  }

  static fixWeirdPDF(statString) {
    // Split the statString into lines
    let lines = statString.split("\n");
    let newLines = [];
    let index = 0;
    let nextType = "string";
    // Loop through each line
    for (let i = 0; i < lines.length; i++) {
      // remove trailing spaces
      lines[i] = lines[i].trim();
      // Is it text ?      
      if (lines[i].match(/^[a-zA-Zéêè\s]+/)) {
        if (nextType == "string") {
          newLines[index] = lines[i];
          nextType = "number";
        } else {
          console.log("Wrong sequence string detected...", lines[i], nextType);
        }
      }
      // Is it a number ?
      if (lines[i].match(/^[\d\s]+/)) {
        if (nextType == "number") {
          newLines[index] = newLines[index] + lines[i];
          nextType = "string";
          index++;
        } else {
          console.log("Wrong sequence number detected...", lines[i], nextType);
        }
      }
    }
  }

  static getHeureKey(heure) {
    for (let h of game.system.rdd.config.heuresRdD) {
      if (h.label.toLowerCase() == heure.toLowerCase()) {
        return h.value;
      }
    }
    return "vaisseau";
  }

  static fixCompName(name) {
    name = name.replace("Voie d'", "");
    name = name.replace("Voie de ", "");
    return name
  }

  static async parseStatBlock(statString) {

    //statString = statBlock03;
    if (!statString) {
      return;
    }

    // Special function to fix strange/weird copy/paste from PDF readers
    // Unused up to now : this.fixWeirdPDF(statString);

    // Replace all endline by space in the statString
    statString = statString.replace(/\n/g, " ");
    // Remove all multiple spaces
    statString = statString.replace(/\s{2,}/g, " ");
    // Remove all leading and trailing spaces
    statString = statString.trim();

    // TODO: check for entite
    let type = RdDStatBlockParser.parseActorType(statString);

    // Now start carac 
    let actorData = foundry.utils.deepClone(game.model.Actor[type]);
    let items = [];

    actorData.flags = { hautRevant: false, malusArmure: 0, type }
    for (let key in actorData.carac) {
      let caracDef = actorData.carac[key];
      // Parse the stat string for each caracteristic
      let carac = XRegExp.exec(statString, XRegExp(caracDef.label + "\\s+(?<value>\\d+)", 'giu'));
      if (carac?.value) {
        actorData.carac[key].value = Number(carac.value);
      }
    }

    // If creature we need to setup additionnal fields 
    switch (type) {
      case "creature":
        RdDStatBlockParser.parseCreature(statString, actorData)
        await RdDStatBlockParser.parseCompetences(statString, actorData, items)
        break
      case "entite":
        RdDStatBlockParser.parseEntite(statString, actorData)
        await RdDStatBlockParser.parseCompetences(statString, actorData, items)
        break
      case "personnage":
        await RdDStatBlockParser.parseArmors(statString, actorData, items);
        await RdDStatBlockParser.parseCompetences(statString, actorData, items);
        await RdDStatBlockParser.parseWeapons(statString, items);
        await RdDStatBlockParser.parseHautReve(statString, actorData, items);
        RdDStatBlockParser.parsePersonnage(statString, actorData);
    }

    const name = RdDStatBlockParser.extractName(type, statString);

    actorData.flags = undefined
    console.log(actorData);

    let newActor = await RdDBaseActorReve.create({ name, type, system: actorData, items });
    await newActor.remiseANeuf()
    await RdDStatBlockParser.adjustAttacks(newActor)
    await RdDStatBlockParser.setValeursActuelles(newActor, statString)
    await newActor?.sheet.render(true)
  }

  static async parseCompetences(statString, actorData, items) {
    const competences = await SystemCompendiums.getCompetences(actorData.flags.type);
    //console.log("Competences : ", competences);
    for (let competence of competences) {
      let pushed = actorData.flags.type != "personnage"
      let compNameToSearch = RdDStatBlockParser.fixCompName(competence.name)
      XRegExp.forEach(statString, XRegExp("\\s" + compNameToSearch + compParser[actorData.flags.type], 'giu'),
        function (compMatch, i) {
          items.push(RdDStatBlockParser.prepareCompetence(actorData, competence, compMatch))
          if (!compMatch.special) {
            pushed = true
          }
        })
      if (!pushed) {
        // ajout niveau de base
        items.push(competence.toObject())
      }

    }
  }

  static prepareCompetence(actorData, competence, compMatch) {
    const comp = competence.toObject();
    if (compMatch.special) {
      comp._id = undefined
      comp.name = `${comp.name} (${compMatch.special})`
    }
    comp.system.niveau = Number(compMatch.value);
    if (compMatch.malus) {
      comp.system.niveau = Number(compMatch.value) - actorData.flags.malusArmure
    }
    if (comp.system.categorie == 'draconic' && comp.system.niveau > -11) {
      actorData.flags.hautRevant = true
    }
    if (["creature", "entite"].includes(actorData.flags.type)) {
      comp.system.carac_value = Number(compMatch.carac);
      if (compMatch.dommages != undefined) {
        comp.system.dommages = Number(compMatch.dommages)
        comp.system.iscombat = true
      }
    }
    return comp
  }

  static async parseArmors(statString, actorData, items) {
    const armors = await SystemCompendiums.getWorldOrCompendiumItems("armure", "equipement");
    for (let armor of armors) {
      let matchArmor = XRegExp.exec(statString, XRegExp(armor.name, 'giu'));
      if (matchArmor) {
        armor = armor.toObject()
        armor.system.equipe = true
        actorData.flags.malusArmure = armor.system.malus
        items.push(armor)
        break
      }
    }
  }

  static async parseWeapons(statString, items) {
    const weapons = await SystemCompendiums.getWorldOrCompendiumItems("arme", "equipement");
    //console.log("Equipement : ", equipment);
    // TODO: les noms d'armes peuvent avoir un suffixe (à une main, lancée) qui détermine la compétence correspondante
    // TODO: une arme peut être spécifique ("fourche"), ajouter une compétence dans ces cas là?
    for (let weapon of weapons) {
      let nomArmeManiement = XRegExp.exec(weapon.name, XRegExp(".*" + XREGEXP_WEAPON_MANIEMENT));
      if (nomArmeManiement) {
        continue // ignore les objets 'Dague de jet" ou "dague mêlée"
      }
      let weapMatch = XRegExp.exec(statString, XRegExp(weapon.name
        + "(\\s*" + XREGEXP_WEAPON_MANIEMENT + ")?"
        + "\\s+(?<value>[\\+\\-]?\\d+)", 'giu'));
      if (weapMatch) {
        weapon = weapon.toObject();
        weapon.system.equipe = 'true';
        items.push(weapon);

        const niveau = Number(weapMatch.value);
        // now process the skill
        if (weapMatch?.maniement) {
          RdDStatBlockParser.setNiveauCompetenceArme(items, MANIEMENTS[weapMatch.maniement](weapon), niveau)
        }
        else {
          RdDStatBlockParser.setNiveauCompetenceArme(items, { name: weapon.system.competence, categorie: 'melee' }, niveau)
          RdDStatBlockParser.setNiveauCompetenceArme(items, { name: weapon.system.tir, categorie: 'tir' }, niveau)
          RdDStatBlockParser.setNiveauCompetenceArme(items, { name: weapon.system.lancer, categorie: 'lancer' }, niveau)
        }
      }
    }
  }

  static setNiveauCompetenceArme(items, competence, niveau) {
    if (competence != "") {
      const item = items.find(i => i.system.categorie == competence.categorie && Grammar.equalsInsensitive(i.name, competence.name))
      if (item) {
        item.system.niveau = niveau
      }
    }
  }

  static async adjustAttacks(newActor) {
    if (["creature", "entite"].includes(newActor.type)) {
      const bonusDommages = newActor.getBonusDegat()
      const ajustementAttaques = newActor.itemTypes[ITEM_TYPES.competencecreature].filter(it => it.system.iscombat)
        .map(it => {
          return {
            _id: it.id,
            'system.categorie': 'melee',
            'system.dommages': it.system.dommages - bonusDommages
          }
        })
      await newActor.updateEmbeddedDocuments('Item', ajustementAttaques)
    }
  }

  static async setValeursActuelles(newActor, statString) {
    const updates = {
    }
    const endurance = XRegExp.exec(statString, XRegExp("endurance\\s+(?<value>\\d+)\\s+(\\(actuelle\\s*:\\s+(?<actuelle>\\d+)\\))?", 'giu'));
    if (endurance?.value) {
      if (newActor.getEnduranceMax() != endurance.value) {
        ui.notifications.warn(`Vérifier le calcul de l'endurance, calcul: ${newActor.getEnduranceMax()} / import: ${endurance.value}`)
      }
    }
    if (endurance?.actuelle) {
      updates['system.sante.endurance.value'] = Number(endurance?.actuelle)
    }

    const vie = XRegExp.exec(statString, XRegExp("vie\\s+(?<value>\\d+)\\s+(\\(actuelle\\s*:\\s+(?<actuelle>\\d+)\\))?", 'giu'));
    if (vie?.value) {
      if (newActor.getVieMax() != vie.value) {
        ui.notifications.warn(`Vérifier le calcul de la vie, calcul: ${newActor.getVieMax()} / import: ${vie.value}`)
      }
    }
    if (vie?.actuelle) {
      updates['system.sante.vie.value'] = Number(vie?.actuelle)
    }
    await newActor.update(updates)
  }

  static async parseHautReve(statString, actorData, items) {
    // Attemp to detect spell
    let sorts = await SystemCompendiums.getWorldOrCompendiumItems("sort", "sorts-oniros");
    sorts = sorts.concat(await SystemCompendiums.getWorldOrCompendiumItems("sort", "sorts-hypnos"));
    sorts = sorts.concat(await SystemCompendiums.getWorldOrCompendiumItems("sort", "sorts-narcos"));
    sorts = sorts.concat(await SystemCompendiums.getWorldOrCompendiumItems("sort", "sorts-thanatos"));

    XRegExp.forEach(statString, XRegExp(XREGEXP_SORT, 'gu' /* keep case sensitive to match the spell draconic skill */),
      function (matchSort, i) {
        actorData.flags.hautRevant = true
        const sortName = Grammar.toLowerCaseNoAccent(matchSort.name).trim().replace("’", "'");
        let sort = sorts.find(s => Grammar.toLowerCaseNoAccent(s.name) == sortName)
        if (sort) {
          sort = sort.toObject();
          if (matchSort.bonus && matchSort.bonuscase) {
            sort.system.bonuscase = `${matchSort.bonuscase}:${matchSort.bonus}`;
          }
          items.push(sort);
        }
        else {
          ui.notifications.warn(`Impossible de trouver le sort ${matchSort.name} / ${sortName}`)
        }
      })
    const sortsReserve = XRegExp.exec(statString, XRegExp('En réserve\\s+(?<reserve>.*)', 'gu' /* keep case sensitive to match the spell draconic skill */))
    if (sortsReserve?.reserve) {
      actorData.flags.hautRevant = true
      XRegExp.forEach(sortsReserve.reserve, XRegExp(XREGEXP_SORT_RESERVE, 'giu'),
        function (matchSortReserve, i) {
          const name = Grammar.toLowerCaseNoAccent(matchSortReserve.name).trim().replace("’", "'");
          const sort = sorts.find(s => Grammar.toLowerCaseNoAccent(s.name) == name)
          if (sort) {
            if (!items.find(it => it._id == sort.id)) {
              const nouveauSort = sort.toObject()
              nouveauSort.system.bonuscase = `${matchSortReserve.coord}:1`;
              items.push(sort.toObject())
            }
            items.push({
              name: sort.name,
              type: 'sortreserve',
              img: sort.img,
              system: {
                sortid: sort.id,
                draconic: sort.system.draconic,
                coord: matchSortReserve.coord,
                ptreve: Number(sort.system.ptreve.match(/\d+/)),
              },
              description: matchSortReserve.description
            })
          }
          else {
            ui.notifications.warn(`Impossible de mettre ${matchSortReserve.name} en réserve en ${matchSortReserve.coord}`)
          }
        })
    }

    if (actorData.flags.hautRevant) {
      const donHR = await RdDItemTete.teteDonDeHautReve();
      if (donHR) {
        items.push(donHR.toObject());
      }

      const demiReve = XRegExp.exec(statString, XRegExp("Demi-rêve\\s+(?<value>[A-M]\\d{1,2})", 'giu'))
      actorData.reve.tmrpos.coord = demiReve?.value ?? 'A1'
    }
  }

  static parsePersonnage(statString, actorData) {
    actorData.reve.seuil.value = actorData.carac.reve.value
    actorData.compteurs.chance.value = actorData.carac.chance.value

    const reveActuel = XRegExp.exec(statString, XRegExp("Rêve actuel\\s+(?<value>\\d+)", 'giu'))
    actorData.reve.reve.value = reveActuel?.value ? Number(reveActuel.value) : actorData.reve.seuil.value

    const feminin = XRegExp.exec(statString, XRegExp("né(?<value>e?) à", 'giu'));
    actorData.sexe = (feminin?.value == 'e') ? 'féminin' : 'masculin';

    // Get hour name : heure du XXXXX
    const heure = XRegExp.exec(statString, XRegExp("heure (du|de la|des|de l\')\\s*(?<value>[A-Za-zÀ-ÖØ-öø-ÿ\\s]+),", 'giu'));
    actorData.heure = this.getHeureKey(heure?.value || "Vaisseau");

    // Get age
    const age = XRegExp.exec(statString, XRegExp("(?<value>\\d+) ans", 'giu'));
    if (age?.value) {
      actorData.age = Number(age.value);
    }
    // Get height
    const taille = XRegExp.exec(statString, XRegExp("(?<value>\\d+m\\d+)", 'giu'));
    if (taille?.value) {
      actorData.taille = taille.value;
    }
    // Get weight
    const poids = XRegExp.exec(statString, XRegExp(",\\s+(?<value>\\d+)\\s+kg", 'giu'));
    if (poids?.value) {
      actorData.poids = poids.value + ' kg';
    }
    // Get cheveux
    const cheveux = XRegExp.exec(statString, XRegExp("kg,\\s+(?<value>[A-Za-zÀ-ÖØ-öø-ÿ\\s\\-]+),\\s+yeux", 'giu'));
    if (cheveux?.value) {
      actorData.cheveux = cheveux.value;
    }
    // Get yeux
    const yeux = XRegExp.exec(statString, XRegExp("yeux\\s+(?<value>[A-Za-zÀ-ÖØ-öø-ÿ\\s\\-]+), Beau", 'giu'));
    if (yeux?.value) {
      actorData.yeux = yeux.value;
    }

    // Get beauty
    const beaute = XRegExp.exec(statString, XRegExp("beauté\\s+(?<value>\\d+)", 'giu'));
    if (beaute?.value) {
      actorData.beaute = Number(beaute.value);
    }
  }

  static parseCreature(statString, actorData) {
    let protection = XRegExp.exec(statString, XRegExp("protection(\\s+naturelle)?\\s+(?<value>[\\-]?\\d+)", 'giu'));
    if (protection?.value) {
      actorData.attributs.protection.value = Number(protection.value);
    }
    let vitesse = XRegExp.exec(statString, XRegExp("vitesse\\s+(?<value>[\\d\\/]+)", 'giu'));
    if (vitesse?.value) {
      actorData.attributs.vitesse.value = vitesse.value;
    }
  }

  static parseEntite(statString, actorData) {
    actorData.definition.categorieentite = 'cauchemar'
    actorData.definition.typeentite = ENTITE_NONINCARNE
    let endurance = XRegExp.exec(statString, XRegExp("endurance\\s+(?<value>\\d+)", 'giu'));
    if (endurance?.value) {
      actorData.sante.endurance.value = Number(endurance.value);
      actorData.sante.endurance.max = Number(endurance.value);
      actorData.definition.typeentite = ENTITE_INCARNE
    }
    let vitesse = XRegExp.exec(statString, XRegExp("vitesse\\s+(?<value>[\\d\\/]+)", 'giu'));
    if (vitesse?.value) {
      actorData.attributs.vitesse.value = vitesse.value;
    }
  }

  static parseActorType(statString) {
    let force = XRegExp.exec(statString, XRegExp("Force\\s+(?<value>[\\+\\-]?\\d+)", 'giu'))
    let vue = XRegExp.exec(statString, XRegExp("Vue\\s+(?<value>[\\+\\-]?\\d+)", 'giu'))
    let perception = XRegExp.exec(statString, XRegExp("perception\\s+(?<value>\\d+)", 'giu'))
    if (!force) {
      return "entite"
    }
    if (!vue || perception) {
      return "creature"
    }
    return "personnage"
  }

  static extractName(actorType, statString) {
    if (actorType == "personnage") {
      // Check if ',né le' is present 
      let namePersonnage = "Importé"
      if (statString.includes(", né")) {
        // Name is all string before first comma ','
        namePersonnage = XRegExp.exec(statString, XRegExp("(?<value>[\\p{Letter}\'\\-\\s\\d]+),", 'giu'));
      } else {
        namePersonnage = XRegExp.exec(statString, XRegExp("(?<value>[\\p{Letter}\'\\-\\s\\d]+)\\s+TAILLE", 'giu'));
      }
      if (namePersonnage?.value) {
        return Misc.upperFirst(namePersonnage?.value.toLowerCase());
      }
    }

    const name = XRegExp.exec(statString, XRegExp("(?<value>.+)\\s+taille", 'giu'));
    if (actorType == "entite") {
      if (!(name?.value)) {
        const nameEntiteReve = XRegExp.exec(statString, XRegExp("(?<value>.+)\\s+rêve", 'giu'));
        return Misc.upperFirst(nameEntiteReve?.value || "Importé");
      }
    }
    return Misc.upperFirst(name?.value || "Importé");
  }

  static warning(message) {
    ui.notifications.warn(message);
  }

}

/************************************************************************************/
// Some internal test strings
let statBlock01 = `+$16(/, baron de Sylvedire, né à l’heure du
Roseau, 40 ans, 1m78, 65 kg, Beauté 13.
TAILLE
 10
 Mêlée
 14
APPARENCE
 13
 Tir
 11
CONSTITUTION
 12
 Lancer
 11
FORCE
 12
 Dérobée
 13
AGILITÉ
 16
 Vie
 11
DEXTÉRITÉ
 13
 Endurance
 25
VUE
 10
 +dom
 0
OUÏE
 11
 Protection
 2 ou 4
ODO-GOÛT
 9
 cuir souple
VOLONTÉ
 14
 ou cuir / métal
INTELLECT
 9
EMPATHIE
 11
RÊVE
 13
CHANCE
 10
niv
 init
 +dom
Épée dragonne
 +5
 12
 +3
Hache de bataille
 +6
 13
 +3
Bouclier moyen
 +5
Dague mêlée
 +4
 11
 +1
Corps à corps
 +4
 11
 (0)
Esquive
 +8
Escalade, Saut +4 / Commerce +3 / Équitation
+6 / Chirurgie 0 / Survie en extérieur +4 / Survie fo-
rêt +6 / Acrobatie -2 / Métallurgie +2 / Natation +3 /
Légendes -1 / Écriture -4 
`;

let statBlock02 = `/HVJDUGHV
TAILLE
 11
 Mêlée
 12
CONSTITUTION
 11
 Tir
 11
FORCE
 12
 Lancer
 11
AGILITÉ
 12
 Dérobée
 11
DEXTERITÉ
 11
 Vie
 11
VUE
 11
 Endurance
 22
OUÏE
 11
 Vitesse
 12
VOLONTÉ
 10
 +dom
 0
Protection
 4
cuir / métal
niv
 init
 +dom
Hache de bataille
 +4
 10
 +3
Bouclier moyen
 +4
Dague mêlée
 +3
 9
 +1
Arc
 +5
 10
 +2
Corps à corps
 +3
 9
 (0)
Esquive avec armure
 +2
Course +1/ Vigilance +4
`;

let statBlock03 = `rencontres sont laissées à /HVFKLHQVORXSVGXEDURQ
 chaque gardien des rêves.
TAILLE
 8
 Vie
 10
CONSTITUTION FORCE
 12
 11
 Endurance
 Vitesse
 12/38
 21
 /HVFKLHQV]RPELV
PERCEPTION 13
 +dom
 0
VOLONTÉ
 10
 Protection
 0
 Les « monstres » apparaîtront un soir, durant
RÊVE
 10
 l’heure du Serpent, et attaqueront les voya-
niv
 init
 +dom
 geurs à leur campement. Si ces derniers ne
Morsure
 13
 +4
 10
 +1
 campent pas, ils apparaîtront tout de même à
Esquive
 11
 +3
 l’heure du Serpent. Le feu ne les effraie pas. Ils
Course, Saut
 12
 +3
 ne sont pas très rapides, mais en revanche, très
Discrétion
 12
 +3
 silencieux : ils n’aboient pas. Les voyageurs
Vigilance
 13
 +3
`
