function capitalize(str) {
  if (!str) return "";
  return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
}

/**
 * Checks if all elements in array are true;
 */
function arrayOfTruth(array) {
  return array.every(element => element === true);
}

/**
 * Returns label for given key found in object containing keys and label pairs. 
 */
function getLabelFromKey(key, labels) {
  if (!labels) return key;
  let label = labels[key];
  if (label) return label;
  else return key;
}

/**
 * Returns value under given path for given object.
 * Example path: human.bodyParts.head.nose
 */
function getValueFromPath(object, pathToValue) {
  for (var i=0, pathToValue=pathToValue.split('.'), length=pathToValue.length; i<length; i++){
    if (object === undefined || object === null) return;
    object = object[pathToValue[i]];
    if (object === undefined) return;
  }  return object;
}

function setValueForPath(object, path, value) {
  const keys = path.split('.');
  let currentObject = object;

  for (let i = 0; i < keys.length - 1; i++) {
    const key = keys[i];

    // If the key doesn't exist in the current object, create an empty object
    currentObject[key] = currentObject[key] || {};
    currentObject = currentObject[key];
  }

  // Set the value at the final key
  currentObject[keys[keys.length - 1]] = value;
}

async function toggleUpOrDown(pathToValue, which, object, upperLimit, lowerLimit) {
  let value = getValueFromPath(object, pathToValue);

  switch (which) {
    case 1: 
      value = Math.min(++value, upperLimit);
      break;
    case 3: 
      value = Math.max(--value, lowerLimit);
      break;
  }
  await object.update({[pathToValue] : value});
}

/**
 * Changes boolean property to opposite value.
 */
async function changeActivableProperty(pathToValue, object){
  let value = getValueFromPath(object, pathToValue);
  if (value === undefined) value = false;
  await object.update({[pathToValue] : !value});
}

/**
 * Changes numeric value for given path.
 */
function changeNumericValue(value, pathToValue, object) {
  let changedValue = parseInt(value);
  if (isNaN(changedValue)) changedValue = 0;
  // if (changedValue < 0) changedValue = 0;

  object.update({[pathToValue] : changedValue});
}

/**
 * Changes value for given path.
 */
function changeValue(value, pathToValue, object) {
  object.update({[pathToValue] : value});
}

function generateKey() {
  return foundry.utils.randomID();
}

function parseFromString(string) {
  if (string === undefined) return undefined;
  if (string === null) return null;
  if (string.startsWith('"') && string.endsWith('"')) string = string.substring(1, string.length-1);
  if (string.startsWith("'") && string.endsWith("'")) string = string.substring(1, string.length-1);
  if (string === "") return string;
  if (string === "true") return true;
  if (string === "false") return false;
  if (!isNaN(Number(string))) return Number(string);
  return string;
}

function mapToObject(map) {
  const object = {};
  map.forEach((value, key) => object[key] = value);
  return object;
}

function translateLabels(object) {
  for (const key in object) {
    if (object.hasOwnProperty(key)) {
      const value = object[key];
      
      if (key === "label") object[key] = game.i18n.localize(object.label) ?? object.label;
      if (typeof value === "object" && value !== null) translateLabels(value);
    }
  }
}

function isPointInPolygon(x, y, polygon) {
  let isInside = false;
  for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
    const xi = polygon[i].x, yi = polygon[i].y;
    const xj = polygon[j].x, yj = polygon[j].y;

    // Check if the point is on the edge or crosses
    const intersect = ((yi > y) !== (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
    if (intersect) isInside = !isInside;
  }
  return isInside;
}

function isPointInSquare(x, y, square) {
  const minX = square.x1y1.x;
  const maxX = square.x2y1.x;
  const minY = square.x1y1.y;
  const maxY = square.x1y2.y;

  if (x < minX || x > maxX) return false;
  if (y < minY || y > maxY) return false;
  return true;
}

function getPointsOnLine(x1, y1, x2, y2, interval) {
  const points = [];
  
  const dx = x2 - x1;
  const dy = y2 - y1;
  const totalDistance = Math.sqrt(dx * dx + dy * dy);
  
  const unitVectorX = dx / totalDistance;
  const unitVectorY = dy / totalDistance;

  // Add points along the line at specified intervals
  for (let d = 0; d <= totalDistance; d += interval) {
      const newX = x1 + unitVectorX * d;
      const newY = y1 + unitVectorY * d;
      points.push({ x: newX, y: newY });
  }
  return points;
}

function roundFloat(float) {
  return Math.round(float * 10)/10;
}

function addNewAreaToItem(item) {
  const key = generateKey();
  item.update({[`system.target.areas.${key}`]: {
    area: "",
    distance: null,
    width: null,
    unit: "",
    difficult: false,
    hideHighlight: false
  }});
}

function removeAreaFromItem(item, key) {
  item.update({[`system.target.areas.-=${key}`]: null});
}

function runWeaponLoadedCheck(item) {
  const reloadProperty = item.system?.properties?.reload;
  if (reloadProperty && reloadProperty.active) {
    if (reloadProperty.loaded) return true;
    else {
      let errorMessage = `You need to reload that weapon first!`;
      ui.notifications.error(errorMessage);
      return false;
    }
  }
  return true;
}

async function reloadWeapon(item, actor) {
  const reloadProperty = item.system?.properties?.reload;
  if (reloadProperty && reloadProperty.active) {
    if (!reloadProperty.loaded) {
      if (subtractAP(actor, 1)) {
        await item.update({[`system.properties.reload.loaded`]: true});
      }
    }
  }
}

function unloadWeapon(item, actor) {
  const usesWeapon = item.system?.usesWeapon;
  if (usesWeapon && usesWeapon.weaponAttack) {
    const weapon = actor.items.get(usesWeapon.weaponId);
    if (weapon) weapon.update({[`system.properties.reload.loaded`]: false});
  }
  else {
    item.update({[`system.properties.reload.loaded`]: false});
  }
}

/**
 * This functions check if item has toggleable property set to true if so it checks item specific condition
 * ex. linkWithToggle property is set to true. 
 * 
 * If both conditions are met it returns value of toggledOn field.
 * If any is false it will always return true because item does not care about toggle in that case.
 */
function toggleCheck(item, itemSpecificCondition) {
  if (item.system.toggle?.toggleable && itemSpecificCondition) return item.system.toggle.toggledOn;
  return true;
}

/**
 * Returns dataset extracted from event's currentTarget.
 * Also calls preventDefault method on event.
 */
function datasetOf(event) {
  event.preventDefault();
  return event.currentTarget.dataset;
}

/**
 * Returns value of event's currentTarget.
 * Also calls preventDefault method on event.
 */
function valueOf(event) {
  event.preventDefault();
  return event.currentTarget.value;
}

function activateDefaultListeners(app, html) {
  const _onActivable = (path) => {
    const value = getValueFromPath(app, path);
    setValueForPath(app, path, !value);
    app.render();
  };
  const _onValueChange = (path, value) => {
    setValueForPath(app, path, value);
    app.render();
  };
  const _onNumericValueChange = (path, value, nullable) => {
    let numericValue = parseInt(value);
    if (nullable && isNaN(numericValue)) numericValue = null;
    setValueForPath(app, path, numericValue);
    app.render();
  };

  html.find('.activable').click(ev => _onActivable(datasetOf(ev).path));
  html.find(".selectable").change(ev => _onValueChange(datasetOf(ev).path, valueOf(ev)));
  html.find(".input").change(ev => _onValueChange(datasetOf(ev).path, valueOf(ev)));
  html.find(".numeric-input").change(ev => _onNumericValueChange(datasetOf(ev).path, valueOf(ev)));
  html.find(".numeric-input-nullable").change(ev => _onNumericValueChange(datasetOf(ev).path, valueOf(ev), true));
}

function addToMultiSelect(object, path, key, value) {
  if (!key) return;
  object.update({[`${path}.${key}`]: value});
}

function removeMultiSelect(object, path, key) {
  object.update({[`${path}.-=${key}`]: null});
}

class TokenSelector extends Dialog {

  constructor(tokens, label, dialogData = {}, options = {}) {
    super(dialogData, options);
    this.tokens = tokens;
    this.label = label;
  }

  static get defaultOptions() {
    return foundry.utils.mergeObject(super.defaultOptions, {
      template: "systems/dc20rpg/templates/dialogs/token-selector.hbs",
      classes: ["dc20rpg", "dialog"]
    });
  }

  getData() {
    return {
      tokens: this.tokens,
      label: this.label
    }
  }

   /** @override */
  activateListeners(html) {
    super.activateListeners(html);
    html.find('.confirm-selection').click(ev => this._onConfirm(datasetOf(ev)));
    html.find('.activable').click(ev => this._onActivable(datasetOf(ev).path));
    html.find('.ping-token').click(ev => this._onPingToken(datasetOf(ev).id));
  }

  _onActivable(path) {
    let value = getValueFromPath(this, path);
    setValueForPath(this, path, !value);
    this.render(true);
  }

  _onPingToken(id) {
    const token = this.tokens[id];
    if (token) canvas.ping({x: token.center.x, y: token.center.y});
  }

  async _onConfirm() {
    const selectedTokens = [];
    Object.values(this.tokens).forEach((token) => {
      if (token.selectedToken) selectedTokens.push(token);
    });
    this.promiseResolve(selectedTokens);
    this.close();
  }

  static async create(tokens, label, dialogData = {}, options = {}) {
    const dialog = new TokenSelector(tokens, label, dialogData, options);
    return new Promise((resolve) => {
      dialog.promiseResolve = resolve;
      dialog.render(true);
    });
  }

  /** @override */
  close(options) {
    if (this.promiseResolve) this.promiseResolve([]);
    super.close(options);
  }
}

async function getTokenSelector(tokens, label) {
  return await TokenSelector.create(tokens, label, {title: "Token Selector"});
}

function getTokenForActor(actor) {
  if (actor.isToken) return actor.token.object;
  else {
    const tokens = canvas.tokens.placeables.filter(token => token.actor?.id === actor.id);
    return tokens[0];
  }
}

function getAllTokensForActor(actor) {
  if (actor.isToken) return [actor.token.object];
  else {
    const tokens = canvas.tokens.placeables.filter(token => token.actor?.id === actor.id);
    return tokens;
  }
}

/**
 * Returns an array of currently selected tokens by user.
 */
function getSelectedTokens() {
  if (canvas.activeLayer === canvas.tokens) return canvas.activeLayer.placeables.filter(p => p.controlled === true);
}

function getTokensInsideMeasurementTemplate(template, dispositions=[]) {
  if (!template) return {};
  const tokens = canvas.tokens.placeables;
  if (!tokens) return {};
  
  const tokensInTemplate = {};
  for (const token of tokens) {
    if (_isTokenInsideTemplate(token, template)) {
      if (dispositions.length > 0) {
        if (dispositions.includes(token.document.disposition)) {
          tokensInTemplate[token.id] = token;
        }
      }
      else {
        tokensInTemplate[token.id] = token;
      }
    }
  }
  return tokensInTemplate;
}

function _isTokenInsideTemplate(token, template) {
  // Gridless Mode
  if (canvas.grid.isGridless) {
    const shape = template._getGridHighlightShape();
    const points = getGridlessTokenPoints(token);

    // Circle
    if (shape.type === 2) {
      const startX = template.document.x;
      const startY = template.document.y;
      const radius = shape.radius;

      for (let i = 0; i < points.length; i++) {
        const x = points[i].x;
        const y = points[i].y;
        const distanceSquared = (x - startX) ** 2 + (y - startY) ** 2;
        if (distanceSquared <= radius ** 2) return true;
      }
      return false;
    }
    // Ray
    if (shape.type === 0) {
      const shapePoints = shape.points;
      const startX = template.document.x;
      const startY = template.document.y;

      // Collect points related to starting position
      const polygon = [];
      for (let i = 0; i < shapePoints.length; i=i+2) {
        const x = startX + shapePoints[i];
        const y = startY + shapePoints[i+1];
        polygon.push({x: x, y: y});
      }

      for (let i = 0; i < points.length; i++) {
        const x = points[i].x;
        const y = points[i].y;
        if (isPointInPolygon(x, y, polygon)) return true;
      }
      return false;
    }
  }
  // Grid Mode
  else {
    const highlightedSpaces = template.highlightedSpaces;
    const tokenSpaces = token.getOccupiedGridSpaces();
    // If at least one token space equal highlighted one we have a match 
    // Should we change it to some % of all token occupied spaces?
    for (let i = 0; i < highlightedSpaces.length; i++) {
      for (let j = 0; j < tokenSpaces.length; j++) {
        const horizontal = highlightedSpaces[i][0] === tokenSpaces[j][0];
        const vertical = highlightedSpaces[i][1] === tokenSpaces[j][1];
        if (horizontal && vertical) return true;
      }
    }
    return false;
  }
}

function getGridlessTokenPoints(token) {
  // We want to collect some points inside a token so we can 
  // check later if any of those fit our measurement template
  const startX = token.x;
  const startY = token.y;
  const endX = startX + token.w;
  const endY = startY + token.h;

  // We assume quarter of the grid size should be enough to match most our cases
  const step = canvas.grid.size/4;
  const tokenPoints = [];
  for (let x = startX; x < endX; x=x+step) {
    for (let y = startY; y < endY; y=y+step) {
      tokenPoints.push({x: x, y: y});
    }
  }
  return tokenPoints;
}

function getGridlessTokenCorners(token) {
  const height = token.getSize().height;
  const width = token.getSize().width;

  return {
    x1y1: {x: token.x, y: token.y},
    x2y1: {x: token.x + width, y: token.y},
    x1y2: {x: token.x, y: token.y + height},
    x2y2: {x: token.x + width, y: token.y + height},
  }
}

function getRangeAreaAroundGridlessToken(token, distance) {
  const rangeArea = getGridlessTokenCorners(token);
  const sizeX = canvas.grid.sizeX;
  const sizeY = canvas.grid.sizeY;

  rangeArea.x1y1.x -= distance * sizeX + (0.1 * sizeX);
  rangeArea.x1y1.y -= distance * sizeY + (0.1 * sizeY);
  rangeArea.x1y2.x -= distance * sizeX + (0.1 * sizeX);
  rangeArea.x1y2.y += distance * sizeY + (0.1 * sizeY);
  rangeArea.x2y1.x += distance * sizeX + (0.1 * sizeX);
  rangeArea.x2y1.y -= distance * sizeY + (0.1 * sizeY);
  rangeArea.x2y2.x += distance * sizeX + (0.1 * sizeX);
  rangeArea.x2y2.y += distance * sizeY + (0.1 * sizeY);
  
  return rangeArea;
}

function getActorFromIds(actorId, tokenId) {
  let actor = game.actors.tokens[tokenId];        // Try to find unlinked actors first
  if (!actor) actor = game.actors.get(actorId);   // Try to find linked actor next
  return actor;
}

async function updateActorHp(actor, updateData) {
  if (updateData.system?.resources?.health) {
    const newHealth = updateData.system.resources.health;
    const actorsHealth = actor.system.resources.health;
    const maxHp = actorsHealth.max;
    const currentHp = actorsHealth.current;
    const tempHp = actorsHealth.temp || 0;

    // When value (temporary + current hp) was changed
    if (newHealth.value !== undefined) {
      const newValue = newHealth.value;
      const oldValue = actorsHealth.value;
  
      // Heal
      if (newValue >= oldValue) {
        const preventHpRegen = actor.system.globalModifier.prevent.hpRegeneration;
        if (preventHpRegen) {
          ui.notifications.error('You cannot regain any HP');
          delete updateData.system.resources.health;
          return updateData;
        }

        const newCurrentHp = Math.min(newValue - tempHp, maxHp);
        const newTempHp = newValue - newCurrentHp > 0 ? newValue - newCurrentHp : null;
        newHealth.current = newCurrentHp;
        newHealth.temp = newTempHp;
        newHealth.value = newCurrentHp + newTempHp;
      }
      // Damage
      else {
        const valueDif = oldValue - newValue;
        const remainingTempHp = tempHp - valueDif;
        if (remainingTempHp <= 0) { // It is a negative value we want to subtract from currentHp
          newHealth.temp = null;
          newHealth.current = currentHp + remainingTempHp; 
          newHealth.value = currentHp + remainingTempHp;
        }
        else {
          newHealth.temp = remainingTempHp;
          newHealth.value = currentHp + remainingTempHp;
        }
      }
    }

    // When only temporary HP was changed
    else if (newHealth.temp !== undefined) {
      const preventHpRegen = actor.system.globalModifier.prevent.hpRegeneration;
      if (preventHpRegen) {
        ui.notifications.error('You cannot regain any HP');
        delete updateData.system.resources.health;
        return updateData;
      }
      newHealth.value = newHealth.temp + currentHp;
    }

    // When only current HP was changed
    else if (newHealth.current !== undefined) {
      const preventHpRegen = actor.system.globalModifier.prevent.hpRegeneration;
      if (preventHpRegen) {
        ui.notifications.error('You cannot regain any HP');
        delete updateData.system.resources.health;
        return updateData;
      }
      newHealth.current = newHealth.current >= maxHp ? maxHp : newHealth.current;
      newHealth.value = newHealth.current + tempHp;
    }
    updateData.system.resources.health = newHealth;
  }
  return updateData;
}

function displayScrollingTextOnToken(token, text, color) {
  canvas.interface.createScrollingText(token.center, text, {
    anchor: CONST.TEXT_ANCHOR_POINTS.BOTTOM,
    fontSize: 32,
    fill: color,
    stroke: "#000000",
    strokeThickness: 4,
    jitter: 0.25
  });
}

/**
 * Called when new actor is being created, makes simple pre-configuration on actor's prototype token depending on its type.
 */
function preConfigurePrototype(actor) {
  const prototypeToken = actor.prototypeToken;
  prototypeToken.displayBars = 20;
  prototypeToken.displayName = 20;
  if (actor.type === "character" || actor.type === "companion") {
    prototypeToken.actorLink = true;
    prototypeToken.disposition = 1;
  }
  actor.update({['prototypeToken'] : prototypeToken});
}

async function evaluateFormula(formula, rollData, skipDiceDisplay=false) {
  formula = _replaceWithRollDataContent(formula, rollData);
  const roll = new Roll(formula, rollData);
  await roll.evaluate();
  // Making Dice so Nice display that roll - it slows down that method alot, so be careful with that 
  if (!skipDiceDisplay && game.dice3d) await game.dice3d.showForRoll(roll, game.user, true, null, false);
  return roll;
}

/**
 * Evaluates given roll formula. 
 * If {@param ignoreDices} is set to true, all dice rolls will be 0.
 */
function evaluateDicelessFormula(formula, rollData={}) {
  if (formula === "") return 0;

  formula = _replaceWithRollDataContent(formula, rollData);
  formula = _enchanceFormula(formula);
  const roll = new Roll(formula, rollData);
  
  // Remove dices
  roll.terms.forEach(term => {
    if (term.faces) term.faces = 0;
  });
  
  roll.evaluateSync({strict: false});
  return roll;
}

function _enchanceFormula(formula) {
  return formula.replace(/(^|\D)(d\d+)(?!\d|\w)/g, "$11$2");
}

function _replaceWithRollDataContent(formula, rollData) {
  const bracketRegex = /\[[^\]]*\]/g; 
  formula = formula.replace(bracketRegex, (match) => {
    match = match.slice(1,-1); // remove [ ]
    const pathValue = getValueFromPath(rollData, match);
    if (pathValue) return pathValue;
    else return match;
  });
  return formula;
}

class SimplePopup extends Dialog {

  constructor(popupType, data, dialogData = {}, options = {}) {
    super(dialogData, options);
    this.popupType = popupType;
    this.data = data;
  }

  static get defaultOptions() {
    return foundry.utils.mergeObject(super.defaultOptions, {
      template: "systems/dc20rpg/templates/dialogs/simple-popup.hbs",
      classes: ["dc20rpg", "dialog", "force-top"]
    });
  }

  getData() {
    if (this.popupType === "info" || this.popupType === "drop") {
      const information = this.data.information; 
      if (information && information.constructor !== Array) {
        this.data.information = [this.data.information];
      }
    }
    return {
      ...this.data,
      popupType: this.popupType
    }
  }

   /** @override */
  activateListeners(html) {
    super.activateListeners(html);
    html.find('.confirm-input-all').click(ev => this._onConfirmAll(html.find(".input-popup-selector"), datasetOf(ev)));
    html.find('.confirm-input').click(ev => this._onConfirm(html.find(".input-popup-selector").val(), datasetOf(ev)));
    html.find('.confirm-select').click(ev => this._onConfirm(html.find(".select-popup-selector").val(), datasetOf(ev)));
    html.find('.confirm-yes').click(ev => this._onConfirm(true, datasetOf(ev)));
    html.find('.confirm-no').click(ev => this._onConfirm(false, datasetOf(ev)));
    if(this.popupType === "drop") html[0].addEventListener('drop', async ev => await this._onDrop(ev));
  }

  async _onConfirmAll(element) {
    const values = [];
    element.each(function() {values.push($(this).val()); });
    this.promiseResolve(values);
    this.close();
  }

  async _onConfirm(outome) {
    this.promiseResolve(outome);
    this.close();
  }

  static async create(popupType, data={}, dialogData = {}, options = {}) {
    const prompt = new SimplePopup(popupType, data, dialogData, options);
    return new Promise((resolve) => {
      prompt.promiseResolve = resolve;
      prompt.render(true);
    });
  }

  /** @override */
  close(options) {
    if (this.promiseResolve) this.promiseResolve(null);
    super.close(options);
  }

  async _onDrop(event) {
    event.preventDefault();
    const droppedData  = event.dataTransfer.getData('text/plain');
    if (!droppedData) return;
    
    const droppedObject = JSON.parse(droppedData);
    if (droppedObject.type !== "Item") return;

    this.promiseResolve(droppedObject.uuid);
    this.close();
  }
}

/**
 * Creates simple dialog for player that triggers it. Calling method can await for results of that dialog.
 * There are few popupType options to use when creating that dialog, deppending on the type data object might differ:
 * - "info" - data = {header: String, information: Array[String]} - display some information to the caller
 * - "select" - data = {header: String, selectOptions: Object} - caller can select one of the options that will be returned by dialog
 * - "input" - data = {header: String} - caller can provide text that will be returned by dialog
 * - "confirm" - data = {header: String} - caller can confirm or deny, result will be returned by dialog
 */
async function getSimplePopup(popupType, data={}) {
  return await SimplePopup.create(popupType, data, {title: "Popup"});
}

/**
 * Creates simple dialog for players with specific userIds[Array]. It will wait only for the first answer.
 * For more information take a look at getSimplePopup documentation
 */
async function sendSimplePopupToUsers(userIds, popupType, popupData={}) {
  const payload = {
    popupType: popupType,
    popupData: popupData,
    userIds: userIds
  };
  const validationData = {emmiterId: game.user.id};
  const simplePopupResult = responseListener("simplePopupResult", validationData);
  emitSystemEvent("simplePopup", payload);
  const response = await simplePopupResult;
  return response;
}

//========================
//       CONVERTERS      =
//========================
/**
 * Converts tokens to targets used by chat message or damage calculation.
 */
function tokenToTarget(token) {
  const actor = token.actor;
  const statuses = actor.statuses.size > 0 ? Array.from(actor.statuses) : [];
  const rollData = actor?.getRollData();
  const target = {
    name: actor.name,
    img: actor.img,
    id: token.id,
    isOwner: actor.isOwner,
    system: actor.system,
    statuses: statuses,
    effects: actor.allEffects,
    isFlanked: token.isFlanked,
    rollData: {
      target: {
        numberOfConditions: _numberOfConditions(actor.coreStatuses),
        system: rollData
      }
    }
  };
  return target;
}

function targetToToken(target) {
  return canvas.tokens.documentCollection.get(target.id);
}

function _numberOfConditions(coreStatuses) {
  let number = 0;
  const conditions = CONFIG.DC20RPG.DROPDOWN_DATA.conditions;
  for (const status of coreStatuses) {
    if (conditions[status]) number += 1;
  }
  return number;
}

//========================
//      CALCULATIONS     =
//========================
function getAttackOutcome(target, data) {
  if (!data.isAttack || !target) return {};
  if (!data.hit) {
    const defence = target.system.defences[data.defenceKey].value;
    data.hit = data.rollTotal - defence;
  }

  const outcome = {};
  if (data.isCritMiss || data.hit < 0) outcome.miss = true;
  outcome.label = _outcomeLabel(data.hit, data.isCritHit, data.isCritMiss, data.skipFor);
  outcome.defenceKey = data.defenceKey;
  return outcome;
}

/**
 * Returns final damage calculated for specific target. Includes DR, resitances, crits and all other modifications.
 * To convert token to target take a look at tokenToTarget documentation.
 * "formulaRoll" = {
 *    "clear": {
 *      "value": Number,
 *      "source": String,
 *      "type": String (ex. "fire")
 *    },
 *    "modified": {
 *      "value": Number,
 *      "source": String,
 *      "type": String (ex. "fire"),
 *      "each5Value": "Number",
 *      "failValue": Number
 *    }
 * }
 * "data" - note: not every field is required = {
 *    "isAttack": Boolean,
 *    "isCheck": Boolean,
 *    "canCrit": Boolean,
 *    "halfDmgOnMiss": Boolean,
 *    "isCritHit": Boolean,
 *    "isCritMiss": Boolean,
 *    "isDamage": Boolean,
 *    "isHealing": Boolean,
 *    "defenceKey": String(ex. "precision"),
 *    "hit": Number,
 *    "rollTotal": Number,
 *    "skipFor": {
 *      "heavy": Boolean,
 *      "brutal": Boolean,
 *      "crit": Boolean,
 *      "conditionals": Boolean,
 *    },
 *    "conditionals": Array
 * }
 */
function calculateForTarget(target, formulaRoll, data) {
  const final = {
    clear: formulaRoll.clear,
    modified: formulaRoll.modified,
  };
  // We might change that data so we need to copy it
  const dr = target ? {...target.system.damageReduction} : null; 
  const hr = target ? {...target.system.healingReduction} : null;

  // 0. For Crit Miss it is always 0
  if (data.isCritMiss) {
    final.modified = _applyCritFail(final.modified, data.isAttack);
    final.clear = _applyCritFail(final.clear, data.isAttack);
    return final;
  }

  // 1.A. If Attack Calculate hit value 1st
  if (data.isAttack && !data.hit) {
    const defence = target.system.defences[data.defenceKey].value;
    data.hit = data.rollTotal - defence;
  }

  // 1.B. If Check Calculate degree of success
  if (data.isCheck && data.againstDC && data.checkDC) {
    _degreeOfSuccess$1(data.rollTotal, data.isCritMiss, data.checkDC, final);
  }
  
  // 2.Collect conditionals with matching condition
  data.conditionals = target ? _matchingConditionals(target, data) : [];

  // 3. Collect modifications from conditionals
  const condFlags = _modificationsFromConditionals(data.conditionals, final, target, data.skipFor?.conditionals);
  
  // 4. Apply only Attack Roll Modifications
  if (data.isAttack && data.isDamage) final.modified = _applyAttackRollModifications(data.hit, final.modified, data.skipFor);
  
  // 5. Apply Crit Success
  const canCrit = data.canCrit && !data.skipFor?.crit;
  final.modified = _applyCritSuccess(final.modified, data.isCritHit, canCrit);

  // 6. Apply Flat Damage/Healing Reduction (X)
  if (data.isDamage) final.modified = _applyFlatReduction(final.modified, dr.flat, "Damage");
  if (data.isHealing) final.modified = _applyFlatReduction(final.modified, hr.flat, "Healing");

  // 7. Apply PDR, EDR and MDR to Resistances
  if (data.isAttack && data.isDamage) _applyDamageReduction(data.hit, final.modified, dr, condFlags.ignore);

  // 8. Apply Vulnerability, Resistance and DR
  if (data.isDamage) final.modified = _applyDamageModifications(final.modified, dr, condFlags.ignore);
  if (data.isDamage) final.clear = _applyDamageModifications(final.clear, dr, condFlags.ignore);

  // 9. Apply Flat Damage/Healing Reduction (Half)
  if (data.isDamage) final.modified = _applyFlatReductionHalf(final.modified, dr.flatHalf, "Damage");
  if (data.isDamage) final.clear = _applyFlatReductionHalf(final.clear, dr.flatHalf, "Damage");
  if (data.isHealing) final.modified = _applyFlatReductionHalf(final.modified, hr.flatHalf, "Healing");
  if (data.isHealing) final.clear = _applyFlatReductionHalf(final.clear, hr.flatHalf, "Healing");

  // 10. Prevent negative values
  final.modified = _finalAdjustments(final.modified);
  final.clear = _finalAdjustments(final.clear);

  // 9. Determine what should happened on Attack Miss 
  if (data.isAttack && data.isDamage && data.hit < 0) {
    if (data.halfDmgOnMiss) {
      final.modified = _applyHalfDamageOnMiss(final.modified);
      final.clear = _applyHalfDamageOnMiss(final.clear);
    }
    else {
      final.modified = _applyAttackMiss(final.modified);
      final.clear = _applyAttackMiss(final.clear);
    }
  }
  return final;
}

function calculateNoTarget(formulaRoll, data) {
  const final = {
    clear: formulaRoll.clear,
    modified: formulaRoll.modified,
  };

  // 0. For Crit Miss it is always 0
  if (data.isCritMiss) {
    final.modified = _applyCritFail(final.modified, data.isAttack);
    final.clear = _applyCritFail(final.clear, data.isAttack);
    return final;
  }

  // 1. If Check Calculate degree of success
  if (data.isCheck && data.againstDC && data.checkDC) {
    _degreeOfSuccess$1(data.rollTotal, data.isCritMiss, data.checkDC, formulaRoll);
  }

  // 2. Apply Crit Success
  const canCrit = data.canCrit && !data.skipFor?.crit;
  final.modified = _applyCritSuccess(final.modified, data.isCritHit, canCrit);

  // 3. Prevent negative values
  final.modified = _finalAdjustments(final.modified);
  final.clear = _finalAdjustments(final.clear);

  return final;
}

function _degreeOfSuccess$1(checkValue, natOne, checkDC, final) {
  const modified = final.modified;

  // Check Failed
  if (natOne || (checkValue < checkDC)) {
    const failValue = modified.failValue;
    if (failValue) {
      modified.source = modified.source.replace("Base Value", "Check Failed");
      modified.value = failValue;
    }
  }
  // Check succeed by 5 or more
  else if (checkValue >= checkDC + 5) {
    const each5Value = modified.each5Value;
    if (each5Value) {
      const degree = Math.floor((checkValue - checkDC) / 5);
      modified.source = modified.source.replace("Base Value", `Check Succeeded over ${(degree * 5)}`);
      modified.value += (degree * each5Value);
    }
  }
  final.modified = modified;
}

function _matchingConditionals(target, data) {
  if (!data.conditionals) return [];
  
  // Helper methods to check statuses and effects
  target.hasAnyCondition = (condsToFind, skipNonConditions) => {
    if (!condsToFind || condsToFind.length === 0) {
      if (!skipNonConditions) return target.statuses.length > 0;
      const conditions = CONFIG.DC20RPG.DROPDOWN_DATA.conditions;
      return target.statuses.find(status => conditions[status.id]) !== undefined;
    }
    return target.statuses.some(cond => condsToFind.includes(cond.id));
  };
  target.hasEffectWithName = (effectName, includeDisabled, selfOnly) => 
    target.effects.filter(effect => {
      const applierId = effect.flags.dc20rpg?.applierId;
      if (selfOnly && applierId && applierId !== data.applierId) return false;

      if (includeDisabled) return true;
      else return !effect.disabled;
    }).find(effect => effect.name === effectName) !== undefined;
  target.hasEffectWithKey = (effectKey, includeDisabled, selfOnly) => 
    target.effects.filter(effect => {
      const applierId = effect.flags.dc20rpg?.applierId;
      if (selfOnly && applierId && applierId !== data.applierId) return false;

      if (includeDisabled) return true;
      else return !effect.disabled;
    }).find(effect => effect.flags.dc20rpg?.effectKey === effectKey) !== undefined;

  const matching = [];
  data.conditionals.forEach(con => {
    const condition = con.condition;
    try {
      const conditionFulfilled = new Function('hit', 'crit', 'target', `return ${condition};`);
      if (conditionFulfilled(data.hit, data.isCritHit, target)) matching.push(con);
    } catch (e) {
      console.warn(`Cannot evaluate '${condition}' conditional: ${e}`);
    }
  });
  return matching;
}
function _modificationsFromConditionals(conditionals, final, target, skipConditionalDamage) {
  const modified = final.modified;
  const ignore = {
    pdr: false,
    edr: false,
    mdr: false,
    resistance: new Set(),
    immune: new Set()
  };
  if (!conditionals) return {ignore: ignore};
  
  conditionals.forEach(con => {
    if (!skipConditionalDamage) {
      // Apply extra dmg/healing
      if (con.bonus && con.bonus !== "" && con.bonus !== "0") {
        modified.source += ` + ${con.name}`;
        modified.value += evaluateDicelessFormula(con.bonus, target.rollData)._total;
      }
    } 

    // Get ignore flags
    const flags = con.flags;
    if (flags) {
      if (flags.ignorePdr) ignore.pdr = true;
      if (flags.ignoreEdr) ignore.edr = true;
      if (flags.ignoreMdr) ignore.mdr = true;
      ignore.resistance = new Set([...ignore.resistance, ...Object.keys(flags.ignoreResistance)]);
      ignore.immune = new Set([...ignore.immune, ...Object.keys(flags.ignoreImmune)]);
    }
  });
  final.modified = modified;
  return {ignore: ignore};
}

function _applyAttackRollModifications(hit, dmg, skipFor) {
  const extraDmg = Math.max(0, Math.floor(hit/5)); // We don't want to have negative extra damage

  // Add dmg from Heavy Hit, Brutal Hit etc.
  if (skipFor?.heavy) return dmg;
  if (extraDmg === 1) dmg.source += " + Heavy Hit";

  if (skipFor?.brutal) {
    dmg.source += " + Heavy Hit";
    dmg.value += extraDmg;
    return dmg;
  }
  if (extraDmg === 2) dmg.source += " + Brutal Hit";
  if (extraDmg >= 3) dmg.source += ` + Brutal Hit(over ${extraDmg * 5})`;
  dmg.value += extraDmg;
  return dmg;
}

function _applyDamageModifications(dmg, damageReduction, ignore) {
  const dmgType = dmg.type;
  if (dmgType == "true" || dmgType == "") return dmg; // True dmg cannot be modified
  const modifications = damageReduction.damageTypes[dmgType];
  const ignoreResitance = ignore.resistance.has(dmgType);
  const ignoreImmune = ignore.immune.has(dmgType);

  // STEP 1 - Adding & Subtracting
  // Resist X
  if (modifications.resist > 0 && !ignoreResitance) {
    dmg.source += ` - Resistance(${modifications.resist})`;
    dmg.value -= modifications.resist;
  }
  // Vulnerable X
  if (modifications.vulnerable > 0) {
    dmg.source += ` + Vulnerability(${modifications.vulnerable})`;
    dmg.value += modifications.vulnerable; 
  }

  // STEP 2 - Doubling & Halving
  // Immunity
  if (modifications.immune && !ignoreImmune) {
    dmg.source = "Resistance(Immune)";
    dmg.value = 0;
    return dmg;
  }
  // Resistance and Vulnerability - cancel each other
  if ((modifications.resistance && !ignoreResitance) && modifications.vulnerability) return dmg;
  // Resistance (reduce minimum 1)
  if (modifications.resistance && !ignoreResitance) {
    const newValue = Math.ceil(dmg.value/2);
    if (newValue === dmg.value) dmg.value = dmg.value - 1;
    else dmg.value = newValue;
    dmg.source += ` - Resistance(Half)`;
  } 
  // Vulnerability (adds minimum 1)
  if (modifications.vulnerability) {
    const newValue = Math.ceil(dmg.value * 2);
    if (newValue === dmg.value) dmg.value = dmg.value + 1;
    else dmg.value = newValue;
    dmg.source += ` + Vulnerability(Double)`;
  }
  return dmg;
}
function _applyDamageReduction(hit, dmg, damageReduction, ignore) {
  if (hit >= 5) return; // DR is applied only for normal hits;

  const dmgType = dmg.type;
  let drKey = "";
  if (CONFIG.DC20RPG.DROPDOWN_DATA.physicalDamageTypes[dmgType]) drKey = "pdr";
  if (CONFIG.DC20RPG.DROPDOWN_DATA.elementalDamageTypes[dmgType]) drKey = "edr";
  if (CONFIG.DC20RPG.DROPDOWN_DATA.mysticalDamageTypes[dmgType]) drKey = "mdr";
  if (!drKey) return;
  if (ignore[drKey]) return;
  
  // If dr is active we want to add resistance to specific damage types
  const activeDr = damageReduction[drKey].active;
  if (activeDr) {
    switch (drKey) {
      case "pdr":
        damageReduction.damageTypes.bludgeoning.resistance = true;
        damageReduction.damageTypes.slashing.resistance = true;
        damageReduction.damageTypes.piercing.resistance = true;
        break;
      case "mdr":
        damageReduction.damageTypes.umbral.resistance = true;
        damageReduction.damageTypes.radiant.resistance = true;
        damageReduction.damageTypes.psychic.resistance = true;
        break;
      case "edr":
        damageReduction.damageTypes.sonic.resistance = true;
        damageReduction.damageTypes.poison.resistance = true;
        damageReduction.damageTypes.corrosion.resistance = true;
        damageReduction.damageTypes.lightning.resistance = true;
        damageReduction.damageTypes.fire.resistance = true;
        damageReduction.damageTypes.cold.resistance = true;
        break;
    }
  }
}
function _applyFlatReduction(toApply, flatValue, label) {
  if (flatValue > 0) toApply.source += ` - ${label} Reduction(${flatValue})`;
  if (flatValue < 0) toApply.source += ` + Extra ${label}(${Math.abs(flatValue)})`;
  toApply.value -= flatValue;
  return toApply;
}
function _applyFlatReductionHalf(toApply, flatHalf, label) {
  if (flatHalf) {
    toApply.source += ` - ${label} Reduction(Half)`;
    toApply.value = Math.ceil(toApply.value/2);  
  }
  return toApply;
}
function _applyCritSuccess(toApply, isCritHit, canCrit) {
  if (isCritHit && canCrit) {
    toApply.source += " + Critical";
    toApply.value += 2;
  }
  return toApply;
}
function _applyCritFail(toApply, isAttack) {
  toApply.value = 0;
  toApply.source = isAttack ? "Critical Miss" : "Critical Fail";
  return toApply;
}
function _applyAttackMiss(toApply) {
  toApply.value = 0;
  toApply.source = "Miss";
  return toApply;
}
function _applyHalfDamageOnMiss(toApply) {
  toApply.source += ` - Miss(Half Damage)`;
  toApply.value = Math.ceil(toApply.value/2);  
  return toApply;
}
function _finalAdjustments(toApply) {
  if (toApply.value < 0) toApply.value = 0;
  toApply.value = Math.ceil(toApply.value);
  return toApply;
}

function _outcomeLabel(hit, critHit, critMiss, skipFor) {
  let label = "";

  // Miss
  if (critMiss) return "Critical Miss";
  if (hit < 0) return "Miss";

  // Crit Hit
  if (critHit && !skipFor?.crit) label += "Critical ";

  // Hit
  if (hit >= 0 && hit < 5) {
    label += "Hit";
    return label;
  }
  if (skipFor?.heavy) {
    label += "Hit";
    return label;
  }

  // Heavy
  if (hit >= 5 && hit < 10)  {
    label += "Heavy Hit";
    return label;
  }
  if (skipFor?.brutal) {
    label += "Heavy Hit";
    return label;
  }

  // Brutal
  if (hit >= 10 && hit < 15) label += "Brutal Hit";
  if (hit >= 15)             label += "Brutal Hit(+)";
  return label;
}

//========================
//         OTHER         =
//========================
function collectTargetSpecificFormulas(target, data, rolls) {
  // Clear any target specific rolls if those were added before
  rolls.dmg = rolls.dmg.filter(roll => !roll.clear.targetSpecific);
  rolls.heal = rolls.heal.filter(roll => !roll.clear.targetSpecific);

  const mathing = target ? _matchingConditionals(target, data) : [];
  for (const cond of mathing) {
    if (cond.addsNewFormula) {
      const type = cond.formula.type;
      const value = evaluateDicelessFormula(cond.formula.formula)._total;
      const source = cond.name;
      const dontMerge = cond.formula.dontMerge;
      const overrideDefence = cond.formula.overrideDefence;
      if (cond.formula.category === "damage") {
        rolls.dmg.push(_toRoll(value, type, source, dontMerge, overrideDefence));
      }
      if (cond.formula.category === "healing") {
        rolls.heal.push(_toRoll(value, type, source, dontMerge, overrideDefence));
      }
    }
  }
  return rolls;
}

function _toRoll(value, type, source, dontMerge, overrideDefence) {
  return {
    modified: {
      _total: value,
      modifierSources: source,
      type: type,
      dontMerge: dontMerge,
      overrideDefence: overrideDefence,
      targetSpecific: true,
    },
    clear: {
      _total: value,
      modifierSources: source,
      type: type,
      dontMerge: dontMerge,
      overrideDefence: overrideDefence,
      targetSpecific: true,
    }
  }
}

function collectTargetSpecificEffects(target, data) {
  const mathing = target ? _matchingConditionals(target, data) : [];
  const effects = [];
  for (const cond of mathing) {
    if (cond.effect) effects.push(cond.effect);
  }
  return effects;
}

function collectTargetSpecificRollRequests(target, data) {
  const mathing = target ? _matchingConditionals(target, data) : [];
  const rollRequests = {
    contests: [],
    saves: []
  };
  for (const cond of mathing) {
    if (cond.addsNewRollRequest && cond.rollRequest.category !== "") {
      const request = cond.rollRequest;
      if (request.category === "save") {
        request.label = getLabelFromKey(request.saveKey, CONFIG.DC20RPG.ROLL_KEYS.saveTypes);
        request.title = game.i18n.localize("dc20rpg.chat.targetSpecificRoll") + cond.name;
        rollRequests.saves.push(request);
      }
      if (request.category === "contest") {
        request.label = getLabelFromKey(request.contestedKey, CONFIG.DC20RPG.ROLL_KEYS.contests);
        request.title = game.i18n.localize("dc20rpg.chat.targetSpecificRoll") + cond.name;
        rollRequests.contests.push(request);
      }
    }
  }
  return rollRequests;
}

/**
 * Changes value of actor's skill skillMastery.
 */
async function toggleSkillMastery(skillType, skillKey, which, actor) {
	const skillMasteryLimit = getSkillMasteryLimit(actor, skillKey);
	const pathToValue = `system.${skillType}.${skillKey}.mastery`;
	const currentValue = getValueFromPath(actor, pathToValue);
  // checks which mouse button were clicked 1(left), 2(middle), 3(right)
  let newValue = which === 3 
    ? _switchMastery(currentValue, true, 0, skillMasteryLimit)
    : _switchMastery(currentValue, false, 0, skillMasteryLimit);

  await actor.update({[pathToValue] : newValue});
}

/**
 * Changes value of actor's language mastery.
 */
async function toggleLanguageMastery(pathToValue, which, actor) {
  let currentValue = getValueFromPath(actor, pathToValue);
  
  // checks which mouse button were clicked 1(left), 2(middle), 3(right)
  let newValue = which === 3 
    ? _switchMastery(currentValue, true, 0, 2)
    : _switchMastery(currentValue, false, 0, 2);

  await actor.update({[pathToValue] : newValue});
}

function getSkillMasteryLimit(actor, skillKey) {
	if (actor.type === "character") {
		const level = actor.system.details.level;
		let skillMasteryLimit = 1 + Math.floor(level/5);

		// Skill Expertise = +1 to the limit
		const expertise = new Set([...actor.system.expertise.automated, ...actor.system.expertise.manual]);
		if (expertise.has(skillKey)) skillMasteryLimit++; 

		return Math.min(skillMasteryLimit, 5) // Grandmaster is a limit for now
	}
	return 5; // For non PC is always 5;
}

function _switchMastery(mastery, goDown, min, max) {
	if (mastery >= max && !goDown) return 0;
	if (mastery <= min && goDown) return max;
	if (goDown) return mastery - 1;
	return mastery + 1;
}

function addCustomSkill(actor, trade) {
	const skillKey = generateKey();
	const skill = {
		label: "New Skill",
		modifier: 0,
		baseAttribute: "int",
		bonus: 0,
		mastery: 0,
		custom: true
	};
	if (trade) actor.update({[`system.tradeSkills.${skillKey}`] : skill});
	else actor.update({[`system.skills.${skillKey}`] : skill});
}

function removeCustomSkill(skillKey, actor, trade) {
	if (trade) actor.update({[`system.tradeSkills.-=${skillKey}`]: null });
	else actor.update({[`system.skills.-=${skillKey}`]: null });
}

function addCustomLanguage(actor) {
	const languageKey = generateKey();
	const language = {
		label: "New Language",
		mastery: 0,
		custom: true
	};
	actor.update({[`system.languages.${languageKey}`] : language});
}

function removeCustomLanguage(languageKey, actor) {
	actor.update({[`system.languages.-=${languageKey}`]: null });
}

async function convertSkillPoints(actor, from, to, opertaion, rate) {
	const skillFrom = actor.system.skillPoints[from];
	const skillTo = actor.system.skillPoints[to];
	
	if (opertaion === "convert") {
		const updateData = {
			[`system.skillPoints.${from}.converted`]: skillFrom.converted + 1,
			[`system.skillPoints.${to}.extra`]: skillTo.extra + parseInt(rate)
		};
		await actor.update(updateData);
	}
	if (opertaion === "revert") {
		const newExtra = skillFrom.extra - parseInt(rate);
		if (newExtra < 0) {
			ui.notifications.error("Cannot revert more points!");
			return;
		}
		const updateData = {
			[`system.skillPoints.${from}.extra`]: newExtra,
			[`system.skillPoints.${to}.converted`]: skillTo.converted - 1 
		};
		await actor.update(updateData);
	}
}

async function manipulateAttribute(key, actor, subtract) {
  const value = actor.system.attributes[key].current;
	if (subtract) {
		const newValue = Math.max(-2, value - 1);
		await actor.update({[`system.attributes.${key}.current`]: newValue});
	}
	else {
		const level = actor.system.details.level;
		const upperLimit = 3 + Math.floor(level/5);
		const newValue = Math.min(upperLimit, value + 1);
		await actor.update({[`system.attributes.${key}.current`]: newValue});
	}
}

async function manualSkillExpertiseToggle(skillKey, actor, skillType) {
	const manual = new Set(actor.system.expertise.manual);
	const automated = new Set(actor.system.expertise.automated);

	if (manual.has(skillKey)) {
		const skillLimit = getSkillMasteryLimit(actor, skillKey);
		const skillValue = actor.system[skillType]?.[skillKey]?.mastery;
		if (skillLimit === skillValue) await toggleSkillMastery(skillType, skillKey, 3, actor);
		manual.delete(skillKey);
		await actor.update({["system.expertise.manual"]: manual});
	}
	else if (automated.has(skillKey)) {
		ui.notifications.warn("You already have expertise in that skill!");
	}
	else {
		manual.add(skillKey);
		await actor.update({["system.expertise.manual"]: manual});
	}
}

//===========================================
//=				PREPARE CHECKS AND SAVES					=
//===========================================
function prepareCheckDetailsFor(key, against, statuses, rollTitle, customLabel) {
	if (!key) return;
	const [formula, rollType] = prepareCheckFormulaAndRollType(key); 

	let label = getLabelFromKey(key, {...CONFIG.DC20RPG.ROLL_KEYS.allChecks, "flat": "Flat d20", "initiative": "Initiative"});
	if (against) label += ` vs ${against}`;
	if (statuses) statuses = statuses.map(status => {
		if (status.hasOwnProperty("id")) return status.id;
		else return status;
	});
	return {
		roll: formula,
		label: label,
		rollTitle: rollTitle,
		type: rollType,
		against: parseInt(against),
		checkKey: key,
		statuses: statuses
	}
}

function prepareSaveDetailsFor(key, dc, statuses, rollTitle, customLabel) {
	if (!key) return;

	let save = "";
	switch (key) {
		case "phy": 
			save = "+ @special.phySave";
			break;
		
		case "men": 
			save = "+ @special.menSave";
			break;

		default:
			save = `+ @attributes.${key}.save`;
			break;
	}

	let label = getLabelFromKey(key, CONFIG.DC20RPG.ROLL_KEYS.saveTypes);
	if (dc) label += ` vs ${dc}`;
	if (statuses) statuses = statuses.map(status => {
		if (status.hasOwnProperty("id")) return status.id;
		else return status;
	});
	return {
		roll: `d20 ${save}`,
		label: label,
		rollTitle: rollTitle,
		type: "save",
		against: parseInt(dc),
		checkKey: key,
		statuses: statuses
	}
}

function prepareCheckFormulaAndRollType(key, rollLevel) {
	rollLevel = rollLevel || 0;
	let rollType = "";
	let formula = "d20";
	if (rollLevel !== 0) formula = `${Math.abs(rollLevel)+1}d20${rollLevel > 0 ? "kh" : "kl"}`;
	if (!key) return [formula, rollType];

	switch (key) {
		case "flat": 
			break;

		case "initiative":
			formula += ` + @special.initiative`;
			rollType = "initiative";
			break;

		case "mig": case "agi": case "int": case "cha": case "prime":
			formula += ` + @attributes.${key}.check`;
			rollType = "attributeCheck";
			break;

		case "att":
			formula += " + @attackMod.value.martial";
			rollType = "attackCheck";
			break;

		case "spe":
			formula += " + @attackMod.value.spell";
			rollType = "spellCheck";
			break;

		case "mar": 
			formula += " + @special.marCheck";
			rollType = "skillCheck";
			break;

		default:
			formula += ` + @allSkills.${key}`;
			rollType = "skillCheck";
			break;
  }
	return [formula, rollType];
}

function createNewCustomResource(name, actor) {
  const customResources = actor.system.resources.custom;
  const newResource = {
    name: name,
    img: "icons/svg/item-bag.svg",
    value: 0,
    maxFormula: null,
    max: 0,
    reset: ""
  };

  // Generate key (make sure that key does not exist already)
  let resourceKey = "";
  do {
    resourceKey = generateKey();
  } while (customResources[resourceKey]);

  actor.update({[`system.resources.custom.${resourceKey}`] : newResource});
}

function createNewCustomResourceFromItem(resource, img, actor) {
  const key = resource.resourceKey;
  const maxFormula = resource.useStandardTable ?  `@scaling.${key}` : resource.customMaxFormula; 
  const newResource = {
    name: resource.name,
    img: img,
    value: 0,
    maxFormula: maxFormula,
    max: 0,
    reset: resource.reset
  };
  actor.update({[`system.resources.custom.${key}`] : newResource});
}

function removeResource(resourceKey, actor) {
  actor.update({[`system.resources.custom.-=${resourceKey}`]: null });
}

function changeResourceIcon(key, actor) {
  new FilePicker({
    type: "image",
    displayMode: "tiles",
    callback: (path) => {
      if (!path) return;
      // Update the actor's custom resource icon with the selected image path
      actor.update({[`system.resources.custom.${key}.img`] : path});
    }
  }).render();
}

function createLegenedaryResources(actor) {
  const lap = {
    name: "Legendary Action Points",
    img: "icons/commodities/currency/coin-embossed-sword-copper.webp",
    value: 3,
    maxFormula: "3",
    max: 0,
    reset: "round"
  };

  const bossPoints = {
    name: "Boss Points",
    img: "icons/commodities/bones/skull-hollow-orange.webp",
    value: 3,
    maxFormula: "3",
    max: 0,
    reset: ""
  };

  const updateData = {
    lap: lap,
    boss: bossPoints
  };
  actor.update({['system.resources.custom'] : updateData});
}

//=============================================
//              HP MANIPULATION               =
//=============================================
/**
 * Applies damage to given actor.
 * Dmg object should look like this:
 * {
 *  "source": String,
 *  "type": String(ex. "fire"),
 *  "value": Number
 * }
 */
async function applyDamage(actor, dmg, options={}) {
  if (!actor) return;
  if (dmg.value === 0) return;

  const health = actor.system.resources.health;
  const newValue = health.value - dmg.value;
  const updateData = {
    ["system.resources.health.value"]: newValue,
    fromEvent: options.fromEvent,
    messageId: options.messageId
  };
  await actor.update(updateData);
  sendHealthChangeMessage(actor, dmg.value, dmg.source, "damage");
}

/**
 * Applies damage to given actor.
 * Heal object should look like this:
 * {
 *  "source": String,
 *  "type": String(ex. "temporary"),
 *  "value": Number,
 *  "allowOverheal": Boolean
 * }
 */
async function applyHealing(actor, heal, options={}) {
  if (!actor) return;
  if (heal.value === 0) return;

  const preventHpRegen = actor.system.globalModifier.prevent.hpRegeneration;
  if (preventHpRegen) {
    ui.notifications.error('You cannot regain any HP');
    return;
  }

  let sources = heal.source;
  const healType = heal.type;
  const healAmount = heal.value;
  const health = actor.system.resources.health;

  if (healType === "heal") {
    const oldCurrent = health.current;
    let newCurrent = oldCurrent + healAmount;
    let temp = health.temp || 0;

    // Overheal
    if (health.max < newCurrent) {
      const overheal = newCurrent - health.max;
      // Allow Overheal to transfer to temporary hp
      if (heal.allowOverheal) {
        if (overheal > temp) {
          sources += ` -> (Overheal <b>${overheal}</b> -> Transfered to TempHP)`;
          temp = overheal;
        }
        else sources += ` -> (Overheal <b>${overheal}</b> -> Would transfer to TempHP but current TempHP is bigger)`;
      }
      else sources += ` -> (Overheal <b>${overheal}</b>)`;
      newCurrent = health.max;
    }

    const updateData = {
      ["system.resources.health.temp"]: temp,
      ["system.resources.health.current"]: newCurrent,
      fromEvent: options.fromEvent,
      messageId: options.messageId
    };
    actor.update(updateData);
    sendHealthChangeMessage(actor, newCurrent - oldCurrent, sources, "healing");
  }
  
  if (healType === "temporary") {
    // Temporary HP do not stack it overrides
    const oldTemp = health.temp || 0;
    if (oldTemp >= healAmount) {
      sources += ` -> (Current Temporary HP is higher)`;
      sendHealthChangeMessage(actor, 0, sources, "temporary");
      return;
    }
    else if (oldTemp > 0) {
      sources += ` -> (Adds ${healAmount - oldTemp} to curent Temporary HP)`;
    }
    await actor.update({["system.resources.health.temp"]: healAmount});
    sendHealthChangeMessage(actor, healAmount - oldTemp, sources, "temporary");
  }
}

let preTriggerTurnedOffEvents = [];
/**
 * EVENT EXAMPLES:
 * "eventType": "damage", "label": "Rozpierdol", "trigger": "turnStart", "value": 1, "type": "fire", "continuous": "true"
 * "eventType": "healing", "label": "Rozpierdol", "trigger": "turnEnd", "value": 2, "type": "heal"
 * "eventType": "saveRequest", "label": "Fear Me", "trigger": "turnEnd", "checkKey": "mig", "statuses": ["rattled", "charmed"]
 * "eventType": "saveRequest/checkRequest", "label": "Exposee", "trigger": "turnStart", "checkKey": "mig", "statuses": ["exposed"], "against": "14"
 * "eventType": "saveRequest", "label": "That Hurts", "trigger": "damageTaken/healingTaken", "checkKey": "mig", "statuses": ["exposed"]
 * "eventType": "basic", "label": "That Hurts but once", "trigger": "damageTaken", "postTrigger":"disable/delete", "preTrigger": "disable/skip" "reenable": "turnStart"
 * lista triggerów: "turnStart", "turnEnd", "damageTaken", "healingTaken", "attack"
 * triggers to add:
 * "targeted" - when you are a target of an attack - 
 * "diceRoll" - when you roll a dice?
 */
async function runEventsFor(trigger, actor, filters=[], extraMacroData={}, specificEvent) {
  let eventsToRun = specificEvent ? [specificEvent] : actor.activeEvents.filter(event => event.trigger === trigger);
  eventsToRun = _filterEvents(eventsToRun, filters);
  eventsToRun = _sortByType(eventsToRun);

  for (const event of eventsToRun) {
    let runTrigger = true;
    runTrigger = await _runPreTrigger(event, actor);
    if (!runTrigger) continue;

    const target = {
      system: {
        damageReduction: actor.system.damageReduction,
        healingReduction: actor.system.healingReduction
      }
    };
    switch(event.eventType) {
      case "damage":
        // Check if damage should be reduced
        let dmg = {
          value: parseInt(event.value),
          source: event.label,
          type: event.type
        };
        dmg = calculateForTarget(target, {clear: {...dmg}, modified: {...dmg}}, {isDamage: true});
        await applyDamage(actor, dmg.modified, {fromEvent: true});
        break;

      case "healing":
        let heal = {
          source: event.label,
          value: parseInt(event.value),
          type: event.type
        };
        heal = calculateForTarget(target, {clear: {...heal}, modified: {...heal}}, {isHealing: true});
        await applyHealing(actor, heal.modified, {fromEvent: true});
        break;

      case "checkRequest":
        const checkDetails = prepareCheckDetailsFor(event.checkKey, event.against, event.statuses, event.label);
        const checkRoll = await promptRollToOtherPlayer(actor, checkDetails);
        await _rollOutcomeCheck(checkRoll, event, actor);
        break;

      case "saveRequest": 
        const saveDetails = prepareSaveDetailsFor(event.checkKey, event.against, event.statuses, event.label);
        const saveRoll = await promptRollToOtherPlayer(actor, saveDetails);
        await _rollOutcomeCheck(saveRoll, event, actor);
        break;

      case "resource":
        await _resourceManipulation(event.value, event.resourceKey, event.custom, event.label, actor);
        break;

      case "macro": 
        const effect = getEffectFrom(event.effectId, actor);
        if (!effect) break;
        const command = effect.flags.dc20rpg?.macro;
        if (!command) break;
        await runTemporaryMacro(command, effect, {actor: actor, effect: effect, event: event, extras: extraMacroData});
        break;
      
      case "basic":
        break;

      default:
        await _runCustomEventTypes(event, actor, effect);
    }
    _runPostTrigger(event, actor);
  }
}

function _filterEvents(events, filters) {
  if (!filters) return events;
  if (filters.length === 0) return events;

  for (const filter of filters) {
    events = events.filter(event => {
      if (filter.required || event.hasOwnProperty(filter.eventField)) {
        return filter.filterMethod(event[filter.eventField]);
      }
      else return true;
    });
  }
  return events;
}

function _sortByType(events) {
  const resourceManipulation = [];
  const macro = [];
  const requests = [];
  const basic = [];

  events.forEach(event => {
    switch (event.eventType) {
      case "damage": case "healing": case "resource":
        resourceManipulation.push(event); break;
      case "macro": 
        macro.push(event); break;
      case "checkRequest": case "saveRequest":
        requests.push(event); break;
      default:
        basic.push(event); break;
    }
  });

  return [
    ...resourceManipulation,
    ...macro,
    ...requests,
    ...basic
  ]
}

async function _rollOutcomeCheck(roll, event, actor) {
  if (!roll) return;
  if (!event.against) return;

  if (event.onSuccess && roll.total >= event.against) {
    switch (event.onSuccess) {
      case "disable":
        _disableEffect(event.effectId, actor);
        break;
  
      case "delete": 
        _deleteEffect(event.effectId, actor);
        break;
  
      case "runMacro": 
        const effect = getEffectFrom(event.effectId, actor);
        if (!effect) break;
        const command = effect.flags.dc20rpg?.macro;
        if (!command) break;
        await runTemporaryMacro(command, effect, {actor: actor, effect: effect, event: event, extras: {success: true}});
        break;

      default:
        console.warn(`Unknown on success type: ${event.onSuccess}`);
    }
  }
  else if (event.onFail && roll.total < event.against) {
    switch (event.onFail) {
      case "disable":
        _disableEffect(event.effectId, actor);
        break;
  
      case "delete": 
        _deleteEffect(event.effectId, actor);
        break;

      case "runMacro": 
        const effect = getEffectFrom(event.effectId, actor);
        if (!effect) break;
        const command = effect.flags.dc20rpg?.macro;
        if (!command) break;
        await runTemporaryMacro(command, effect, {actor: actor, effect: effect, event: event, extras: {success: false}});
        break;
  
      default:
        console.warn(`Unknown on fail type: ${event.onFail}`);
    }
  }
}

async function _resourceManipulation(value, key, custom, label, actor) {
  const canSubtract = custom ? canSubtractCustomResource : canSubtractBasicResource;
  const regain = custom ? regainCustomResource : regainBasicResource;
  const subtract = custom ? subtractCustomResource : subtractCustomResource;

  const cost = {
    value: value,
    name: label
  };
  // Subtract
  if (value > 0) {
    if (canSubtract(key, actor, cost)) {
      await subtract(key, actor, value, true);
      ui.notifications.info(`"${label}" - subtracted ${value} from ${key}`);
    }
  }
  // Regain
  if (value < 0) {
    await regain(key, actor, Math.abs(value), true);
    ui.notifications.info(`"${label}" - regained ${Math.abs(value)} ${key}`);
  }
}

async function _runPreTrigger(event, actor) {
  if (!event.preTrigger) return true;
  const label = event.label || event.effectName;
  const confirmation = await getSimplePopup("confirm", {header: `Do you want to use "${label}" as a part of that action?`});
  if (!confirmation) {
    // Disable event until enabled by reenablePreTriggerEvents() method
    if (event.preTrigger === "disable") {
      const effect = await _disableEffect(event.effectId, actor);
      if (effect) preTriggerTurnedOffEvents.push(effect); 
      return false;
    }
    if (event.preTrigger === "skip") {
      return false;
    }
  }
  return true;
}

function _runPostTrigger(event, actor) {
  if (!event.postTrigger) return;
  switch (event.postTrigger) {
    case "disable":
      _disableEffect(event.effectId, actor);
      break;

    case "delete": 
      _deleteEffect(event.effectId, actor);
      break;

    default:
      console.warn(`Unknown post trigger type: ${event.postTrigger}`);
  }
}

async function reenableEventsOn(reenable, actor, filters=[]) {
  let eventsToReenable = actor.allEvents.filter(event => event.reenable === reenable);
  eventsToReenable = _filterEvents(eventsToReenable, filters);

  for (const event of eventsToReenable) {
    await _enableEffect(event.effectId, actor);
  }
}

function reenablePreTriggerEvents() {
  for(const effect of preTriggerTurnedOffEvents) {
    effect.enable({dontUpdateTimer: true});
  }
  preTriggerTurnedOffEvents = [];
}

function parseEvent(event) {
  if (!event) return;
  try {
    const obj = JSON.parse(`{${event}}`);
    return obj;
  } catch (e) {
    console.warn(`Cannot parse event json {${event}} with error: ${e}`);
  }
}

async function _deleteEffect(effectId, actor) {
  const effect = getEffectFrom(effectId, actor);
  if (!effect) return;
  sendEffectRemovedMessage(actor, effect);
  await effect.delete();
}

async function _disableEffect(effectId, actor) {
  const effect = getEffectFrom(effectId, actor, {active: true});
  if (!effect) return;
  await effect.disable();
  return effect;
}

async function _enableEffect(effectId, actor) {
  const effect = getEffectFrom(effectId, actor, {disabled: true});
  if (!effect) return;
  await effect.enable();
  return effect;
}

async function runInstantEvents(effect, actor) {
  if (!effect.changes) return;

  for (const change of effect.changes) {
    if (change.key === "system.events" && change.value.includes('"instant"')) {
      const event = await parseEvent(change.value);
      event.effectId = effect.id;
      await runEventsFor("instantTrigger", actor, {}, {}, event);
    }
  }
}

//=================================
//=       CUSTOM EVENT TYPES      =
//=================================
function registerEventType(eventType, method, displayedLabel) {
  CONFIG.DC20Events[eventType] = method;
  CONFIG.DC20RPG.eventTypes[eventType] = displayedLabel;
}

function registerEventTrigger(trigger, displayedLabel) {
  CONFIG.DC20RPG.allEventTriggers[trigger] = displayedLabel;
}

function registerEventReenableTrigger(trigger, displayedLabel) {
  CONFIG.DC20RPG.reenableTriggers[trigger] = displayedLabel;
}

async function _runCustomEventTypes(event, actor, effect) {
  const method = CONFIG.DC20Events[event.eventType];
  if (method) await method(event, actor, effect);
}

//=================================
//=        FILTER METHODS         =
//=================================
function effectEventsFilters(effectName, statuses, effectKey) {
  const filters = [];
  if (effectName !== undefined) {
    filters.push({
      required: false,
      eventField: "withEffectName",
      filterMethod: (field) => {
        if (!field) return true;
        return field === effectName
      }
    });
  }
  if (effectKey !== undefined) {
    filters.push({
      required: false,
      eventField: "withEffectKey",
      filterMethod: (field) => {
        if (!field) return true;
        return field === effectKey
      }
    });
  }
  if (statuses !== undefined) {
    filters.push({
      required: false,
      eventField: "withStatus",
      filterMethod: (field) => {
        if (!field) return true;
        return statuses?.has(field);
      }
    });
  }
  return filters;
}

function minimalAmountFilter(amount) {
  const filter = {
    required: false,
    eventField: "minimum",
    filterMethod: (field) => {
      if (!field) return true;
      return amount >= field;
    }
  };
  return [filter];
}

function currentRoundFilter(actor, currentRound) {
  const filter = {
    required: true,
    eventField: "effectId",
    filterMethod: (field) => {
      const effect = getEffectFrom(field, actor);
      if (!effect) return true;
      return effect.duration.startRound < currentRound;
    }
  };
  return [filter];
}

function actorIdFilter(actorId) {
  const filter = {
    required: true,
    eventField: "actorId",
    filterMethod: (field) => {
      if (!field) return true;
      return field === actorId
    }
  };
  return [filter];
}

function triggerOnlyForIdFilter(expecetdId) {
  const filter = {
    required: false,
    eventField: "triggerOnlyForId",
    filterMethod: (field) => {
      if (!field) return true;
      return field === expecetdId;
    }
  };
  return [filter];
}

function restTypeFilter(expectedRests) {
  const filter = {
    required: false,
    eventField: "restType",
    filterMethod: (field) => {
      if (!field) return true;
      return expectedRests.includes(field);
    }
  };
  return [filter];
}

/**
 * Dialog window for resting.
 */
class RestDialog extends Dialog {

  constructor(actor, preselected, dialogData = {}, options = {}) {
    super(dialogData, options);
    this.actor = actor;
    this.data = {
      selectedRestType: preselected || "long",
      noActivity: true
    };
  }

  static get defaultOptions() {
    return foundry.utils.mergeObject(super.defaultOptions, {
      template: "systems/dc20rpg/templates/dialogs/rest-dialog.hbs",
      classes: ["dc20rpg", "dialog", "flex-dialog"]
    });
  }

  getData() {
    const restTypes = CONFIG.DC20RPG.DROPDOWN_DATA.restTypes;
    this.data.rest = this.actor.system.rest;
    this.data.resources = {
      restPoints: this.actor.system.resources.restPoints
    };

    return {
      restTypes: restTypes,
      ...this.data
    }
  }

   /** @override */
  activateListeners(html) {
    super.activateListeners(html);
    html.find(".selectable").change(ev => this._onSelection(ev));
    html.find(".regain-rp").click(ev => this._onRpRegained(ev));
    html.find(".spend-rp").click(ev => this._onRpSpend(ev));
    html.find(".finish-rest").click(ev => this._onFinishRest(ev));
    html.find(".reset-rest").click(ev => this._onResetRest(ev));
    html.find(".activity").click(ev => this._onSwitch(ev));
  }

  async _onSelection(event) {
    event.preventDefault();
    this.data.selectedRestType = event.currentTarget.value;
    this.render();
  }

  async _onSwitch(event) {
    const activity = datasetOf(event).activity === "true";
    this.data.noActivity = !activity;
    this.render();
  }

  async _onRpSpend(event) {
    event.preventDefault();
    await spendRpOnHp(this.actor, 1);
    this.render();
  }

  async _onRpRegained(event) {
    event.preventDefault();
    await regainBasicResource("restPoints", this.actor, 1, true);
    this.render();
  }

  async _onResetRest(event) {
    event.preventDefault();
    await this._resetLongRest();
    this.render();
  }

  //==================================//
  //            Finish Rest           //
  //==================================//
  async _onFinishRest(event) {
    event.preventDefault();
    switch (this.data.selectedRestType) {
      case "quick":
        await this._finishQuickRest(this.actor);
        this.close();
        break;
      case "short":
        await this._finishShortRest(this.actor);
        this.close();
        break;
      case "long":
        const closeWindow = await this._finishLongRest(this.actor, this.data.noActivity);
        if (closeWindow) this.close();
        else this.render(true);
        break;
      case "full": 
        await this._finishFullRest(this.actor);
        this.close();
        break;
      default:
        ui.notifications.error("Choose correct rest type first.");
    }
  }

  async _finishQuickRest(actor) {
    await _refreshItemsOn(actor, ["round", "quick"]);
    await _refreshCustomResourcesOn(actor, ["round", "quick"]);
    await runEventsFor("rest", actor, restTypeFilter(["quick"]));
    return true;
  }
  
  async _finishShortRest(actor) {
    await _refreshItemsOn(actor, ["round", "quick", "short"]);
    await _refreshCustomResourcesOn(actor, ["round", "quick", "short"]);
    await runEventsFor("rest", actor, restTypeFilter(["quick", "short"]));
    return true;
  }
  
  async _finishLongRest(actor, noActivity) {
    await _respectActivity(actor, noActivity);
  
    const halfFinished = actor.system.rest.longRest.half;
    if (halfFinished) {
      await _refreshMana(actor);
      await _refreshGrit(actor);
      await _refreshItemsOn(actor, ["round", "combat", "quick", "short", "long"]);
      await _refreshCustomResourcesOn(actor, ["round", "combat", "quick", "short", "long"]);
      await _checkForExhaustionSave(actor);
      await _clearDoomed(actor);
      await runEventsFor("rest", actor, restTypeFilter(["long"]));
      await this._resetLongRest();
      return true;
    } 
    else {
      await _refreshRestPoints(actor);
      await _refreshItemsOn(actor, ["round", "quick", "short"]);
      await _refreshCustomResourcesOn(actor, ["round", "quick", "short"]);
      await runEventsFor("rest", actor, restTypeFilter(["quick", "short"]));
      await actor.update({["system.rest.longRest.half"]: true});
      return false;
    }
  }
  
  async _finishFullRest(actor) {
    await _refreshMana(actor);
    await _refreshGrit(actor);
    await _refreshRestPoints(actor);
    await _refreshHealth(actor);
    await _clearExhaustion(actor);
    await _clearDoomed(actor);
    await _refreshItemsOn(actor, ["round", "combat", "quick", "short", "long", "full", "day"]);
    await _refreshCustomResourcesOn(actor, ["round", "combat", "quick", "short", "long", "full"]);
    await runEventsFor("rest", actor, restTypeFilter(["quick", "short", "long", "full"]));
    return true;
  }

  async _resetLongRest() {
    const updateData = {
      ["system.rest.longRest.half"]: false,
      ["system.rest.longRest.noActivity"]: false
    };
    await this.actor.update(updateData);
    return;
  }
}

/**
 * Opens Rest Dialog popup for given actor.
 */
function createRestDialog(actor, preselected) {
  new RestDialog(actor, preselected, {title: `Begin Your Rest ${actor.name}`}).render(true);
}

function openRestDialogForOtherPlayers(actor, preselected) {
  emitSystemEvent("startRest", {
    actorId: actor.id,
    preselected: preselected
  });
}

async function refreshOnRoundEnd(actor) {
  refreshAllActionPoints(actor);
  await _refreshItemsOn(actor, ["round"]);
  await _refreshCustomResourcesOn(actor, ["round"]);
}

async function refreshOnCombatStart(actor) {
  refreshAllActionPoints(actor);
  await _refreshStamina(actor);
  await _refreshItemsOn(actor, ["round", "combat"]);
  await _refreshCustomResourcesOn(actor, ["round", "combat"]);
}

async function rechargeItem(item, half) {
  if (!item.system.costs) return;
  const charges = item.system.costs.charges;
  if (charges.max === charges.current) return;

  const rollData = await item.getRollData();
  let newCharges = charges.max;

  if (charges.rechargeDice) {
    const roll = await evaluateFormula(charges.rechargeDice, rollData);
    const result = roll.total;
    const rechargeOutput = result >= charges.requiredTotalMinimum 
                                ? game.i18n.localize("dc20rpg.rest.rechargedDescription") 
                                : game.i18n.localize("dc20rpg.rest.notrechargedDescription");
    ui.notifications.notify(`${item.actor.name} ${rechargeOutput} ${item.name}`);
    if (result < charges.requiredTotalMinimum) return;
  }
  if (charges.overriden) {
    const roll = await evaluateFormula(charges.rechargeFormula, rollData);
    newCharges = roll.total;
  }

  if (half) newCharges = Math.ceil(newCharges/2);
  item.update({[`system.costs.charges.current`]: Math.min(charges.current + newCharges, charges.max)});
}

async function _refreshItemsOn(actor, resetTypes) {
  const items = actor.items;

  items.forEach(async item => {
    if (!item.system.costs) return;
    const charges = item.system.costs.charges;
    if (charges.max === charges.current) return;
    if (!resetTypes.includes(charges.reset) && !_halfOnShortValid(charges.reset, resetTypes)) return;

    const half = charges.reset === "halfOnShort" && resetTypes.includes("short") && !resetTypes.includes("long");
    rechargeItem(item, half);
  });
}

function _halfOnShortValid(reset, resetTypes) {
  if (reset !== "halfOnShort") return false;
  if (resetTypes.includes("short") || resetTypes.includes("long")) return true;
  return false;
}

async function _refreshCustomResourcesOn(actor, resetTypes) {
  const customResources = actor.system.resources.custom;
  const updateData = {};
  Object.entries(customResources).forEach(([key, resource]) => {
    if (resetTypes.includes(resource.reset) || (resource.reset === "halfOnShort" && resetTypes.includes("long"))) {
      resource.value = resource.max;
      updateData[`system.resources.custom.${key}`] = resource;
    }
    else if (resource.reset === "halfOnShort" && resetTypes.includes("short")) {
      const newValue = resource.value + Math.ceil(resource.max/2);
      resource.value = Math.min(newValue, resource.max);
      updateData[`system.resources.custom.${key}`] = resource;
    }
  });
  actor.update(updateData);
}

async function _clearExhaustion(actor) {
  actor.effects.forEach(effect => {
    if (effect.system?.statusId === "exhaustion") effect.delete();
  });
  await actor.update({["system.rest.longRest.exhSaveDC"]: 10});
}

async function _clearDoomed(actor) {
  actor.effects.forEach(effect => {
    if (effect.system?.statusId === "doomed") effect.delete();
  });
}

async function _respectActivity(actor, noActivity) {
  if (noActivity) {
    await actor.toggleStatusEffect("exhaustion", { active: false });
    await actor.update({["system.rest.longRest.noActivity"]: true});
  }
}

async function _checkForExhaustionSave(actor) {
  const noActivity = actor.system.rest.longRest.noActivity;
  if (!noActivity) {
    const rollDC = actor.system.rest.longRest.exhSaveDC;
    const details = {
      roll: "d20 + @attributes.mig.save",
      label: `Might Save vs DC ${rollDC}`,
      rollTitle: "Exhaustion Save",
      type: "save",
      against: rollDC
    };
    const roll = await promptRoll(actor, details);
    if (roll.total < rollDC) {
      await actor.toggleStatusEffect("exhaustion", { active: true });
      await actor.update({["system.rest.longRest.exhSaveDC"]: rollDC + 5});
    }
  }
}

async function _refreshMana(actor) {
  if (!actor.system.resources.mana) return;
  const manaMax = actor.system.resources.mana.max;
  await actor.update({["system.resources.mana.value"]: manaMax});
}

async function _refreshStamina(actor) {
  if (!actor.system.resources.stamina) return;
  const manaStamina = actor.system.resources.stamina.max;
  await actor.update({["system.resources.stamina.value"]: manaStamina});
}

async function _refreshHealth(actor) {
  const hpMax = actor.system.resources.health.max;
  await actor.update({["system.resources.health.current"]: hpMax});
}

async function _refreshGrit(actor) {
  if (!actor.system.resources.grit) return;
  const gritMax = actor.system.resources.grit.max;
  await actor.update({["system.resources.grit.value"]: gritMax});
}

async function _refreshRestPoints(actor) {
  const rpMax = actor.system.resources.restPoints.max;
  await actor.update({["system.resources.restPoints.value"]: rpMax});
}

function validateUserOwnership(pack) {
  if (pack.ownership.default === 0) return false;

  const userRole = CONST.USER_ROLE_NAMES[game.user.role];
  const packOwnership = pack.ownership[userRole];
  if (packOwnership === "NONE") return false;
  
  return true;
}

async function collectItemsForType(itemType) {
  const hiddenItems = game.dc20rpg.compendiumBrowser.hideItems;
  const collectedItems = [];
  for (const pack of game.packs) {
    if (!validateUserOwnership(pack)) continue;

    if (pack.documentName === "Item") {
      if (pack.isOwner) continue;
      const items = await pack.getDocuments();
      for(const item of items) {
        if (item.type === itemType) {
          const packageType = pack.metadata.packageType;
          // If system item is overriden by some other module we want to hide it from browser
          if (packageType === "system" && hiddenItems.has(item.id)) continue;

          // For DC20 Players Handbook module we want to keep it as a system instead of module pack
          const isDC20Handbook = pack.metadata.packageName === "dc20-core-rulebook";
          item.fromPack = isDC20Handbook ? "system" : packageType;
          item.sourceName = _getSourceName(pack);
          collectedItems.push(item);
        }
      }
    }
  }
  _sort(collectedItems);
  return collectedItems;
}
async function collectActors() {
  const collectedActors = [];

  for (const pack of game.packs) {
    if (!validateUserOwnership(pack)) continue;

    if (pack.documentName === "Actor") {
      if (pack.isOwner) continue;
      const actors = await pack.getDocuments();
      for(const actor of actors) {
        // For DC20 Players Handbook module we want to keep it as a system instead of module pack
        const isDC20Handbook = pack.metadata.packageName === "dc20-core-rulebook";
        actor.fromPack = isDC20Handbook ? "system" : pack.metadata.packageType;
        actor.sourceName = _getSourceName(pack);
        collectedActors.push(actor);
      }
    }
  }
  _sort(collectedActors);
  return collectedActors;
}
function _sort(array) {
  array.sort(function(a, b) {
    const textA = a.name.toUpperCase();
    const textB = b.name.toUpperCase();
    return (textA < textB) ? -1 : (textA > textB) ? 1 : 0;
  });
}
function _getSourceName(pack) {
  const type = pack.metadata.packageType;
  if (type === "module") {
    const module = game.modules.get(pack.metadata.packageName);
    if (module) return module.title;
    else return capitalize(type);
  }
  else return capitalize(type);
}

function filterDocuments(collectedDocuments, filters) {
  const filtered = [];
  for (const document of collectedDocuments) {
    let filtersFailed = false;
    // Go over filters
    for (const filter of filters) {
      if (filter.nestedFilters && filter.nestedFilters.length > 0) {
        // Check for nested filters first
        for (const key of filter.nestedFilters) {
          const nested = filter[key];
          if (nested && !nested.check(document, nested.value)) filtersFailed = true;
        }
      }
      else if (!filter.check || !filter.check(document, filter.value)) filtersFailed = true;
    }

    // Check if should be hidden
    if (document.system?.hideFromCompendiumBrowser) filtersFailed = true;
    if (!filtersFailed) filtered.push(document);
  }
  return filtered;
}

function getDefaultItemFilters(preSelectedFilters) {
  let parsedFilters = {};
  if (preSelectedFilters) {
    try {
      parsedFilters = JSON.parse(preSelectedFilters);
    } catch (e) {
      console.warn(`Cannot parse pre selected filters '${preSelectedFilters}' with error: ${e}`);
    }
  }

  return {
    name: _filter("name", "name", "text"),
    compendium: _filter("fromPack", "compendium", "multi-select", {
      system: true,
      world: true,
      module: true
    }, "stringCheck"),
    sourceName: _filter("sourceName", "sourceName", "text"),
    feature: {
      featureOrigin: _filter("system.featureOrigin", "feature.featureOrigin", "text", parsedFilters["featureOrigin"]),
      featureType: _filter("system.featureType", "feature.featureType", "select", parsedFilters["featureType"], CONFIG.DC20RPG.DROPDOWN_DATA.featureSourceTypes),
      level: {
        over: _filter("system.requirements.level", "feature.level.over", "over"),
        under: _filter("system.requirements.level", "feature.level.under", "under"),
        filterType: "over-under",
        updatePath: "level",
        nestedFilters: ["over", "under"]
      },
    },
    technique: {
      techniqueOrigin: _filter("system.techniqueOrigin", "technique.techniqueOrigin", "text", parsedFilters["techniqueOrigin"]),
      techniqueType: _filter("system.techniqueType", "technique.techniqueType", "select", parsedFilters["techniqueType"], CONFIG.DC20RPG.DROPDOWN_DATA.techniqueTypes)
    },
    spell: {
      spellOrigin: _filter("system.spellOrigin", "spell.spellOrigin", "text", parsedFilters["spellOrigin"]),
      spellType: _filter("system.spellType", "spell.spellType", "select", parsedFilters["spellType"], CONFIG.DC20RPG.DROPDOWN_DATA.spellTypes),
      magicSchool: _filter("system.magicSchool", "spell.magicSchool", "select", parsedFilters["magicSchool"], CONFIG.DC20RPG.DROPDOWN_DATA.magicSchools),
      spellLists: _filter("system.spellLists", "spell.spellLists", "multi-select", parsedFilters["spellLists"] || {
        arcane: true,
        divine: true,
        primal: true
      }) 
    },
    weapon: {
      weaponType: _filter("system.weaponType", "weapon.weaponType", "select", parsedFilters["weaponType"], CONFIG.DC20RPG.DROPDOWN_DATA.weaponTypes),
      weaponStyle: _filter("system.weaponStyle", "weapon.weaponStyle", "select", parsedFilters["weaponStyle"], CONFIG.DC20RPG.DROPDOWN_DATA.weaponStyles),
    },
    equipment: {
      equipmentType: _filter("system.equipmentType", "equipment.equipmentType", "select", parsedFilters["equipmentType"], CONFIG.DC20RPG.DROPDOWN_DATA.equipmentTypes)
    },
    consumable: {
      consumableType: _filter("system.consumableType", "consumable.consumableType", "select", parsedFilters["consumableType"], CONFIG.DC20RPG.DROPDOWN_DATA.consumableTypes)
    },
    subclass: {
      classSpecialId: _filter("system.forClass.classSpecialId", "subclass.classSpecialId", "select", parsedFilters["classSpecialId"], CONFIG.DC20RPG.UNIQUE_ITEM_IDS.class)
    }
  }
}

function getDefaultActorFilters() {
  return {
    name: _filter("name", "name", "text"),
    level: {
      over: _filter("system.details.level", "level.over", "over"),
      under: _filter("system.details.level", "level.under", "under"),
      filterType: "over-under",
      updatePath: "level",
      nestedFilters: ["over", "under"]
    },
    type: _filter("type", "type", "multi-select", {
      character: false,
      npc: true,
      companion: false
    }, "stringCheck"),
    role: _filter("system.details.role", "role", "text"),
    creatureType: _filter("system.details.creatureType", "creatureType", "text"),
    compendium: _filter("fromPack", "compendium", "multi-select", {
      system: true,
      world: true,
      module: true
    }, "stringCheck"),
    sourceName: _filter("sourceName", "sourceName", "text"),
  }
}

function _filter(pathToCheck, filterUpdatePath, filterType, defaultValue, options) {
  const value = defaultValue || "";
  
  // Prepare check method
  let method = (document, value) => {
    if (!value) return true;
    return getValueFromPath(document, pathToCheck) === value;
  };
  if (filterType === "text") method = (document, value) => {
    if (!value) return true;
    const documentValue = getValueFromPath(document, pathToCheck);
    if (!documentValue) return false;
    return documentValue.toLowerCase().includes(value.toLowerCase());
  };
  if (filterType === "multi-select") method = (document, expected) => {
    if (!expected) return true;
    // We need to check if string value is one of the selected filter value
    if (options === "stringCheck") return expected[getValueFromPath(document, pathToCheck)];

    // We need to check if at least one filter value equals activated document multi select options
    const selected = getValueFromPath(document, pathToCheck);
    if (!selected) return false;

    let mathing = false;
    for (const [key, value] of Object.entries(expected)) {
      if (value && selected[key] && selected[key].active) mathing = true;
    }
    return mathing;
  };
  if (filterType === "under") method = (document, under) => {
    if (under === undefined || under === null || under === "") return true;
    const value = getValueFromPath(document, pathToCheck);
    return value <= under;
  };
  if (filterType === "over") method = (document, over) => {
    if (over === undefined || over === null || over === "") return true;
    const value = getValueFromPath(document, pathToCheck);
    return value >= over;
  };

  return {
    check: method,
    updatePath: filterUpdatePath, 
    filterType: filterType,
    value: value,
    options: options
  }
}

function itemDetailsToHtml(item, includeCosts) {
  if (!item) return "";
  let content = "";
  if(includeCosts) content += _cost(item);
  content += _range(item);
  content += _target(item);
  content += _duration(item);
  content += _weaponStyle(item);
  content += _magicSchool(item);
  content += _props(item);
  content += _components(item);
  return content;
}

function _cost(item) {
  let content = "";
  const cost = item.system?.costs?.resources;
  if (!cost) return "";
  
  if (cost.actionPoint > 0)   content += `<div class='detail red-box'>${cost.actionPoint} AP</div>`;
  if (cost.stamina > 0)       content += `<div class='detail red-box'>${cost.stamina} SP</div>`;
  if (cost.mana > 0)          content += `<div class='detail red-box'>${cost.mana} MP</div>`;
  if (cost.health > 0)        content += `<div class='detail red-box'>${cost.health} HP</div>`;
  if (cost.grit > 0)          content += `<div class='detail red-box'>${cost.grit} GP</div>`;
  if (cost.restPoints > 0)    content += `<div class='detail red-box'>${cost.restPoints} RP</div>`;

  // Prepare Custom resource cost
  if (cost.custom) {
    for (const custom of Object.values(cost.custom)) {
      if (custom.value > 0)   content += `<div class='detail red-box'>${custom.value} ${custom.name}</div>`;
    }
  }
  return content;  
}

function _range(item) {
  const range = item.system?.range;
  let content = "";

  if (range) {
    const melee = range.melee;
    const normal = range.normal;
    const max = range.max;
    const unit = range.unit ? range.unit : "Spaces";

    if (normal) {
      content += `<div class='detail'> ${normal}`;
      if (max) content += `/${max}`;
      content += ` ${unit} Range </div>`;
    }
    if (melee && melee > 1) {
      content += `<div class='detail'> ${melee}`;
      content += ` ${unit} Melee Range </div>`;
    }
  }
  return content;
}

function _target(item) {
  const target =  item.system?.target;
  let content = "";

  if (target) {
    content += _invidual(target);
    content += _area(target);
  }
  return content;
}
  
function _invidual(target) {
  let content = "";
  const type = target.type;
  const count = target.count;

  if (type) {
    content += "<div class='detail'>";
    if (count) content += ` ${count}`;
    content += ` ${getLabelFromKey(type, CONFIG.DC20RPG.DROPDOWN_DATA.invidualTargets)}`;
    content += "</div>";
  }
  return content;
}
  
function _area(target) {
  let content = "";

  Object.values(target.areas).forEach(ar => {
    const area = ar.area;
    const unit = ar.unit;
    const distance = ar.distance;
    const width = ar.width;
  
    if (area) {
      content += "<div class='detail'>";
      if (distance) {
        content += area === "line" ? ` ${distance}/${width}` : ` ${distance}`;
        content += unit ? ` ${unit}` : " Spaces";
      }
      content += ` ${getLabelFromKey(area, CONFIG.DC20RPG.DROPDOWN_DATA.areaTypes)}`;
      content += "</div>";
    }
  });
  return content;
}

function _duration(item) {
  const duration =  item.system?.duration;
  let content = "";

  if (duration) {
    const type = duration.type;
    const value = duration.value;
    const timeUnit = duration.timeUnit;

    if (type && timeUnit) {
      content += "<div class='detail'>";
      content += `${getLabelFromKey(type, CONFIG.DC20RPG.DROPDOWN_DATA.durations)} (`;
      if (value) content += `${value}`;
      content += ` ${getLabelFromKey(timeUnit, CONFIG.DC20RPG.DROPDOWN_DATA.timeUnits)}`;
      content += ")</div>";
    }
    else if (type) {
      content += "<div class='detail'>";
      content += `${getLabelFromKey(type, CONFIG.DC20RPG.DROPDOWN_DATA.durations)}`;
      content += "</div>";
    }
  }
  return content;
}

function _weaponStyle(item) {
  const weaponStyle = item.system?.weaponStyle;
  if (!weaponStyle) return "";

  return `<div class='detail green-box box journal-tooltip box-style'
  data-uuid="${getLabelFromKey(weaponStyle, CONFIG.DC20RPG.SYSTEM_CONSTANTS.JOURNAL_UUID.weaponStylesJournal)}"
  data-header="${getLabelFromKey(weaponStyle, CONFIG.DC20RPG.DROPDOWN_DATA.weaponStyles)}"> 
  ${getLabelFromKey(weaponStyle, CONFIG.DC20RPG.DROPDOWN_DATA.weaponStyles)}
  </div>`;
}

function _magicSchool(item) {
  const magicSchool = item.system?.magicSchool;
  if (!magicSchool) return "";
  return `<div class='detail green-box box'> 
    ${getLabelFromKey(magicSchool, CONFIG.DC20RPG.DROPDOWN_DATA.magicSchools)}
  </div>`;
}

function _props(item) {
  const properties =  item.system?.properties;
  let content = "";
  if (properties) {
    Object.entries(properties).forEach(([key, prop]) => {
      if (prop.active) {
        content += `<div class='detail box journal-tooltip box-style'
        data-uuid="${getLabelFromKey(key, CONFIG.DC20RPG.SYSTEM_CONSTANTS.JOURNAL_UUID.propertiesJournal)}"
        data-header="${getLabelFromKey(key, CONFIG.DC20RPG.DROPDOWN_DATA.properties)}"
        > 
        ${getLabelFromKey(key, CONFIG.DC20RPG.DROPDOWN_DATA.properties)}`;
        if (prop.value) content += ` (${prop.value})`;
        content += "</div>";
      }
    });
  }
  return content;
}

function _components(item) {
  const components = item.system?.components;
  let content = "";
  if (components) {
    Object.entries(components).forEach(([key, comp]) => {
      if (comp.active) {
        content += `<div class='detail box'> ${getLabelFromKey(key, CONFIG.DC20RPG.DROPDOWN_DATA.components)}`;
        if (key === "material") {
          if (comp.description) {
            const cost = comp.cost ? ` (${comp.cost} GP)` : "";
            const consumed = comp.consumed ? " [Consumed]" : "";
            content += `: ${comp.description}${cost}${consumed}`;
          } 
        }
        content += "</div>";
      }
    });
  }
  return content;
}

function getFormulaHtmlForCategory(category, item) {
  const types = { ...CONFIG.DC20RPG.DROPDOWN_DATA.damageTypes, ...CONFIG.DC20RPG.DROPDOWN_DATA.healingTypes };
  let formulas = item.system.formulas;
  let formulaString = "";

  let filteredFormulas = Object.values(formulas)
    .filter(formula => formula.category === category);

  for (let i = 0; i < filteredFormulas.length; i++) {
    let formula = filteredFormulas[i];
    if (formula.formula === "") continue;
    formulaString += formula.formula;
    formulaString += " <em>" + getLabelFromKey(formula.type, types) + "</em>";
    formulaString += " + ";
  }

  if (formulaString !== "") formulaString = formulaString.substring(0, formulaString.length - 3);
  return formulaString;
}

function getRollRequestHtmlForCategory(category, item) {
  const rollRequests = item.system.rollRequests;
  if (!rollRequests) return "";

  const filtered = Object.values(rollRequests).filter(request => request.category === category);

  let rollRequestString = "";
  for (let i = 0; i < filtered.length; i++) {
    if (category === "save") rollRequestString += " <em>" + getLabelFromKey(filtered[i].saveKey, CONFIG.DC20RPG.ROLL_KEYS.saveTypes) + "</em>";
    if (category === "contest") rollRequestString += " <em> " + getLabelFromKey(filtered[i].contestedKey, CONFIG.DC20RPG.ROLL_KEYS.contests) + "</em>";
    rollRequestString += " or ";
  }

  if (rollRequestString !== "") rollRequestString = rollRequestString.substring(0, rollRequestString.length - 4);
  return rollRequestString;
}

function effectTooltip(effect, event, html, options={}) {
  if (!effect) return _showTooltip(html, event, "-", "Effect not found", "");
  const header = _itemHeader(effect);
  const description = `<div class='description'> ${_enhanceDescription(effect.description)} </div>`;
  _showTooltip(html, event, header, description, null, options);
}

function itemTooltip(item, event, html, options={}) {
  if (!item) return _showTooltip(html, event, "-", "Item not found", "");

  const header = _itemHeader(item);
  const description = _itemDescription$1(item);
  const details = _itemDetails$1(item);
  _showTooltip(html, event, header, description, details, options);
}

function traitTooltip(trait, event, html, options={}) {
  if (!trait) return _showTooltip(html, event, "-", "Trait not found", "");

  const header = _itemHeader(trait.itemData);
  const description = _itemDescription$1(trait.itemData);
  const details = _itemDetails$1(trait.itemData);
  _showTooltip(html, event, header, description, details, options);
}

function enhTooltip(item, enhKey, event, html, options={}) {
  if(!item) return _showTooltip(html, event, "-", "Item not found", "");
  const enhancement = item.allEnhancements.get(enhKey);
  if(!enhancement) return _showTooltip(html, event, "-", "Enhancement not found", "");

  const header = `<input disabled value="${enhancement.name}"/>`;
  const description = `<div class='description'> ${_enhanceDescription(enhancement.description)} </div>`;
  _showTooltip(html, event, header, description, null, options);
}

function textTooltip(text, title, img, event, html, options={}) {
  const description = `<div class='description'> ${text} </div>`;
  let tooltipHeader = '';
  if (title) {
    if (img) tooltipHeader += `<img src="${img}"/>`;
    tooltipHeader += `<input disabled value="${title}"/>`;
  }
  _showTooltip(html, event, tooltipHeader, description, null, options);
}

async function journalTooltip(uuid, header, img, event, html, options={}) {
  const page = await fromUuid(uuid);
  if (!page) return;

  const description = page.text.content;
  let imgHeader = "";
  if (img !== undefined) imgHeader = `<img src="${img}" style="background-color:black;"/>`;
  const tooltipHeader = `${imgHeader}<input disabled value="${header}"/>`;
  _showTooltip(html, event, tooltipHeader, description, null, options);
}

function hideTooltip(event, html) {
  event.preventDefault();
  if (event.altKey) return;

  const tooltip = html.find("#tooltip-container");
  tooltip[0].style.opacity = 0;
  tooltip[0].style.visibility = "hidden";
}

function _showTooltip(html, event, header, description, details, options) {
  const tooltip = html.find("#tooltip-container");

  // If tooltip is already visible we dont want other tooltips to appear
  if(tooltip[0].style.visibility === "visible") return;

  _showHidePartial(header, tooltip.find(".tooltip-header"));
  _showHidePartial(description, tooltip.find(".tooltip-description"));
  _showHidePartial(details, tooltip.find(".tooltip-details"));
  _setPosition(event, tooltip, options);
  _addEventListener$1(tooltip);

  tooltip.contextmenu(() => {
    if (tooltip.oldContent && tooltip.oldContent.length > 0) {
      const oldContent = tooltip.oldContent.pop();
      _swapTooltipContent(tooltip, oldContent.header, oldContent.description, oldContent.details);
    }
  });

  // Visibility
  tooltip[0].style.opacity = 1;
  tooltip[0].style.visibility = "visible";
}

function _addEventListener$1(tooltip) {
  // Repleace Content
  tooltip.find('.journal-tooltip').click(async ev => {
    const data = datasetOf(ev);
    if (tooltip.oldContent === undefined) tooltip.oldContent = [];

    const page = await fromUuid(data.uuid);
    if (!page) return;

    // We need to store old tooltips so we could go back
    tooltip.oldContent.push({
      header: tooltip.find(".tooltip-header").html(),
      description: tooltip.find(".tooltip-description").html(),
      details: tooltip.find(".tooltip-details").html()
    });

    const description = page.text.content;
    let imgHeader = "";
    if (data.img !== undefined) imgHeader = `<img src="${data.img}" style="background-color:black;"/>`;
    const tooltipHeader = `${imgHeader}<input disabled value="${data.header}"/>`;
    _swapTooltipContent(tooltip, tooltipHeader, description, null);
  });

  tooltip.find('.item-tooltip').click(async ev => {
    const data = datasetOf(ev);
    if (tooltip.oldContent === undefined) tooltip.oldContent = [];

    const item = await fromUuid(data.uuid);
    if (!item) return;

    // We need to store old tooltips so we could go back
    tooltip.oldContent.push({
      header: tooltip.find(".tooltip-header").html(),
      description: tooltip.find(".tooltip-description").html(),
      details: tooltip.find(".tooltip-details").html()
    });

    const header = _itemHeader(item);
    const description = _itemDescription$1(item);
    const details = _itemDetails$1(item);
    _swapTooltipContent(tooltip, header, description, details);
  });
}

function _swapTooltipContent(tooltip, header, description, details) {
  _showHidePartial(header, tooltip.find(".tooltip-header"));
  _showHidePartial(description, tooltip.find(".tooltip-description"));
  _showHidePartial(details, tooltip.find(".tooltip-details"));
  _addEventListener$1(tooltip);
}

function _showHidePartial(value, partial) {
  if (value) {
    partial.html(value);
    partial.removeClass("invisible");
  }
  else {
    partial.html(null);
    partial.addClass("invisible");
  }
}

function _setPosition(event, tooltip, options) {
    // Force height and width if provided
    if (options.position) {
      const pos = options.position; 

      if (pos.height) tooltip.height(pos.height);
      if (pos.width) tooltip.width(pos.width);
    }
    else {
      tooltip[0].style.maxHeight = "500px";
      tooltip[0].style.minWidth = "300px";

      // Horizontal position
      const height = tooltip[0].getBoundingClientRect().height;
      tooltip[0].style.top = (event.pageY - (height/2)) + "px";
      const bottom = tooltip[0].getBoundingClientRect().bottom;
      const top = tooltip[0].getBoundingClientRect().top;
      const viewportHeight = window.innerHeight;
      
      // We dont want our tooltip to exit top nor bottom borders
      if (bottom > viewportHeight) {
        tooltip[0].style.top = (viewportHeight - height) + "px";
      }
      if (top < 0) tooltip[0].style.top = "0px";
      // Vertical position
      tooltip[0].style.left = "";
      const left = tooltip[0].getBoundingClientRect().left;
      const width = tooltip[0].getBoundingClientRect().width;
      if (!options.inside) tooltip[0].style.left = (left - width) + "px";
      if (tooltip[0].getBoundingClientRect().left < 0) {
        // In the case that tooltip exits window areas we want to put it next to the cursor
        const cursorPosition = event.pageX;
        tooltip[0].style.left = (cursorPosition + 50) + "px";
      }
    } 
}

function _itemHeader(item) {
  return `
    <img src="${item.img}"/>
    <input disabled value="${item.name}"/>
  `
}

function _itemDescription$1(item) {
  if (!item.system) return `<div class='description'> <b>Item not found</b> </div>`
  const identified = item.system.statuses ? item.system.statuses.identified : true;
  const description = item.system.description;
  const enhDescription = _enhanceDescription(description);
  if (identified) return `<div class='description'> ${enhDescription} </div>`;
  else return `<div class='description'> <b>UNIDENTIFIED</b> </div>`;
}

function _enhanceDescription(description) {
  const uuidRegex = /@UUID\[[^\]]*]\{[^}]*}/g;
  const itemLinks = [...description.matchAll(uuidRegex)];
  itemLinks.forEach(link => {
    link = link[0];
    let [uuid, name] = link.split("]{");    
    // Remove "trash"
    uuid = uuid.slice(6);
    name = name.slice(0, name.length- 1);

    let tooltipLink = ""; 
    if (uuid.includes(".Item.")) tooltipLink = `<span class="item-tooltip hyperlink-style" data-uuid="${uuid}">${name}</span>`;
    else if (uuid.includes(".JournalEntryPage.")) tooltipLink = `<span class="journal-tooltip hyperlink-style" data-uuid="${uuid}" data-header="${name}">${name}</span>`;
    else tooltipLink = `<span><b>${name}</b></span>`;
    description = description.replace(link, tooltipLink);
  });
  return description;
}

function _itemDetails$1(item) {
  const identified = item?.system?.statuses ? item.system.statuses.identified : true;
  if (identified) return itemDetailsToHtml(item, true);
  else return null;
}

class CompendiumBrowser extends Dialog {

  constructor(itemType, lockItemType, parentWindow, preSelectedFilters, dialogData = {}, options = {}) {
    super(dialogData, options);
    this.collectedItems = [];
    this.collectedItemCache = {};
    this.lockItemType = lockItemType;
    this.parentWindow = parentWindow;
    this.filters = getDefaultItemFilters(preSelectedFilters);

    if (itemType === "inventory") {
      this.allItemTypes = CONFIG.DC20RPG.DROPDOWN_DATA.inventoryTypes;
      itemType = "weapon";
    }
    else if (itemType === "advancement") {
      this.allItemTypes = {
        ...CONFIG.DC20RPG.DROPDOWN_DATA.featuresTypes,
        ...CONFIG.DC20RPG.DROPDOWN_DATA.spellsTypes,
        ...CONFIG.DC20RPG.DROPDOWN_DATA.techniquesTypes
      };
      itemType = "feature";
    }
    else {
      this.allItemTypes = CONFIG.DC20RPG.DROPDOWN_DATA.allItemTypes;
    }
    this._collectItems(itemType);
  }

  static get defaultOptions() {
    return foundry.utils.mergeObject(super.defaultOptions, {
      template: "systems/dc20rpg/templates/dialogs/compendium-browser/item-browser.hbs",
      classes: ["dc20rpg", "dialog"],
      dragDrop: [
        {dragSelector: ".item-row[data-uuid]", dropSelector: null},
      ],
      width: 850,
      height: 650,
      resizable: true,
      draggable: true,
    });
  }

  async getData() {
    const itemSpecificFilters = this._getFilters();
    const filteredItems = filterDocuments(this.collectedItems, itemSpecificFilters);
    
    return {
      itemType: this.currentItemType,
      collectedItems: filteredItems,
      collectingData: this.collectingData,
      lockItemType: this.lockItemType,
      allItemTypes: this.allItemTypes,
      filters: itemSpecificFilters,
      canAddItems: this.parentWindow !== undefined
    }
  }

  async _collectItems(itemType) {
    // We do not need to refresh if the same item type was selected
    if (this.currentItemType === itemType) return; 

    this.collectingData = true;
    this.currentItemType = itemType;
    
    // If we already collected that item type before we can get it from cache
    if (this.collectedItemCache[itemType]) {
      this.collectedItems = this.collectedItemCache[itemType];
    }
    // If there is nothing in the cache we need to collect those items
    else {
      this.collectedItems = await collectItemsForType(itemType);
      this.collectedItemCache[itemType] = this.collectedItems;
    }
    this.collectingData = false;
    this.render(true);
  }

  _getFilters() {
    const typeSpecific = this.filters[this.currentItemType] || {};
    const filters = [
      this.filters.name,
      ...Object.values(typeSpecific),
      this.filters.compendium,
      this.filters.sourceName,
    ];
    return filters;
  }

  activateListeners(html) {
    super.activateListeners(html);
    activateDefaultListeners(this, html);
    html.find(".show-item").click(ev => this._onItemShow(ev));
    html.find(".add-item").click(ev => this._onAddItem(ev));
    html.find(".select-type").change(ev => this._onSelectType(valueOf(ev)));

    html.find('.item-tooltip').hover(ev => {
      let position = null;
      const column = html.find(".filter-column");
      if (column[0]) {
        position = {
          width: column.width() - 10,
          height: column.height() - 10,
        };
      }
      const uuid = datasetOf(ev).uuid;
      const item = fromUuidSync(uuid);
      if (item) itemTooltip(item, ev, html, {position: position});
    },
    ev => hideTooltip(ev, html));

    // Drag and drop events
    html[0].addEventListener('dragover', ev => ev.preventDefault());
  }

  _onSelectType(value) {
    this._collectItems(value);
    this.render(true);
  }

  _onValueChange(path, value) {
    setValueForPath(this, path, value);
    this.render(true);
  }

  _onActivable(path) {
    let value = getValueFromPath(this, path);
    setValueForPath(this, path, !value);
    this.render(true);
  }

  _onItemShow(ev) {
    const uuid = datasetOf(ev).uuid;
    const item = fromUuidSync(uuid);
    if (item) item.sheet.render(true);
  }

  _onAddItem(ev) {
    ev.stopPropagation();
    const uuid = datasetOf(ev).uuid;
    if (!uuid) return;

    const parentWindow = this.parentWindow;
    if (!parentWindow) return;

    const dragData = {
      uuid:  uuid,
      type: "Item"
    };
    const dragEvent = new DragEvent('dragstart', {
      bubbles: true,
      cancelable: true,
      dataTransfer: new DataTransfer()
    });
    dragEvent.dataTransfer.setData("text/plain", JSON.stringify(dragData));
    parentWindow._onDrop(dragEvent);

    this.render(true);
  }

  async _render(...args) {
    const selector = this.element.find('.item-selector');
    let scrollPosition = 0;

    if (selector.length > 0) scrollPosition = selector[0].scrollTop;
    await super._render(...args);
    if (selector.length > 0) {
      this.element.find('.item-selector')[0].scrollTop = scrollPosition;
    }
  }

  _onDragStart(event) {
    const dataset = event.currentTarget.dataset;
    dataset.type = "Item";
    event.dataTransfer.setData("text/plain", JSON.stringify(dataset));
  }

  _canDragDrop(selector) {
    return true;
  }

  _canDragStart(selector) {
    return true;
  }

  setPosition(position) {
    super.setPosition(position);

    this.element.css({
      "min-height": "400px",
      "min-width": "600px",
    });
    this.element.find("#compendium-browser").css({
      height: this.element.height() -30,
    });
  }
}

let itemBrowserInstance = null;
function createItemBrowser(itemType, lockItemType, parentWindow, preSelectedFilters) {
  if (itemBrowserInstance) itemBrowserInstance.close();
  const dialog = new CompendiumBrowser(itemType, lockItemType, parentWindow, preSelectedFilters, {title: `Item Browser`});
  dialog.render(true);
  itemBrowserInstance = dialog;
}

function updateScalingValues(item, dataset, value) {
  const key = dataset.key;
  const index = parseInt(dataset.index);
  const newValue = parseInt(value);

  const currentArray = item.system.scaling[key].values;
  currentArray[index] = newValue;
  item.update({[`system.scaling.${key}.values`]: currentArray});
}

function updateResourceValues(item, index, value) {
  index = parseInt(index);
  value = parseInt(value);

  const currentArray = item.system.resource.values;
  currentArray[index] = value;
  item.update({[`system.resource.values`]: currentArray});
}

function overrideScalingValue(item, index, mastery) {
  if (!item.system.talentMasteries) return;

  const talentMasteriesPerLevel = item.system.talentMasteries;
  talentMasteriesPerLevel[index] = mastery;
  const numberOfTalents = talentMasteriesPerLevel.filter(elem => elem === mastery).length;
  const overridenScalingValues = _overridenScalingValues(item, mastery, numberOfTalents, index, false);

  const updateData = {
    ...overridenScalingValues,
    [`system.talentMasteries`]: talentMasteriesPerLevel
  };
  item.update(updateData);
  return numberOfTalents;
}

async function clearOverridenScalingValue(item, index) {
    const talentMasteriesPerLevel = item.system.talentMasteries;
    const mastery = talentMasteriesPerLevel[index];
    const numberOfTalents = talentMasteriesPerLevel.filter(elem => elem === mastery).length;
    talentMasteriesPerLevel[index] = "";
    const clearedScalingValues = _overridenScalingValues(item, mastery, numberOfTalents, index, true);

    const updateData = {
      ...clearedScalingValues,
      [`system.talentMasteries`]: talentMasteriesPerLevel
    };
    await item.update(updateData);
}

function _overridenScalingValues(item, mastery, talentNumber, index, clearOverriden) {
  let operator = 1;
  if (clearOverriden) operator = -1;

  if (mastery === "martial") {
    item.system.scaling.bonusStamina.values[index] += (operator * 1);
    item.system.scaling.maneuversKnown.values[index] += (operator * 1);
    item.system.scaling.techniquesKnown.values[index] += (operator * 1);

    if (talentNumber % 2 === 0) return {
      [`system.scaling.maneuversKnown.values`]: item.system.scaling.maneuversKnown.values,
    }
    else return {
      [`system.scaling.bonusStamina.values`]: item.system.scaling.bonusStamina.values,
      [`system.scaling.maneuversKnown.values`]: item.system.scaling.maneuversKnown.values,
      [`system.scaling.techniquesKnown.values`]: item.system.scaling.techniquesKnown.values
    }
  }
  if (mastery === "spellcaster") {
    item.system.scaling.bonusMana.values[index] += (operator * 2);
    item.system.scaling.cantripsKnown.values[index] += (operator * 1);
    item.system.scaling.spellsKnown.values[index] += (operator * 1);

    if (talentNumber % 2 === 0) return {
      [`system.scaling.bonusMana.values`]: item.system.scaling.bonusMana.values,
      [`system.scaling.spellsKnown.values`]: item.system.scaling.spellsKnown.values
    }
    else return {
      [`system.scaling.bonusMana.values`]: item.system.scaling.bonusMana.values,
      [`system.scaling.cantripsKnown.values`]: item.system.scaling.cantripsKnown.values,
      [`system.scaling.spellsKnown.values`]: item.system.scaling.spellsKnown.values
    }
  }
}

function canApplyAdvancement(advancement) {
  if (advancement.mustChoose && advancement.pointsLeft !== 0) {
    ui.notifications.error(`Spend correct amount of Choice Points! Points Left: ${advancement.pointsLeft}`); 
    return false;
  }
  if (advancement.progressPath && !advancement.mastery) {
    ui.notifications.error("Choose Spellcaster or Martial Path Progression!");
    return false;
  }
  return true;
}

async function applyAdvancement(advancement, actor, owningItem) {
  let selectedItems = advancement.items;
  if (advancement.mustChoose) selectedItems = Object.fromEntries(Object.entries(advancement.items).filter(([key, item]) => item.selected));

  const extraAdvancements = await _addItemsToActor(selectedItems, actor, advancement, owningItem);
  
  // Check for Martial Expansion that comes from the class, Martial Path or some other items
  let martialExpansion = _checkMartialExpansion(owningItem, advancement, actor);
  if (martialExpansion) extraAdvancements.set("martialExpansion", martialExpansion);
  
  if (advancement.repeatable) await _addRepeatableAdvancement(advancement, owningItem);
  if (advancement.progressPath) await _applyPathProgression(advancement, owningItem, extraAdvancements);

  await _markAdvancementAsApplied(advancement, owningItem, actor);
  if (advancement.addItemsOptions?.talentFilter) await _fillMulticlassInfo(advancement, actor);
  return extraAdvancements.values();
}

async function addAdditionalAdvancement(advancement, item, advancementCollection) {
  advancement.additionalAdvancement = true;
  advancementCollection.push([advancement.key, advancement]);
  await item.update({[`system.advancements.${advancement.key}`]: advancement});
}

async function addNewSpellTechniqueAdvancements(actor, item, advancementCollection, level) {
  const addedAdvancements = [];
  for (const [key, known] of Object.entries(actor.system.known)) {
    const newKnownAmount = known.max - known.current;
    if (newKnownAmount > 0) {
      const advancement = createNewAdvancement();
      advancement.name = game.i18n.localize(`dc20rpg.known.${key}`);
      advancement.allowToAddItems = true;
      advancement.customTitle = `You gain new ${advancement.name} (${newKnownAmount})`;
      advancement.level = level;
      advancement.addItemsOptions = {
        helpText: `Add ${advancement.name}`,
        itemLimit: newKnownAmount
      };
      _prepareCompendiumFilters(advancement, key);
      await addAdditionalAdvancement(advancement, item, advancementCollection);
      addedAdvancements.push(advancement);
    }
  }
  return addedAdvancements;
}

async function shouldLearnAnyNewSpellsOrTechniques(actor) {
  actor = await refreshActor(actor);
  for (const [key, known] of Object.entries(actor.system.known)) {
    if (known.max - known.current > 0) return true;
  }
  return false;
}

async function _applyPathProgression(advancement, item, extraAdvancements) {
  const index = advancement.level -1;
  switch(advancement.mastery) {
    case "martial":
      const numberOfMartialPaths = overrideScalingValue(item, index, "martial"); 
      if (numberOfMartialPaths === 2 && !item.system.martial) {
        const expansion = _getSpellcasterStaminaAdvancement();
        expansion.level = advancement.level;
        expansion.key = "spellcasterStamina";
        extraAdvancements.set(expansion.key, expansion);
      }
      break;

    case "spellcaster":
      overrideScalingValue(item, index, "spellcaster"); 
      break;
  }
}

async function _addItemsToActor(items, actor, advancement, owningItem) {
  let extraAdvancements = new Map();
  for (const [key, record] of Object.entries(items)) {
    const item = await fromUuid(record.uuid);
    const created = await createItemOnActor(actor, item);

    // Check if has extra advancements
    const extraAdvancement = _extraAdvancement(created);
    if (extraAdvancement) {
      extraAdvancement.level = advancement.level;
      extraAdvancements.set(extraAdvancement.key, extraAdvancement);
    }

    const martialExpansion = _martialExpansion(created, actor, owningItem);
    if (martialExpansion) {
      martialExpansion.level = advancement.level;
      extraAdvancements.set(martialExpansion.key, martialExpansion);
    }

    // Add created id to advancement record
    if (record.ignoreKnown) created.update({["system.knownLimit"]: false});
    record.createdItemId = created._id;
    advancement.items[key] = record;
  }
  return extraAdvancements;
}

async function _markAdvancementAsApplied(advancement, owningItem, actor) {
  advancement.applied = true;
  advancement.featureSourceItem = ""; // clear filter
  advancement.hideRequirementMissing = false; // clear filter
  advancement.hideOwned = true; // clear filter
  advancement.itemNameFilter = ""; // clear filter
  await owningItem.update({[`system.advancements.${advancement.key}`]: advancement});
  if (advancement.key === "martialExpansion") await actor.update({["system.details.martialExpansionProvided"]: true});
}

function _extraAdvancement(item) {
  // Additional Advancement
  if (item.system.hasAdvancement) {
    const additional = item.system.advancements.default;
    additional.key = generateKey();
    return additional;
  }
  return null;
}

function _martialExpansion(item, actor, owningItem) {
  // Martial Expansion
  if (item.system.provideMartialExpansion && !actor.system.details.martialExpansionProvided && !owningItem.martialExpansionProvided) {
    const expansion = _getMartialExpansionAdvancement();
    expansion.key = "martialExpansion";
    owningItem.martialExpansionProvided = true;
    return expansion;
  }
  return null;
}

function _checkMartialExpansion(item, advancement, actor) {
  if (actor.system.details.martialExpansionProvided || item.martialExpansionProvided) return null;

  const fromItem = item.system.martialExpansion;
  const fromMartialPath = advancement.progressPath && advancement.mastery === "martial";
  if (fromItem || fromMartialPath) {
    const expansion = _getMartialExpansionAdvancement();
    expansion.level = advancement.level;
    expansion.key = "martialExpansion";
    item.martialExpansionProvided = true;
    return expansion;
  }
  return null;
}

async function _addRepeatableAdvancement(oldAdv, owningItem) {
  let nextLevel = null;
  // Collect next level where advancement should appear
  for (let i = oldAdv.level + 1; i <= 20; i++) {
    const choicePoints = oldAdv.repeatAt[i];
    if (choicePoints > 0) {
      nextLevel = {
        level: i,
        pointAmount: choicePoints
      };
      break;
    }
  }
  if (nextLevel === null) return;

  // If next level advancement was already created before we want to replace it, if not we will create new one
  const advKey = oldAdv.cloneKey || generateKey();
  const newAdv = foundry.utils.deepClone(oldAdv);
  newAdv.pointAmount = nextLevel.pointAmount;
  newAdv.level = nextLevel.level;
  newAdv.additionalAdvancement = false;
  newAdv.cloneKey = null;

  // Remove already added items
  const filteredItems = Object.fromEntries(
    Object.entries(newAdv.items).filter(([key, item]) => !item.selected)
  );

  oldAdv.cloneKey = advKey;
  newAdv.items = filteredItems;

  // We want to clear item list before we add new ones
  if(oldAdv.cloneKey) await owningItem.update({[`system.advancements.${advKey}.-=items`]: null});
  await owningItem.update({
    [`system.advancements.${advKey}`]: newAdv,
    [`system.advancements.${oldAdv.key}`]: oldAdv,
  });
}

function _getMartialExpansionAdvancement() {
  const martialExpansion = fromUuidSync(CONFIG.DC20RPG.SYSTEM_CONSTANTS.martialExpansion);
  if (!martialExpansion) {
    ui.notifications.warn("Martial Expansion Item cannot be found");
    return;
  }
  const advancement = Object.values(martialExpansion.system.advancements)[0];
  advancement.customTitle = advancement.name;
  return advancement;
}

function _getSpellcasterStaminaAdvancement() {
  const spellcasterStamina = fromUuidSync(CONFIG.DC20RPG.SYSTEM_CONSTANTS.spellcasterStamina);
  if (!spellcasterStamina) {
    ui.notifications.warn("Spellcaster Stamina Expansion Item cannot be found");
    return;
  }
  const advancement = Object.values(spellcasterStamina.system.advancements)[0];
  advancement.customTitle = advancement.name;
  return advancement;
}

function _prepareCompendiumFilters(advancement, key) {
  switch(key) {
    case "cantrips":
      advancement.addItemsOptions.itemType = "spell";
      advancement.addItemsOptions.preFilters = '{"spellType": "cantrip"}';
      break;
    case "spells":
      advancement.addItemsOptions.itemType = "spell";
      advancement.addItemsOptions.preFilters = '{"spellType": "spell"}';
      break;
    case "maneuvers":
      advancement.addItemsOptions.itemType = "technique";
      advancement.addItemsOptions.preFilters = '{"techniqueType": "maneuver"}';
      break;
    case "techniques":
      advancement.addItemsOptions.itemType = "technique";
      advancement.addItemsOptions.preFilters = '{"techniqueType": "technique"}';
      break;
  }
}

async function _fillMulticlassInfo(advancement, actor) {
  if (advancement.talentFilterType === "general" || advancement.talentFilterType === "class") return;
  const options = {...CONFIG.DC20RPG.UNIQUE_ITEM_IDS.class, ...CONFIG.DC20RPG.UNIQUE_ITEM_IDS.subclass};
  const classTalent = await getSimplePopup("select", {header: "What Class/Subclass is that Multiclass Talent from?", selectOptions: options});
  if (classTalent) {
    await actor.update({[`system.details.advancementInfo.multiclassTalents.${advancement.key}`]: classTalent});
  }
}

function markItemRequirements(items, talentFilterType, actor) {
  for (const item of items) {
    const requirements = item.system.requirements;
    let requirementMissing = "";

    // Required Level
    const actorLevel = actor.system.details.level;
    if (requirements.level > actorLevel) {
      requirementMissing += `Required Level: ${requirements.level}`;
    }

    // Required Item
    if (requirements.items) {
      const itemNames = requirements.items.split(',');
      for (const name of itemNames) {
        if (actor.items.filter(item => item.name === name).length === 0) {
          if (requirementMissing !== "") requirementMissing += "\n"; 
          requirementMissing += `Missing Required Item: ${name}`;
        }
      }
    }

    const baseClassKey = actor.system.details.class.classKey;
    const multiclass = Object.values(actor.system.details.advancementInfo.multiclassTalents);
    // Subclass 3rd level feature requires at least one feature from Class or needs to be from your class
    if (["expert", "master", "grandmaster", "legendary"].includes(talentFilterType)) {
      if (item.system.featureType === "subclass" && requirements.level === 3) {
        const subclassKey = item.system.featureSourceItem;
        const classKey = CONFIG.DC20RPG.SUBCLASS_CLASS_LINK[subclassKey];
        if (!multiclass.find(key => key === classKey) && classKey !== baseClassKey) {
          const className = CONFIG.DC20RPG.UNIQUE_ITEM_IDS.class[classKey];
          if (requirementMissing !== "") requirementMissing += "\n"; 
          requirementMissing += `Requires at least one talent from ${className} Class`;
        }
      }
    }
    // Subclass 6th level feature requires at least one feature from that Subclass 
    if (["master", "grandmaster", "legendary"].includes(talentFilterType)) {
      if (item.system.featureType === "subclass" && requirements.level === 6) {
        const subclassKey = item.system.featureSourceItem;
        if (!multiclass.find(key => key === subclassKey)) {
          const subclassName = CONFIG.DC20RPG.UNIQUE_ITEM_IDS.subclass[subclassKey];
          if (requirementMissing !== "") requirementMissing += "\n"; 
          requirementMissing += `Requires at least one talent from ${subclassName} Subclass`;
        }
      }
    }
    // Class Capstone 8th level feature requires at least two features from that Class 
    if (["grandmaster", "legendary"].includes(talentFilterType)) {
      if (item.system.featureType === "class" && requirements.level === 8) {
        const classKey = item.system.featureSourceItem;
        if (multiclass.filter(key => key === classKey).length < 2) {
          const className = CONFIG.DC20RPG.UNIQUE_ITEM_IDS.class[classKey];
          if (requirementMissing !== "") requirementMissing += "\n"; 
          requirementMissing += `Requires at least two talents from ${className} Class`;
        }
      }
    }
    // Subclass Capstone 9th level feature requires at least two features from that Subclass 
    if (["legendary"].includes(talentFilterType)) {
      if (item.system.featureType === "subclass" && requirements.level === 9) {
        const subclassKey = item.system.featureSourceItem;
        if (multiclass.filter(key => key === subclassKey).length < 2) {
          const subclassName = CONFIG.DC20RPG.UNIQUE_ITEM_IDS.subclass[subclassKey];
          if (requirementMissing !== "") requirementMissing += "\n"; 
          requirementMissing += `Requires at least two talents from ${subclassName} Subclass`;
        }
      }
    }
    if (requirementMissing) item.requirementMissing = requirementMissing;
  }
}

async function collectScalingValues(actor, oldSystemData) {
  await refreshActor(actor);
  const scalingValues = [];

  const resources = actor.system.resources;
  const oldResources = oldSystemData.resources;
  
  // Go over core resources and collect changes
  Object.entries(resources).forEach(([key, resource]) => {
    if (key === "custom") ;
    else if (resource.max !== oldResources[key].max) {
      scalingValues.push({
        resourceKey: key,
        label: game.i18n.localize(`dc20rpg.resource.${key}`),
        previous: oldResources[key].max,
        current: resource.max
      });
    }
  });

  // Go over custom resources
  Object.entries(resources.custom).forEach(([key, custom]) => {    
    if (custom.max !== oldResources.custom[key]?.max) {
      scalingValues.push({
        resourceKey: key,
        custom: true,
        label: custom.name,
        previous: oldResources.custom[key]?.max || 0,
        current: custom.max
      });
    }
  });
  return scalingValues;
}

async function refreshActor(actor) {
  const counter = actor.flags.dc20rpg.advancementCounter + 1;
  return await actor.update({[`flags.dc20rpg.advancementCounter`]: counter});
}

async function collectSubclassesForClass(classKey) {
  const dialog =  new SimplePopup("non-closable", {header: "Collecting Subclasses", message: "Collecting Subclasses... Please wait it might take a while"}, {title: "Collecting Subclasses"});
  await dialog._render(true);

  const matching = [];
  for (const pack of game.packs) {
    if (!validateUserOwnership(pack)) continue;

    if (pack.documentName === "Item") {
      const items = await pack.getDocuments();
      items.filter(item => item.type === "subclass")
            .filter(item => item.system.forClass.classSpecialId === classKey)
            .forEach(item => matching.push(item));
    }
  }
  
  dialog.close();
  return matching;
}

/**
 * Configuration of advancements on item
 */
class ActorAdvancement extends Dialog {

  constructor(actor, advForItems, oldSystemData, openSubclassSelector, dialogData = {}, options = {}) {
    super(dialogData, options);

    this.knownApplied = false;
    this.showFinal = false;
    this.spendPoints = false;
    
    this.actor = actor;
    this.advForItems = advForItems;
    this.oldSystemData = oldSystemData;
    this.tips = [];
    
    this.suggestionsOpen = false;
    this.itemSuggestions = [];
    if (openSubclassSelector) this._selectSubclass();
    else this._prepareData();
  }

  static get defaultOptions() {
    return foundry.utils.mergeObject(super.defaultOptions, {
      template: "systems/dc20rpg/templates/dialogs/character-progress/advancement-dialog.hbs",
      classes: ["dc20rpg", "dialog"],
      width: 1200,
      height: 800,
      resizable: true,
      draggable: true,
    });
  }

  async _selectSubclass() {
    if (this.advForItems.subclass) return this._prepareData();
    const subclasses = await collectSubclassesForClass(this.actor.system.details.class.classKey);
    if (subclasses.length === 0) return this._prepareData();
    this.selectSubclass = subclasses;
    this.render();
  }

  _prepareData() {
    if (this.selectSubclass) return;
    const current = Object.values(this.advForItems)[0];
    if (!current) {this.showFinal = true; return;}
    
    const currentItem = current.item;
    if (!currentItem) {this.close(); return;}

    const advancementsForCurrentItem =  Object.entries(current.advancements);
    const currentAdvancement = advancementsForCurrentItem[0];
    if (!currentAdvancement) {this.close(); return;}

    // Set first item
    this.currentItem = currentItem;
    this.itemIndex = 0;
    
    // Set first advancement
    this.advancementsForCurrentItem = advancementsForCurrentItem;
    this.currentAdvancement = currentAdvancement[1];
    this.currentAdvancementKey = currentAdvancement[0];
    this._prepareItemSuggestions();
    this.advIndex = 0;
  }

  hasNext() {
    const nextAdvancement = this.advancementsForCurrentItem[this.advIndex + 1];
    if (nextAdvancement) return true;
    
    const nextItem =  Object.values(this.advForItems)[this.itemIndex + 1];
    if (nextItem) return true;
    else return false;
  }

  next() {
    const nextAdvancement = this.advancementsForCurrentItem[this.advIndex + 1];
    if (nextAdvancement) {
      this.currentAdvancement = nextAdvancement[1];
      this.currentAdvancementKey = nextAdvancement[0];
      this._prepareItemSuggestions();
      this.advIndex++;
      return;
    }
    
    const next = Object.values(this.advForItems)[this.itemIndex + 1];
    if (!next) {
      ui.notifications.error("Advancement cannot be progressed any further");
      this.close();
      return;
    }

    const nextItem = next.item;
    if (!nextItem) {
      ui.notifications.error("Advancement cannot be progressed any further");
      this.close();
      return;
    }

    const advancementsForItem = Object.entries(next.advancements);
    const currentAdvancement = advancementsForItem[0];

    if (!currentAdvancement) {
      ui.notifications.error("Advancement cannot be progressed any further");
      this.close();
      return;
    }

    // Go to next item  
    this.currentItem = nextItem;
    this.itemIndex++;

    // Reset advancements
    this.advancementsForCurrentItem = advancementsForItem;
    this.currentAdvancement = currentAdvancement[1];
    this.currentAdvancementKey = currentAdvancement[0];
    this._prepareItemSuggestions();
    this.advIndex = 0;
  }

  //=====================================
  //              Get Data              =  
  //=====================================
  async getData() {
    const scalingValues = await collectScalingValues(this.actor, this.oldSystemData);
    const skillPoints = {
      attributePoints: this.actor.system.attributePoints,
      skillPoints: this.actor.system.skillPoints,
    };
    const multiclass = this._getLevelMulticlassOption();
    const talentFilterTypes = {
      general: "General Talent",
      class: "Class Talent",
      ...multiclass
    };
    const multiclassTooltip = {
      key: Object.keys(multiclass)[0],
      header: Object.values(multiclass)[0]
    };
    const advancementData = await this._getCurrentAdvancementData();
    const showItemSuggestions = this._shouldShowItemSuggestions(advancementData);
    if (!showItemSuggestions) this.suggestionsOpen = false;

    return {
      suggestionsOpen: this.suggestionsOpen,
      suggestions: this._filterSuggestedItems(),
      showItemSuggestions: showItemSuggestions,
      talentFilterTypes: talentFilterTypes,
      featureSourceItems: this._prepareFeatureSourceItems(multiclass),
      multiclassTooltip: multiclassTooltip,
      applyingAdvancement: this.applyingAdvancement,
      tips: this.tips,
      actor: this.actor,
      showFinal: this.showFinal,
      scalingValues: scalingValues,
      points: skillPoints,
      selectSubclass: this.selectSubclass,
      advancement: advancementData,
      ...this._prepareSkills()
    }
  }

  _getLevelMulticlassOption() {
    const actorLevel = this.actor.system.details.level;
    if (actorLevel >= 17) return {legendary: "Legendary Multiclass"}
    if (actorLevel >= 13) return {grandmaster: "Grandmaster Multiclass"}
    if (actorLevel >= 10) return {master: "Master Multiclass"}
    if (actorLevel >= 7) return {expert: "Expert Multiclass"}
    if (actorLevel >= 4) return {adept: "Adept Multiclass"}
    return {basic: "Multiclass"}
  }

  _shouldShowItemSuggestions(advancementData) {
    if (!advancementData.allowToAddItems) return false;
    const itemLimit = advancementData.addItemsOptions?.itemLimit;
    if (!itemLimit) return true;
    return advancementData.removableItemsAdded < itemLimit;
  }

  _prepareFeatureSourceItems(multiclass) {
    if (this.currentItem?.type === "class") {
      const multiclassType = Object.keys(multiclass);
      if (["basic", "adept"].includes(multiclassType[0])) return CONFIG.DC20RPG.UNIQUE_ITEM_IDS.class;
      
      let list = {}; 
      Object.entries(CONFIG.DC20RPG.UNIQUE_ITEM_IDS.class).forEach(([key, label]) => {
        list[key] = `[Class] ${label}`;
      });
      Object.entries(CONFIG.DC20RPG.UNIQUE_ITEM_IDS.subclass).forEach(([key, label]) => {
        list[key] = `[Subclass] ${label}`;
      });
      return list;
    }
    if (this.currentItem?.type === "ancestry") {
      this.currentAdvancement.ancestryFilter = true;
      return CONFIG.DC20RPG.UNIQUE_ITEM_IDS.ancestry;
    }
    return null;
  }

  async _getCurrentAdvancementData() {
    const advancement = this.currentAdvancement;
    if (!advancement) return {};

    // Prepare missing filters 
    if (advancement.hideOwned === undefined) advancement.hideOwned = true;

    let removableItemsAdded = 0;
    // Collect items that are part of advancement
    for(const record of Object.values(advancement.items)) {
      const item = await fromUuid(record.uuid);
      if (!item) {
        ui.notifications.error(`Advancement corrupted, cannot find saved items.`);
        return advancement;
      } 
      record.img = item.img;
      record.name = item.name;
      record.description = await TextEditor.enrichHTML(item.system.description, {secrets:true});
      if (record.removable) removableItemsAdded++;
    }

    // Determine how many points left to spend
    if (advancement.mustChoose) {
      const choosen = Object.fromEntries(Object.entries(advancement.items).filter(([key, item]) => item.selected));
      
      let pointsSpend = 0; 
      Object.values(choosen).forEach(item => pointsSpend += item.pointValue);
      advancement.pointsLeft = advancement.pointAmount - pointsSpend;
    }
    advancement.removableItemsAdded = removableItemsAdded;
    return advancement;
  }

  _prepareSkills() {
    // Go over skills and mark ones that reach max mastery level
    const skills = this.actor.system.skills;
    const trades = this.actor.system.tradeSkills;
    const languages = this.actor.system.languages;
    const attributes = this.actor.system.attributes;

    for (const [key, skill] of Object.entries(skills)) {
      skill.masteryLimit = getSkillMasteryLimit(this.actor, key);
    }
    for (const [key, trade] of Object.entries(trades)) {
      trade.masteryLimit = getSkillMasteryLimit(this.actor, key);
    }
    for (const [key, lang] of Object.entries(languages)) {
      lang.masteryLimit = 2;
    }
    const maxPrime = 3 + Math.floor(this.actor.system.details.level/5);
    for (const [key, attr] of Object.entries(attributes)) {
      attr.maxPrime = maxPrime === attr.value;
    }

    return {
      skills: skills,
      tradeSkills: trades,
      languages: languages,
      attributes: attributes,
    }
  }

  async _prepareItemSuggestions() {
    const advancement = this.currentAdvancement;
    if (!advancement.allowToAddItems) return;

    this.collectingSuggestedItems = true;
    let type = advancement.addItemsOptions?.itemType;
    const preFilters = advancement.addItemsOptions?.preFilters || "";
    if (!type || type === "any") type = "feature";
    this.itemSuggestions = await collectItemsForType(type);
    this.currentAdvancement.filters = getDefaultItemFilters(preFilters);
    this.currentAdvancement.talentFilterType = this.currentAdvancement.talentFilterType || "general";
    this.collectingSuggestedItems = false;
  }

  _filterSuggestedItems() {
    const advancement = this.currentAdvancement;
    if (!advancement) return [];
    const talentFilter = advancement.addItemsOptions?.talentFilter;
    const hideOwned = advancement.hideOwned;

    const filters = this._prepareItemSuggestionsFilters();
    if (this.currentItem.type === "class" && talentFilter && advancement.talentFilterType) {
      filters.push({
        check: (item) => this._talentFilterMethod(item)
      });
    }
    if (this.currentItem.type === "ancestry") {
      filters.push({
        check: (item) => this._featureSource(item, advancement.featureSourceItem)
      });
    }
    // Stamina/Flavor feature Filter
    filters.push({check: (item) => !item.system.staminaFeature && !item.system.flavorFeature});

    // Name Filter
    filters.push({
      check: (item) => {
        const value = advancement.itemNameFilter;
        if (!value) return true;
        return item.name.toLowerCase().includes(value.toLowerCase());
      }
    });
    
    let filtered = filterDocuments(this.itemSuggestions, filters);
    if (hideOwned) filtered = filtered.filter(item => this.actor.items.getName(item.name) === undefined);

    markItemRequirements(filtered, advancement.talentFilterType, this.actor);
    if (advancement.hideRequirementMissing) return filtered.filter(item => !item.requirementMissing)
    else return filtered;
  }

  _talentFilterMethod(item) {
    const advancement = this.currentAdvancement;
    const type = advancement.talentFilterType;
    switch (type) {
      case "general": 
        return this._featureType(item, "talent") && this._minLevel(item, advancement.level);
      case "class":
        return (this._featureType(item, "class") || this._featureType(item, "subclass")) && this._minLevel(item, advancement.level) && this._featureSource(item, this.currentItem.system.itemKey);
      case "basic":
        return this._featureType(item, "class") && this._minLevel(item, 1) && this._featureSource(item, advancement.featureSourceItem) && this._notCurrentItem(item);
      case "adept":
        return this._featureType(item, "class") && this._minLevel(item, 2) && this._featureSource(item, advancement.featureSourceItem) && this._notCurrentItem(item);
      case "expert":
        return (this._featureType(item, "class") || this._featureType(item, "subclass")) && this._minLevel(item, 5) && this._featureSource(item, advancement.featureSourceItem) && this._notCurrentItem(item);
      case "master":
        return (this._featureType(item, "class") || this._featureType(item, "subclass")) && this._minLevel(item, 6) && this._featureSource(item, advancement.featureSourceItem) && this._notCurrentItem(item);
      case "grandmaster":
        return (this._featureType(item, "class") || this._featureType(item, "subclass")) && this._minLevel(item, 8) && this._featureSource(item, advancement.featureSourceItem) && this._notCurrentItem(item);
      case "legendary":
        return (this._featureType(item, "class") || this._featureType(item, "subclass")) && this._minLevel(item, 9) && this._featureSource(item, advancement.featureSourceItem) && this._notCurrentItem(item);
      default:
        return false;
    }
  }

  _featureType(item, expected) {
    return item.system.featureType === expected;
  }

  _minLevel(item, expected) {
    return item.system.requirements.level <= expected;
  }

  _featureSource(item, originName) {
    if (!originName) return true;
    const featureOrigin = item.system.featureSourceItem;
    if (!featureOrigin) return false;

    const origin = featureOrigin.toLowerCase().trim();
    const expectedName = originName.toLowerCase().trim();
    if (origin.includes(expectedName)) return true;
    return false;
  }

  _notCurrentItem(item) {
    const featureOrigin = item.system.featureOrigin;
    if (!featureOrigin) return false;

    const origin = featureOrigin.toLowerCase().trim();
    const currentName = this.currentItem.name.toLowerCase().trim();
    if (origin.includes(currentName)) return false;
    return true;
  }

  _prepareItemSuggestionsFilters() {
    const itemType = this.currentAdvancement.addItemsOptions?.itemType;
    if (!itemType) return [];
    
    const filterObject = this.currentAdvancement.filters;
    if (!filterObject) return [];
    const filters = [
      filterObject.name,
      filterObject.compendium,
      filterObject.sourceName,
      ...Object.values(filterObject[itemType])
    ];
    return filters;
  }

  //===========================================
  //           Activate Listerners            =  
  //===========================================
   /** @override */
  activateListeners(html) {
    super.activateListeners(html);
    html.find('.activable').click(ev => this._onActivable(datasetOf(ev).path));
    html.find(".selectable").change(ev => this._onValueChange(datasetOf(ev).path, valueOf(ev)));
    html.find(".input").change(ev => this._onValueChange(datasetOf(ev).path, valueOf(ev)));
    html.find(".numeric-input").change(ev => this._onNumericValueChange(datasetOf(ev).path, valueOf(ev)));

    html.find(".apply").click(ev => this._onApply(ev));
    html.find('.finish').click(ev => this._onFinish(ev));
    html.find(".skip").click(ev => this._onSkip(ev));
    html.find(".select-subclass").click(ev => this._onSelectSubclass(datasetOf(ev).uuid));
    html.find('.item-delete').click(ev => this._onItemDelete(datasetOf(ev).key)); 
    html.find(".path-selector").click(ev => this._onPathMasteryChange(datasetOf(ev).type));

    // Drag and drop events
    html[0].addEventListener('dragover', ev => ev.preventDefault());
    html[0].addEventListener('drop', async ev => await this._onDrop(ev));

    // Spend Points
    html.find(".add-attr").click(ev => this._onAttrChange(datasetOf(ev).key, true));
    html.find(".sub-attr").click(ev => this._onAttrChange(datasetOf(ev).key, false));
    html.find('.save-mastery').click(ev => this._onSaveMastery(datasetOf(ev).key));
    html.find(".skill-point-converter").click(async ev => {await convertSkillPoints(this.actor, datasetOf(ev).from, datasetOf(ev).to, datasetOf(ev).operation, datasetOf(ev).rate); this.render();});
    html.find(".skill-mastery-toggle").mousedown(async ev => {await toggleSkillMastery(datasetOf(ev).type, datasetOf(ev).key, ev.which, this.actor); this.render();});
    html.find(".expertise-toggle").click(async ev => {await manualSkillExpertiseToggle(datasetOf(ev).key, this.actor, datasetOf(ev).type); this.render();});
    html.find(".language-mastery-toggle").mousedown(async ev => {await toggleLanguageMastery(datasetOf(ev).path, ev.which, this.actor); this.render();});

    // Tooltips
    html.find('.item-tooltip').hover(ev => {
      const item = datasetOf(ev).source === "advancement" ? this._itemFromAdvancement(datasetOf(ev).itemKey) : this._itemFromUuid(datasetOf(ev).itemKey);
      itemTooltip(item, ev, html, {position: this._getTooltipPosition(html)});
    },
    ev => hideTooltip(ev, html));
    html.find('.journal-tooltip').hover(ev => {
      const type = datasetOf(ev).type;
      const tooltipList = CONFIG.DC20RPG.SYSTEM_CONSTANTS.JOURNAL_UUID.advancementToolitps;
      journalTooltip(tooltipList[type], datasetOf(ev).header, datasetOf(ev).img, ev, html, {position: this._getTooltipPosition(html)});
    },
    ev => hideTooltip(ev, html));
    html.find('.text-tooltip').hover(ev => textTooltip(`<p>${datasetOf(ev).text}</p>`, "Tip", datasetOf(ev).img, ev, html, {position: this._getTooltipPosition(html)}), ev => hideTooltip(ev, html));

    html.find('.open-compendium-browser').click(() => {
      const itemType = this.currentAdvancement.addItemsOptions.itemType;
      const preFilters = this.currentAdvancement.addItemsOptions.preFilters;
      if (!itemType || itemType === "any") createItemBrowser("advancement", false, this, preFilters);
      else createItemBrowser(itemType, true, this, preFilters);
    });
    html.find('.open-item-suggestions').click(() => this._onOpenSuggestions());
    html.find('.close-item-suggestions').click(() => {this.suggestionsOpen = false; this.render();});
    html.find('.add-edit-item').mousedown(ev => {
      if (ev.which === 1) this._onItemAdd(datasetOf(ev).uuid);
      else this._itemFromUuid(datasetOf(ev).uuid).sheet.render(true);
    });
  }

  async _onOpenSuggestions() {
    if (this.collectingSuggestedItems) {
      const dialog =  new SimplePopup("non-closable", {header: "Collecting Suggestions", message: "Collecting Suggested Items... Please wait it might take a while"}, {title: "Popup"});
      await dialog._render(true);

      await new Promise((resolve) => {
        let counter = 0;
        const checkInterval = setInterval(() => {
          if (counter > 40) { // Max 20 seconds waiting time
            ui.notifications.warn("Waiting time exceeded");
            clearInterval(checkInterval);
            resolve();
          }
          if (!this.collectingSuggestedItems) {
            clearInterval(checkInterval);
            resolve();
          }
          counter++;
        }, 500); // Check every 500ms
      });
    
      dialog.close();
    }
    this.suggestionsOpen = true; 
    this.render();
  }

  async _onAttrChange(key, add) {
    await manipulateAttribute(key, this.actor, !add);
    this.render();
  }

  async _onSaveMastery(key) {
    await changeActivableProperty(`system.attributes.${key}.saveMastery`, this.actor);
    this.render();
  }

  async _onFinish(event) {
    event.preventDefault();
    // Add new resource values
    const scalingValues = await collectScalingValues(this.actor, this.oldSystemData);
    for (const scaling of scalingValues) {
      if (!scaling.resourceKey) continue;
      if (scaling.previous === scaling.current) continue;

      const toAdd = scaling.current - scaling.previous;
      if (scaling.custom) regainCustomResource(scaling.resourceKey, this.actor, toAdd, true);
      else regainBasicResource(scaling.resourceKey, this.actor, toAdd, true);
    }
    
    this.close();
    return;
  }

  _onSkip(event) {
    event.preventDefault();
    this.selectSubclass = null;
    this._prepareData();
    this.render();
  }

  async _onSelectSubclass(subclassUuid) {
    if (this.applyingAdvancement) return; // When there was a lag user could apply advancement multiple times
    this.applyingAdvancement = true;
    this.render();
    await game.settings.set("dc20rpg", "suppressAdvancements", true);
    
    const subclass = await fromUuid(subclassUuid);
    const createdSubclass = await createItemOnActor(this.actor, subclass.toObject());
    this.advForItems.subclass = {
      item: createdSubclass,
      advancements: collectAdvancementsFromItem(3, createdSubclass)
    };

    this.applyingAdvancement = false;
    this.selectSubclass = null;
    this._prepareData();
    this.render();
  }

  _onActivable(pathToValue) {
    let value = getValueFromPath(this.currentAdvancement, pathToValue);
    setValueForPath(this.currentAdvancement, pathToValue, !value);
    this.render();
  }

  _onValueChange(pathToValue, value) {
    setValueForPath(this.currentAdvancement, pathToValue, value);
    this.render();
  }

  _onNumericValueChange(pathToValue, value) {
    const numericValue = parseInt(value);
    setValueForPath(this.currentAdvancement, pathToValue, numericValue);
    this.render();
  }

  _onPathMasteryChange(mastery) {
    this.currentAdvancement.mastery = mastery;
    this.render();
  }

  async _onApply(event) {
    event.preventDefault();
    if (this.applyingAdvancement) return; // When there was a lag user could apply advancement multiple times
    this.applyingAdvancement = true;

    if (!canApplyAdvancement(this.currentAdvancement)) {
      this.applyingAdvancement = false;
      this.render();
      return;
    }

    this.currentAdvancement.key = this.currentAdvancementKey;
    const extraAdvancements = await applyAdvancement(this.currentAdvancement, this.actor, this.currentItem);
    if (this.currentAdvancement.tip) this.tips.push({img: this.currentItem.img, tip: this.currentAdvancement.tip});
    
    // Add Extra advancements
    for (const extra of extraAdvancements) await addAdditionalAdvancement(extra, this.currentItem, this.advancementsForCurrentItem);

    // Go to next advancement
    if (this.hasNext()) {
      this.next();
    }
    else {
      if (!await shouldLearnAnyNewSpellsOrTechniques(this.actor) || this.knownApplied) this.showFinal = true;
      else {
        const addedAdvancements = await addNewSpellTechniqueAdvancements(this.actor, this.currentItem, this.advancementsForCurrentItem, this.currentAdvancement.level);
        this.knownApplied = true;
        if (addedAdvancements.length > 0) this.next();
      }
    }

    // Render dialog
    this.applyingAdvancement = false;
    this.suggestionsOpen = false;
    this.render();
  }

  async _onDrop(event) {
    event.preventDefault();
    const droppedData  = event.dataTransfer.getData('text/plain');
    if (!droppedData) return;
    
    const droppedObject = JSON.parse(droppedData);
    if (droppedObject.type !== "Item") return;

    await this._onItemAdd(droppedObject.uuid);
  }

  async _onItemAdd(itemUuid) {
    if (!this.advancementsForCurrentItem) return;
    const currentAdvancement = this.advancementsForCurrentItem[this.advIndex][1];
    if (!currentAdvancement.allowToAddItems) return;

    const item = await fromUuid(itemUuid);
    if (!["feature", "technique", "spell", "weapon", "equipment", "consumable"].includes(item.type)) return;

    // Can be countent towards known spell/techniques
    const canBeCounted = ["technique", "spell"].includes(item.type);

    // Get item
    currentAdvancement.items[item.id] = {
      uuid: itemUuid,
      createdItemId: "",
      selected: true,
      pointValue: item.system.choicePointCost !== undefined ? item.system.choicePointCost : 1,
      mandatory: false,
      removable: true,
      canBeCounted: canBeCounted,
      ignoreKnown: false,
    };
    this.render();
  }

  _onItemDelete(itemKey) {
    const currentAdvancement = this.advancementsForCurrentItem[this.advIndex][1];
    const currentAdvKey = this.advancementsForCurrentItem[this.advIndex][0];
    delete currentAdvancement.items[itemKey];
    this.currentItem.update({[`system.advancements.${currentAdvKey}.items.-=${itemKey}`] : null});
    this.render();
  }

  _itemFromAdvancement(itemKey) {
    const currentAdvancement = this.advancementsForCurrentItem[this.advIndex][1];
    const uuid = currentAdvancement.items[itemKey].uuid;
    const item = fromUuidSync(uuid);
    return item;
  }

  _itemFromUuid(uuid) {
    const item = fromUuidSync(uuid);
    return item;
  }

  setPosition(position) {
    super.setPosition(position);

    this.element.css({
      "min-height": "600px",
      "min-width": "800px",
    });
    this.element.find("#advancement-dialog").css({
      height: this.element.height() -30,
    });
  }

  _getTooltipPosition(html) {
    let position = null;
    const left = html.find(".left-column");
    if (left[0]) {
      position = {
        width: left.width() - 25,
        height: left.height() - 20,
      };
    }
    return position;
  }

  async _render(...args) {
    let scrollPosition = 0;

    let selector = this.element.find('.middle-column');
    if (selector.length > 0) {
      scrollPosition = selector[0].scrollTop;
    }
    
    await super._render(...args);
    
    // Refresh selector
    selector = this.element.find('.middle-column');
    if (selector.length > 0) {
      selector[0].scrollTop = scrollPosition;
    }
  }

  close(options) {
    super.close(options);
    game.settings.set("dc20rpg", "suppressAdvancements", false);
  }
}

/**
 * Creates and returns ActorAdvancement dialog. 
 */
function actorAdvancementDialog(actor, advForItems, oldSystemData, openSubclassSelector) {
  const dialog = new ActorAdvancement(actor, advForItems, oldSystemData, openSubclassSelector, {title: `You Become Stronger`});
  dialog.render(true);
}

function createNewAdvancement() {
	return { 
		name: "Advancement",
		customTitle: "",
		tip: "",
		level: 1,
		applied: false,
		additionalAdvancement: false,
		repeatable: false,
		repeatAt: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
		progressPath: false,
		mustChoose: false,
		pointAmount: 1,
		allowToAddItems: false,
		addItemsOptions: {
			itemType: "",
			preFilters: "",
			talentFilter: false,
			helpText: "",
			itemLimit: null,
 		},
		items: {}
	};
}

function deleteAdvancement(item, key) {
	item.update({[`system.advancements.-=${key}`]: null});
}

function applyAdvancements(actor, level, clazz, subclass, ancestry, background, oldSystem) {
	let advForItems = {};

	if (ancestry) {
		const advancements = collectAdvancementsFromItem(level, ancestry);
		if (Object.keys(advancements).length !== 0) advForItems = {...advForItems, ancestry: {item: ancestry, advancements: advancements}};
	}
	if (background) {
		const advancements = collectAdvancementsFromItem(level, background);
		if (Object.keys(advancements).length !== 0) advForItems = {...advForItems, background: {item: background, advancements: advancements}};
	}
	if (subclass) {
		const advancements = collectAdvancementsFromItem(level, subclass);
		if (Object.keys(advancements).length !== 0) advForItems = {...advForItems, subclass: {item: subclass, advancements: advancements}};
	}
	if (clazz) {
		const advancements = collectAdvancementsFromItem(level, clazz);
		if (Object.keys(advancements).length !== 0) advForItems = {...advForItems, clazz: {item: clazz, advancements: advancements}};
	}

	actorAdvancementDialog(actor, advForItems, oldSystem, level === 3);
}

function collectAdvancementsFromItem(level, item) {
	const advancements = item.system.advancements;
	return Object.fromEntries(Object.entries(advancements)
		.filter(([key, advancement]) => advancement.level <= level)
		.filter(([key, advancement]) => !advancement.applied));
}

async function removeAdvancements(actor, level, clazz, subclass, ancestry, background, itemDeleted) {
	const dialog =  new SimplePopup("non-closable", {header: "Removing advancements", message: "Removing advancements - it might take a moment, please wait."}, {title: "Popup"});
	await dialog._render(true);
	if (clazz) await _removeAdvancementsFrom(actor, level, clazz, itemDeleted);
	if (subclass) await _removeAdvancementsFrom(actor, level, subclass, itemDeleted);
	if (ancestry) await _removeAdvancementsFrom(actor, level, ancestry, itemDeleted);
	if (background) await _removeAdvancementsFrom(actor, level, background, itemDeleted);
	dialog.close();
}

async function _removeAdvancementsFrom(actor, level, item, itemDeleted) {
	const advancements = item.system.advancements;
	const entries = Object.entries(advancements)
		.filter(([key, advancement]) => advancement.level >= level)
		.filter(([key, advancement]) => {
			if (advancement.applied) return true;
			if (advancement.level === level && advancement.additionalAdvancement) return true;
		});

	for (const [key, advancement] of entries) {
		await _removeItemsFromActor(actor, advancement.items);
		await _removeMulticlassInfoFromActor(actor, key);
		if (!itemDeleted) {
			// We dont need to mark advancement if parent was removed.
			await _markAdvancementAsNotApplied(advancement, key, actor, item._id);
		}
	}
}

async function _removeItemsFromActor(actor, items) {
	for (const [key, record] of Object.entries(items)) {
		const itemExist = await actor.items.has(record.createdItemId);
		if (itemExist) {
			const item = await actor.items.get(record.createdItemId);
			record.createdItemId = "";
			await item.delete();
		}	
	}
}

async function _markAdvancementAsNotApplied(advancement, key, actor, id) {
	const itemStillExist = await actor.items.has(id);
	if (itemStillExist) {
		const item = await actor.items.get(id);

		// If advancement does not come from base item we want to remove it instad of marking it as not applied
		if (advancement.additionalAdvancement) {
			if (key === "martialExpansion") await actor.update({["system.details.martialExpansionProvided"]: false});
			await item.update({[`system.advancements.-=${key}`]: null});
		}
		else {
			advancement.applied = false;
			await item.update({[`system.advancements.${key}`]: advancement});
		}
	}
}

async function _removeMulticlassInfoFromActor(actor, key) {
	const multiclassTalents = actor.system.details.advancementInfo?.multiclassTalents;
	if (multiclassTalents[key]) await actor.update({[`system.details.advancementInfo.multiclassTalents.-=${key}`]: null});
}


async function registerUniqueSystemItems() {
	CONFIG.DC20RPG.UNIQUE_ITEM_IDS = {
		class: {},
		subclass: {},
		ancestry: {},
		background: {}
	};
	CONFIG.DC20RPG.SUBCLASS_CLASS_LINK = {};

	const clazz = await collectItemsForType("class");
	clazz.forEach(item => {
		const itemKey = item.system.itemKey;
		if (itemKey) CONFIG.DC20RPG.UNIQUE_ITEM_IDS.class[itemKey] = item.name;
	});
	const subclass = await collectItemsForType("subclass");
	subclass.forEach(item => {
		const itemKey = item.system.itemKey;
		const classKey = item.system.forClass.classSpecialId;
		if (itemKey) {
			CONFIG.DC20RPG.UNIQUE_ITEM_IDS.subclass[itemKey] = item.name;
			if (classKey) CONFIG.DC20RPG.SUBCLASS_CLASS_LINK[itemKey] = classKey;
		}
	});
	const ancestry = await collectItemsForType("ancestry");
	ancestry.forEach(item => {
		const itemKey = item.system.itemKey;
		if (itemKey) CONFIG.DC20RPG.UNIQUE_ITEM_IDS.ancestry[itemKey] = item.name;
	});
	const background = await collectItemsForType("background");
	background.forEach(item => {
		const itemKey = item.system.itemKey;
		if (itemKey) CONFIG.DC20RPG.UNIQUE_ITEM_IDS.background[itemKey] = item.name;
	});
}

//================================================
//           Item Manipulaton on Actor           =
//================================================
function getItemFromActor(itemId, actor) {
  return actor.items.get(itemId);
}

/**
 * Returns item with specific itemKey from actor.
 */
function getItemFromActorByKey(itemKey, actor) {
  return actor.items.find(item => item.system.itemKey === itemKey)
}

async function createItemOnActor(actor, itemData) {
  if (!actor.canUserModify(game.user)) {
    emitEventToGM("addDocument", {
      docType: "item",
      docData: itemData, 
      actorUuid: actor.uuid
    });
    return;
  }
  return await Item.create(itemData, { parent: actor });
}

async function deleteItemFromActor(itemId, actor) {
  if (!actor.canUserModify(game.user)) {
    emitEventToGM("removeDocument", {
      docType: "item",
      docId: itemId, 
      actorUuid: actor.uuid
    });
    return;
  }
  const item = getItemFromActor(itemId, actor);
  if(item) await item.delete();
}

function editItemOnActor(itemId, actor) {
  const item = getItemFromActor(itemId, actor);
  item.sheet.render(true);
}

async function duplicateItem(itemId, actor) {
  const item = getItemFromActor(itemId, actor);
  return await createItemOnActor(actor, item);
}

//======================================
//    Item Manipulation Interceptors   =
//======================================
async function addItemToActorInterceptor(item, actor) {
  // Unique Item
  if (["class", "subclass", "ancestry", "background"].includes(item.type)) {
    if (actor.type === "character") {
      return await addUniqueItemToActor(item, actor);
    }
    return;
  }

  // Item Provided Custom Resource
  if (item.system.isResource) {
    createNewCustomResourceFromItem(item.system.resource, item.img, actor);
  }
}

async function modifiyItemOnActorInterceptor(item, updateData, actor) {
  // Check if isResource was we can update actor's custom resources
  if (updateData.system?.hasOwnProperty("isResource")) {
    if(updateData.system.isResource) createNewCustomResourceFromItem(item.system.resource, item.img, actor);
    else removeResource(item.system.resource.resourceKey, actor);
  }

  // Check if on item toggle macro should be runned 
  if (updateData.system?.toggle?.hasOwnProperty("toggledOn")) {
    const toggledOn = updateData.system.toggle.toggledOn;
    runTemporaryItemMacro(item, "onItemToggle", actor, {on: toggledOn, off: !toggledOn, equipping: false});
  }
  // Check if on item toggle macro should be runned when item is equipped
  if (updateData.system?.statuses?.hasOwnProperty("equipped")) {
    const equipped = updateData.system.statuses.equipped;
    runTemporaryItemMacro(item, "onItemToggle", actor, {on: equipped, off: !equipped, equipping: true});
  }
}

async function removeItemFromActorInterceptor(item, actor) {
  // Unique Item
  if (["class", "subclass", "ancestry", "background"].includes(item.type)) {
    return removeUniqueItemFromActor(item, actor);
  }

  // Item Provided Custom Resource
  if (item.system.isResource) {
    removeResource(item.system.resource.resourceKey, actor);
  }
}

//======================================
//            Actor's Class            =
//======================================
// TODO: Separate to advancement file?
async function addUniqueItemToActor(item, actor) {
  const itemType = item.type;
  const details = actor.system.details;

  const uniqueItemId = details[itemType].id;
  if (uniqueItemId) {
    const errorMessage = `Cannot add another ${itemType} to ${actor.name}.`;
    ui.notifications.error(errorMessage);
    item.delete();
  } 
  else {
    const oldActorData = foundry.utils.deepClone(actor.system);
    await actor.update({[`system.details.${itemType}.id`]: item._id});
    const suppressAdvancements = game.settings.get("dc20rpg", "suppressAdvancements");
    if (suppressAdvancements) return;
    const actorLevel = details.level;

    // Apply Item Advancements
    switch (itemType) {
      case "class":
        // When adding class we also need to add subclass and ancestry advancements
        const subclass = actor.items.get(details.subclass.id);
        const ancestry = actor.items.get(details.ancestry.id);
        const background = actor.items.get(details.background.id);
        applyAdvancements(actor, 1, item, subclass, ancestry, background, oldActorData); // When we are putting class it will always be at 1st level
        break;
      case "subclass":
        applyAdvancements(actor, actorLevel, null, item, null, null, oldActorData);
        break;
      case "ancestry":
        applyAdvancements(actor, actorLevel, null, null, item, null, oldActorData);
        break;
      case "background":
        applyAdvancements(actor, actorLevel, null, null, null, item, oldActorData);
    }
  }
}

function runAdvancements(actor, level) {
  const suppressAdvancements = game.settings.get("dc20rpg", "suppressAdvancements");
  if (suppressAdvancements) return;
  const oldActorData = foundry.utils.deepClone(actor.system);

  const clazz = actor.items.get(actor.system.details.class.id);
  const subclass = actor.items.get(actor.system.details.subclass.id);
  const ancestry = actor.items.get(actor.system.details.ancestry.id);
  const background = actor.items.get(actor.system.details.background.id);

  applyAdvancements(actor, level, clazz, subclass, ancestry, background, oldActorData);
}

async function removeUniqueItemFromActor(item, actor) {
  const itemType = item.type;

  const uniqueItemId = actor.system.details[itemType].id;
  if (uniqueItemId === item._id) {
    
    // Remove item's custom resources from actor
    Object.entries(item.system.scaling)
      .filter(([key, scalingValue]) => scalingValue.isResource)
      .forEach(([key, scalingValue]) => removeResource(key, actor));

    switch (itemType) {
      case "class":
        // When removing class we also need to remove subclass and ancestry advancements
        const subclass = actor.items.get(actor.system.details.subclass.id);
        const ancestry = actor.items.get(actor.system.details.ancestry.id);
        const background = actor.items.get(actor.system.details.background.id);
        await removeAdvancements(actor, 1, item, subclass, ancestry, background, true);
        break;
      case "subclass":
        await removeAdvancements(actor, 1, null, item, null, null, true);
        break;
      case "ancestry":
        await removeAdvancements(actor, 0, null, null, item, null, true); // Ancestries have level 0 traits
        break;
      case "background":
        await removeAdvancements(actor, 0, null, null, null, item, true); // Background have level 0 traits
        break;
    }

    await actor.update({[`system.details.${itemType}`]: {id: ""}});
  }
}

function mixAncestry(first, second) {
  if (!first || !second) {
    ui.notifications.warn("You need to privide both Ancestries to merge!");
    return;
  }

  const itemData = {
    type: "ancestry",
    system: {
      description: `<p>Mixed Ancestry made from @UUID[${first.uuid}]{${first.name}} and @UUID[${second.uuid}]{${second.name}}</p>`,
    },
    name: `${first.name} / ${second.name}`,
    img: first.img
  };

  // Mix Advancements
  const firstAdvByLevel = _collectAdvancementsByLevel(first.system.advancements);
  const secondAdvByLevel = _collectAdvancementsByLevel(second.system.advancements);

  let coreAdv = [];
  let additionalAdv = [];

  if (firstAdvByLevel.length > secondAdvByLevel.length) {
    coreAdv = firstAdvByLevel;
    additionalAdv = secondAdvByLevel;
  }
  else {
    coreAdv = secondAdvByLevel;
    additionalAdv = firstAdvByLevel;
  }

  const advancements = {};
  for (let i = 0; i < coreAdv.length; i++) {
    const core = coreAdv[i];
    const add = additionalAdv[i];

    const coreSize = core?.length || 0;
    const addSize = add?.length || 0;

    const length = coreSize >= addSize ? coreSize : addSize;

    for (let j = 0; j < length; j++) {
      const fst = core ? core[j] : undefined;
      const snd = add ? add[j] : undefined;

      const merged = _mergeAdvancements(fst, snd);
      if (merged) advancements[generateKey()] = merged;
    }
  }
  itemData.system.advancements = advancements;
  
  return itemData;
}

function _collectAdvancementsByLevel(advancements) {
  const advByLevel = [];
  for (const advancement of Object.values(advancements)) {
    _fillBefore(advancement.level, advByLevel);
    advByLevel[advancement.level].push(advancement);
  }
  return advByLevel;
}

function _fillBefore(level, advByLevel) {
  for (let i = 0; i <= level; i++) {
    if (advByLevel[i]) continue;
    else advByLevel[i] = [];
  }
}

function _mergeAdvancements(first, second) {
  if (!first && !second) return;
  if (!second) return first;
  if (!first) return second;

  const items = {
    ..._mergeItems(first.items), 
    ..._mergeItems(second.items)
  };

  return {
    name: `Merged: ${first.name} + ${second.name}`,
    mustChoose: first.mustChoose || second.mustChoose,
    pointAmount: first.pointAmount,
    level: first.level,
    applied: first.applied || second.applied,
    talent: first.talent || second.talent,
    repeatable: first.repeatable,
    repeatAt: first.repeatAt,
    allowToAddItems: first.allowToAddItems || second.allowToAddItems,
    compendium: first.compendium,
    preFilters: first.preFilters,
    items: items,
  }
}

function _mergeItems(items) {
  const collected = {};
  for (const [key, item] of Object.entries(items)) {
    item.mandatory = false;
    item.selected = false;
    collected[key] = item;
  }
  return collected;
}

//======================================
//          Other Item Methods         =
//======================================
async function changeLevel(up, itemId, actor) {
  const item = getItemFromActor(itemId, actor);
  if (!item) return;
  let currentLevel = item.system.level;
  const oldActorData = foundry.utils.deepClone(actor.system);

  const clazz = actor.items.get(actor.system.details.class.id);
  const ancestry = actor.items.get(actor.system.details.ancestry.id);
  let subclass = actor.items.get(actor.system.details.subclass.id);

  if (up === "true") {
    currentLevel = Math.min(currentLevel + 1, 20);
    applyAdvancements(actor, currentLevel, clazz, subclass, ancestry, null, oldActorData);
  }
  else {
    await clearOverridenScalingValue(clazz, currentLevel - 1);
    await removeAdvancements(actor, currentLevel, clazz, subclass, ancestry);
    currentLevel = Math.max(currentLevel - 1, 0);
  }

  await item.update({[`system.level`]: currentLevel});
  await game.settings.set("dc20rpg", "suppressAdvancements", false);
}

async function rerunAdvancement(actor, classId) {
  const confirmed = await getSimplePopup("confirm", {header: "Do you want to repeat the last level up?"});
  if (!confirmed) return;
  await changeLevel("false", classId, actor);
  await changeLevel("true", classId, actor);
}

async function createScrollFromSpell(spell) {
  if (spell.type !== "spell") return;

  // Prepare Scroll data;
  const scroll = spell.toObject();
  scroll.name += " - Scroll";
  scroll.type = 'consumable';
  scroll.system.consumableType = "scroll";
  scroll.system.enhancements = {};
  scroll.system.costs.resources = { actionPoint: 2 };

  if (spell.actor) createItemOnActor(spell.actor, scroll);
  else Item.create(scroll);
  spell.sheet.close();
}

//======================================
//             Item Tables             =
//======================================
function reorderTableHeaders(tab, current, swapped, actor) {
  const headersOrdering = actor.flags.dc20rpg.headersOrdering;

  const currentOrder = headersOrdering[tab][current].order;
  const swappedOrder = headersOrdering[tab][swapped].order;
  headersOrdering[tab][current].order = swappedOrder;
  headersOrdering[tab][swapped].order = currentOrder;

  actor.update({[`flags.dc20rpg.headersOrdering`]: headersOrdering });
}

function createNewTable(tab, actor) {
  const headers = actor.flags.dc20rpg.headersOrdering[tab];
  const order = Object.entries(headers)
                .sort((a, b) => a[1].order - b[1].order)
                .map(([a, b]) => b.order);
  const last = order[order.length - 1];

  let key = "";
  do {
    key = generateKey();
  } while (headers[key]);

  const newTable = {
    name: "New Table",
    custom: true,
    order: last + 1
  };

  actor.update({[`flags.dc20rpg.headersOrdering.${tab}.${key}`] : newTable});
}

function removeCustomTable(tab, table, actor) {
  actor.update({[`flags.dc20rpg.headersOrdering.${tab}.-=${table}`]: null});
}

//======================================
//          Companion Traits           =
//======================================
function createTrait(itemData, actor) {
  const trait = {
    itemData: itemData,
    active: 0,
    repeatable: false,
    itemIds: []
  };
  actor.update({[`system.traits.${generateKey()}`]: trait});
}

async function deleteTrait(traitKey, actor) {
  const trait = actor.system?.traits[traitKey];
  if (!trait) return;
  
  for (let i = 0; i < trait.itemIds.length; i++) {
    await deleteItemFromActor(trait.itemIds[i], actor);
  }
  await actor.update({[`system.traits.-=${traitKey}`]: null});
}

async function activateTrait(traitKey, actor) {
  const trait = actor.system?.traits[traitKey];
  if (!trait) return;

  const max = trait.repeatable ? 99 : 1;
  trait.active = Math.min(trait.active+1, max);
  await _handleItemsFromTraits(trait, actor);
  await actor.update({[`system.traits.${traitKey}`]: trait});
}

async function deactivateTrait(traitKey, actor) {
  const trait = actor.system?.traits[traitKey];
  if (!trait) return;

  trait.active = Math.max(trait.active-1, 0);
  await _handleItemsFromTraits(trait, actor);
  await actor.update({[`system.traits.${traitKey}`]: trait});
}

async function _handleItemsFromTraits(trait, actor) {
  if (trait.active > trait.itemIds.length) {
    const createdItem = await createItemOnActor(actor, trait.itemData);
    trait.itemIds.push(createdItem.id);
  }

  if (trait.active < trait.itemIds.length) {
    const itemId = trait.itemIds.pop();
    await deleteItemFromActor(itemId, actor);
  }
}

function registerSystemSockets() {

  // Simple Popup
  game.socket.on('system.dc20rpg', async (data, emmiterId) => {
    if (data.type === "simplePopup") {
      const { popupType, popupData, userIds } = data.payload;
      if (userIds.includes(game.user.id)) {
        const result = await getSimplePopup(popupType, popupData);
        game.socket.emit('system.dc20rpg', {
          payload: result, 
          emmiterId: emmiterId,
          type: "simplePopupResult"
        });
      }
    }
  });

  // Roll Prompt
  game.socket.on('system.dc20rpg', async (data, emmiterId) => {
    if (data.type === "rollPrompt") {
      await rollPrompt(data.payload, emmiterId);
    }
  });

  // Open Rest Dialog
  game.socket.on('system.dc20rpg', async (data) => {
    if (data.type === "startRest") {
      const {actorId, preselected} = data.payload;
      let actor = game.actors.get(actorId);

      if (actor && actor.ownership[game.user.id] === 3) {
        createRestDialog(actor, preselected);
      }
    }
  });

  // Create actor for a player
  game.socket.on('system.dc20rpg', async (data, emmiterId) => {
    if (data.type === "createActor") {
      if (game.user.id === data.gmUserId) {
        const actor = await Actor.create(data.actorData);
        game.socket.emit('system.dc20rpg', {
          payload: actor._id, 
          emmiterId: emmiterId,
          type: "actorCreated"
        });
      }
    }
  });

  // Update Chat Message
  game.socket.on('system.dc20rpg', async (data) => {
    if (data.type === "updateChatMessage") {
      const m = data.payload;
      if (game.user.id === m.gmUserId) {
        const message = game.messages.get(m.messageId);
        if (message) message.update(m.updateData);
      }
    }
  });

  // Add Document to Actor 
  game.socket.on('system.dc20rpg', async (data) => {
    if (data.type === "addDocument") {
      const { docType, docData, actorUuid, gmUserId } = data.payload;
      if (game.user.id === gmUserId) {
        const actor = await fromUuid(actorUuid);
        if (!actor) return;
        if (docType === "item") await createItemOnActor(actor, docData);
        if (docType === "effect") await createEffectOn(docData, actor);
      }
    }
  });

  // Remove Document from Actor
  game.socket.on('system.dc20rpg', async (data) => {
    if (data.type === "removeDocument") {
      const { docType, docId, actorUuid, gmUserId } = data.payload;
      if (game.user.id === gmUserId) {
        const actor = await fromUuid(actorUuid);
        if (!actor) return;
        if (docType === "item") await deleteItemFromActor(docId, actor);
        if (docType === "effect") await deleteEffectFrom(docId, actor);
      }
    }
  });

  // Remove Effect from Actor 
  game.socket.on('system.dc20rpg', async (data) => {
    if (data.type === "removeEffectFrom") {
      const m = data.payload;
      if (game.user.id === m.gmUserId) {
        effectsToRemovePerActor(m.toRemove);
      }
    }
  });
  
  // Re-render Roll Menu Dialog
  game.socket.on("system.dc20rpg", (data) => {
    if (data.type === "rollPromptRerendered") {
      Object.values(ui.windows)
        .filter(window => window instanceof RollPromptDialog)
        .forEach(window => {
          if (window.itemRoll) {
            if (window.item.id === data.payload.itemId) {
              window.render(false, {dontEmit: true});
            }
          }
          else {
            if (window.actor.id === data.payload.actorId) {
              window.render(false, {dontEmit: true});
            }
          }
        });
    }
  });

  game.socket.on("system.dc20rpg", (data) => {
    if (data.type === "askGmForHelp") {
      const p = data.payload;
      if (game.user.id === p.gmUserId) {
        const actor = game.actors.get(p.actorId);
        if (!actor) return;

        const item = actor.items.get(p.itemId);
        if (!item) return;
        promptItemRoll(actor, item, false, true);
      }
    }
  });
}

async function rollPrompt(payload, emmiterId) {
  const {actorId, details, isToken, tokenId} = payload;
  let actor = game.actors.get(actorId);
  // If we are rolling with unlinked actor we need to use token version
  if (isToken) actor = game.actors.tokens[tokenId]; 
  
  if (actor && actor.ownership[game.user.id] === 3) {
    const roll = await promptRoll(actor, details);
    game.socket.emit('system.dc20rpg', {
      payload: {...roll}, 
      emmiterId: emmiterId,
      actorId: actorId,
      type: "rollPromptResult"
    });
  }
}

//================================
//    Socket helper functions    =
//================================
/**
 * Response listener. Will report only first recieved response
 */
async function responseListener(type, validationData={}) {
  return new Promise((resolve) => {
    game.socket.once('system.dc20rpg', (response) => {
      if (response.type !== type) {
        resolve(responseListener(type, validationData));
      }
      else if (!_validateResponse(response, validationData)) {
        resolve(responseListener(type, validationData));
      }
      else {
        resolve(response.payload);
      }
    });
  });
}

function _validateResponse(response, validationData) {
  for (const [key, expectedValue] of Object.entries(validationData)) {
    if (response[key]) {
      if (response[key] !== expectedValue) return false;
    }
  }
  return true;
}

function emitSystemEvent(type, payload) {
  game.socket.emit('system.dc20rpg', {
    type: type,
    payload: payload
  });
}

function emitEventToGM(type, payload) {
  const activeGM = game.users.activeGM;
  if (!activeGM) {
    ui.notifications.error("There needs to be an active GM to proceed with that operation");
    return false;
  }
  emitSystemEvent(type, {
    gmUserId: activeGM.id,
    ...payload,
  });
}

function prepareActiveEffectsAndStatuses(owner, context) {
  const hideNonessentialEffects = owner.flags.dc20rpg?.hideNonessentialEffects;
  // Prepare all statuses 
  const statuses = foundry.utils.deepClone(CONFIG.statusEffects);

  // Define effect header categories
  const effects = {
    temporary: {
      type: "temporary",
      effects: []
    },
    passive: {
      type: "passive",
      effects: []
    },
    inactive: {
      type: "inactive",
      effects: []
    },
    disabled: {
      type: "disabled",
      effects: []
    }
  };

  // Iterate over active effects, classifying them into categories
  for ( const effect of owner.allEffects ) {
    if (effect.statuses?.size > 0) _connectEffectAndStatus(effect, statuses);
    if (effect.sourceName === "None") ; // None means it is a condition, we can ignore that one.
    else {
      effect.originName = effect.parent.name;
      effect.timeLeft = effect.roundsLeft;
      effect.canChangeState = effect.stateChangeLocked;
      if (effect.flags.dc20rpg?.nonessential && hideNonessentialEffects) continue;
      if (effect.isTemporary && effect.disabled) effects.disabled.effects.push(effect);
      else if (effect.disabled) effects.inactive.effects.push(effect);
      else if (effect.isTemporary) effects.temporary.effects.push(effect);
      else effects.passive.effects.push(effect);
    }
  }

  context.effects = effects;
  context.statuses = statuses;
}

function prepareActiveEffects(owner, context) {
  const effects = {
    temporary: {
      type: "temporary",
      effects: []
    },
    passive: {
      type: "passive",
      effects: []
    }
  };

  for ( const effect of owner.allEffects ) {
    if (effect.isTemporary) effects.temporary.effects.push(effect);
    else effects.passive.effects.push(effect);
  }
  context.effects = effects;
}

function _connectEffectAndStatus(effect, statuses) {
  statuses
      .filter(status => effect.statuses.has(status.id) && !effect.disabled)
      .map(status => {
        status.effectId = effect.id;
        
        // Collect stacks for conditions
        if (!status.stack) status.stack = 1;
        else if (status.stackable) status.stack += 1; 

        // If status comes from other active effects we want to give info about it with tooltip
        if ((effect.statuses.size > 1 && effect.name !== status.name) || effect.sourceName !== "None") {
          if (!status.tooltip) status.tooltip = `Additional stack from ${effect.name}`;
          else status.tooltip += ` and ${effect.name}`;
          status.fromOther = true;
        }

        return status;
      });
}


//==================================================
//    Manipulating Effects On Other Objects        =  
//==================================================
async function createNewEffectOn(type, owner, flags) {
  const duration = type === "temporary" ? 1 : undefined;
  const inactive = type === "inactive";
  const created = await owner.createEmbeddedDocuments("ActiveEffect", [{
    label: "New Effect",
    img: "icons/svg/aura.svg",
    origin: owner.uuid,
    "duration.rounds": duration,
    disabled: inactive,
    flags: {dc20rpg: flags}
  }]);
  return created[0];
}

async function createEffectOn(effectData, owner) {
  if (!owner.canUserModify(game.user)) {
    emitEventToGM("addDocument", {
      docType: "effect",
      docData: effectData, 
      actorUuid: owner.uuid
    });
    return;
  }
  if (!effectData.origin) effectData.origin = owner.uuid;
  const created = await owner.createEmbeddedDocuments("ActiveEffect", [effectData]);
  return created[0];
}

function editEffectOn(effectId, owner) {
  const effect = getEffectFrom(effectId, owner);
  if (effect) effect.sheet.render(true);
}

async function deleteEffectFrom(effectId, owner) {
  if (!owner.canUserModify(game.user)) {
    emitEventToGM("removeDocument", {
      docType: "effect",
      docId: effectId, 
      actorUuid: owner.uuid
    });
    return;
  }
  const effect = getEffectFrom(effectId, owner);
  if (effect) await effect.delete();
}

async function toggleEffectOn(effectId, owner, turnOn) {
  const options = turnOn ? {disabled: true} : {active: true};
  const effect = getEffectFrom(effectId, owner, options);
  if (effect) {
    if (turnOn) await effect.enable();
    else await effect.disable();
  }
}

function getEffectFrom(effectId, owner, options={}) {
  if (options.active) return owner.allEffects.find(effect => effect._id === effectId && effect.disabled === false);
  if (options.disabled) return owner.allEffects.find(effect => effect._id === effectId && effect.disabled === true);
  return owner.allEffects.find(effect => effect._id === effectId);
}

function getEffectByName(effectName, owner) {
  return owner.getEffectWithName(effectName);
}

function getEffectById(effectId, owner) {
  return owner.allEffects.find(effect => effect._id === effectId);
}

function getEffectByKey(effectKey, owner) {
  if (!effectKey) return;
  return owner.allEffects.find(effect => effect.flags.dc20rpg?.effectKey === effectKey);
}

async function createOrDeleteEffect(effectData, owner) {
  const alreadyExist = getEffectByName(effectData.name, owner);
  if (alreadyExist) return await deleteEffectFrom(alreadyExist.id, owner);
  else return await createEffectOn(effectData, owner);
}

async function effectsToRemovePerActor(toRemove) {
  const actor = getActorFromIds(toRemove.actorId, toRemove.tokenId);
  if (actor) {
    const effect = getEffectFrom(toRemove.effectId, actor);
    const afterRoll = toRemove.afterRoll;
    if (effect) {
      if (afterRoll === "delete") {
        sendEffectRemovedMessage(actor, effect);
        effect.delete();
      }
      if (afterRoll === "disable") effect.disable();
    }
  }
}
   

//===========================================================
function injectFormula(effect, effectOwner) {
  if (!effectOwner) return;
  const rollData = effectOwner.getRollData();

  for (const change of effect.changes) {
    const value = change.value;
    
    // formulas start with "<#" and end with "#>"
    if (value.includes("<#") && value.includes("#>")) {
      // We want to calculate that formula and repleace it with value calculated
      const formulaRegex = /<#(.*?)#>/g;
      const formulasFound = value.match(formulaRegex);

      formulasFound.forEach(formula => {
        const formulaString = formula.slice(2,-2); // We need to remove <# and #>
        const calculated = evaluateDicelessFormula(formulaString, rollData);
        change.value = change.value.replace(formula, calculated.total); // Replace formula with calculated value
      });
    }
  }
}

function getMesuredTemplateEffects(item, applicableEffects) {
  if (!item) return {applyFor: "", effects: []};
  if (item.effects.size === 0) return {applyFor: "", effects: []};
  if (item.system.effectsConfig.addToTemplates === "") return {applyFor: "", effects: []};

  return {
    applyFor: item.system.effectsConfig.addToTemplates,
    effects: applicableEffects || item.effects.toObject()
  }
}

//===========================================================
/**
 * List of default actor keys that are expected to be modified by effects
 */
function getEffectModifiableKeys() {
  return {
    // Defence bonus
    "system.defences.precision.bonuses.always": "Precision Defense: Bonus (always)",
    "system.defences.precision.bonuses.noArmor": "Precision Defense: Bonus (when no armor equipped)",
    "system.defences.precision.bonuses.noHeavy": "Precision Defense: Bonus (when no heavy armor equipped)",
    "system.defences.precision.formulaKey": "Precision Defence: Calculation Formula Key",
    "system.defences.precision.customFormula": "Precision Defence: Custom Calculation Formula",
    "system.defences.area.bonuses.always": "Area Defense: Bonus (always)",
    "system.defences.area.bonuses.noArmor": "Area Defense: Bonus (when no armor equipped)",
    "system.defences.area.bonuses.noHeavy": "Area Defense: Bonus (when no heavy armor equipped)",
    "system.defences.area.formulaKey": "Area Defence: Calculation Formula Key",
    "system.defences.area.customFormula": "Area Defence: Custom Calculation Formula",

    // Damage reduction
    "system.damageReduction.pdr.active": "Physical Damage Reduction",
    "system.damageReduction.edr.active": "Elemental Damage Reduction",
    "system.damageReduction.mdr.active": "Mystical Damage Reduction",
    ..._damageReduction$2(),

    // Flat Damage/healing Modification
    "system.damageReduction.flat": "Flat Damage Modification On You (Value)",
    "system.damageReduction.flatHalf": "Flat Damage Modification On You (Half)",
    "system.healingReduction.flat": "Flat Healing Modification On You (Value)",
    "system.healingReduction.flatHalf": "Flat Healing Modification On You (Half)",

    // Status resistances
    ..._statusResistances$1(),
    "system.customCondition": "Custom Condition",

    // Resources
    "system.resources.health.bonus": "Health - Max Value Bonus",
    "system.resources.mana.bonus": "Mana - Max Value Bonus",
    "system.resources.mana.maxFormula" : "Mana - Calculation Formula",
    "system.resources.stamina.bonus": "Stamina - Max Value Bonus",
    "system.resources.stamina.maxFormula" : "Stamina - Calculation Formula",
    "system.resources.grit.bonus": "Grit - Max Value Bonus",
    "system.resources.grit.maxFormula" : "Grit - Calculation Formula",
    "system.resources.restPoints.bonus" : "Rest Points - Max Value Bonus",
    
    // Help Dice
    "system.help.maxDice": "Help Dice - Max Dice",

    // Death
    "system.death.bonus": "Death's Door Threshold Bonus",

    // Movement
    "system.moveCost": "Cost of moving 1 Space",
    "system.movement.ground.bonus": "Ground Speed Bonus",
    "system.movement.climbing.bonus": "Climbing Speed Bonus",
    "system.movement.climbing.fullSpeed": "Climbing equal Movement",
    "system.movement.climbing.halfSpeed": "Climbing equal Half Movement",
    "system.movement.swimming.bonus": "Swimming Speed Bonus",
    "system.movement.swimming.fullSpeed": "Swimming equal Movement",
    "system.movement.swimming.halfSpeed": "Swimming equal Half Movement",
    "system.movement.burrow.bonus": "Burrow Speed Bonus",
    "system.movement.burrow.fullSpeed": "Burrow equal Movement",
    "system.movement.burrow.halfSpeed": "Burrow equal Half Movement",
    "system.movement.glide.bonus": "Glide Speed Bonus",
    "system.movement.glide.fullSpeed": "Glide equal Movement",
    "system.movement.glide.halfSpeed": "Glide equal Half Movement",
    "system.movement.flying.bonus": "Flying Speed Bonus",
    "system.movement.flying.fullSpeed": "Flying equal Movement",
    "system.movement.flying.halfSpeed": "Flying equal Half Movement",
    "system.jump.bonus": "Jump Distance Bonus",
    "system.jump.key": "Jump Attribute",

    // Senses
    "system.senses.darkvision.range": "Darkvision - Base range (always)",
    "system.senses.darkvision.bonus": "Darkvision - Bonus (always)",
    "system.senses.darkvision.orOption.range": "Darkvision - Base range (if no other source)",
    "system.senses.darkvision.orOption.bonus": "Darkvision - Bonus (if other source exist)",
    "system.senses.tremorsense.range": "Tremorsense - Base range (always)",
    "system.senses.tremorsense.bonus": "Tremorsense - Bonus (always)",
    "system.senses.tremorsense.orOption.range": "Tremorsense - Base range (if no other source)",
    "system.senses.tremorsense.orOption.bonus": "Tremorsense - Bonus (if other source exist)",
    "system.senses.blindsight.range": "Blindsight - Base range (always)",
    "system.senses.blindsight.bonus": "Blindsight - Bonus (always)",
    "system.senses.blindsight.orOption.range": "Blindsight - Base range (if no other source)",
    "system.senses.blindsight.orOption.bonus": "Blindsight - Bonus (if other source exist)",
    "system.senses.truesight.range": "Truesight - Base range (always)",
    "system.senses.truesight.bonus": "Truesight - Bonus (always)",
    "system.senses.truesight.orOption.range": "Truesight - Base range (if no other source)",
    "system.senses.truesight.orOption.bonus": "Truesight - Bonus (if other source exist)",

    // Creature size
    "system.size.size": "Size",

    // Attack and Save
    "system.attackMod.bonus.spell": "Spell Check Bonus",
    "system.attackMod.bonus.martial": "Attack Check Bonus",
    "system.saveDC.bonus.spell": "Spell Save DC Bonus",
    "system.saveDC.bonus.martial": "Martial Save DC Bonus",

    // Attribute bonus
    ..._attributeBonuses(),

    // Skills bonus
    ..._skillBonuses(),

    // Skill expertise
    "system.expertise.automated": "Expertise (Skill Mastery Limit Increase)",

    // Skill Points bonus
    "system.attributePoints.bonus": "Attribute Points",
    "system.skillPoints.skill.bonus": "Skill Points",
    "system.skillPoints.trade.bonus": "Trade Skill Points",
    "system.skillPoints.language.bonus": "Language Points",

    "system.known.cantrips.max": "Cantrips Known",
    "system.known.spells.max": "Spells Known",
    "system.known.maneuvers.max": "Maneuvers Known",
    "system.known.techniques.max": "Techniques Known",

    // Combat Training
    ..._combatTraining$1(),

    // Global Modifiers
    "system.globalModifier.range.melee": "Global Modifier: Melee Range",
    "system.globalModifier.range.normal": "Global Modifier: Normal Range",
    "system.globalModifier.range.max": "Global Modifier: Max Range",
    "system.globalModifier.ignore.difficultTerrain": "Global Modifier: Ignore Difficult Terrain",
    "system.globalModifier.ignore.closeQuarters": "Global Modifier: Ignore Close Quarters",
    "system.globalModifier.ignore.longRange": "Global Modifier: Ignore Long Range Disadvantage",
    "system.globalModifier.ignore.flanking": "Global Modifier: Ignore being Flanked",
    "system.globalModifier.provide.halfCover": "Global Modifier: Provide Half Cover",
    "system.globalModifier.provide.tqCover": "Global Modifier: Provide 3/4 Cover",
    "system.globalModifier.allow.overheal": "Global Modifier: Convert overheal you done to Temp HP",
    "system.globalModifier.prevent.goUnderAP": "Global Modifier: Prevent from going under X AP",
    "system.globalModifier.prevent.healingReduction": "Global Modifier: Reduce Healing Recieved", 
    "system.globalModifier.prevent.hpRegeneration": "Global Modifier: Prevent any Healing", 
    
    // Global Formula modifier
    "system.globalFormulaModifiers.attackCheck": "Formula Modifier: Attack Check",
    "system.globalFormulaModifiers.spellCheck": "Formula Modifier: Spell Check",
    "system.globalFormulaModifiers.attributeCheck": "Formula Modifier: Attribute Check",
    "system.globalFormulaModifiers.save": "Formula Modifier: Save",
    "system.globalFormulaModifiers.skillCheck": "Formula Modifier: Skill Check",
    "system.globalFormulaModifiers.healing": "Formula Modifier: Healing",
    "system.globalFormulaModifiers.attackDamage.martial.melee": "Formula Modifier: Melee Martial Damage",
    "system.globalFormulaModifiers.attackDamage.martial.ranged": "Formula Modifier: Ranged Martial Damage",
    "system.globalFormulaModifiers.attackDamage.spell.melee": "Formula Modifier: Melee Spell Damage",
    "system.globalFormulaModifiers.attackDamage.spell.ranged": "Formula Modifier: Ranged Spell Damage",

    // Roll Level
    "system.rollLevel.onYou.martial.melee": "Roll Level with Melee Martial Attack",
    "system.rollLevel.onYou.martial.ranged": "Roll Level with Ranged Martial Attack",
    "system.rollLevel.onYou.spell.melee": "Roll Level with Melee Spell Attack",
    "system.rollLevel.onYou.spell.ranged": "Roll Level with Ranged Spell Attack",

    "system.rollLevel.onYou.checks.mig": "Roll Level with Might Checks",
    "system.rollLevel.onYou.checks.agi": "Roll Level with Agility Checks",
    "system.rollLevel.onYou.checks.cha": "Roll Level with Charisma Checks",
    "system.rollLevel.onYou.checks.int": "Roll Level with Inteligence Checks",
    "system.rollLevel.onYou.checks.att": "Roll Level with Attack Check",
    "system.rollLevel.onYou.checks.spe": "Roll Level with Spell Check",
    "system.rollLevel.onYou.initiative": "Roll Level with Initiative Check",

    "system.rollLevel.onYou.skills": "Roll Level with Skill Check",
    "system.rollLevel.onYou.tradeSkills": "Roll Level with Trade Check",

    "system.rollLevel.onYou.saves.mig": "Roll Level with Might Saves",
    "system.rollLevel.onYou.saves.agi": "Roll Level with Agility Saves",
    "system.rollLevel.onYou.saves.cha": "Roll Level with Charisma Saves",
    "system.rollLevel.onYou.saves.int": "Roll Level with Inteligence Saves",
    "system.rollLevel.onYou.deathSave": "Roll Level with Death Save",

    "system.rollLevel.againstYou.martial.melee": "Against You: Roll Level with Melee Martial Attack ",
    "system.rollLevel.againstYou.martial.ranged": "Against You: Roll Level with Ranged Martial Attack",
    "system.rollLevel.againstYou.spell.melee": "Against You: Roll Level with Melee Spell Attack",
    "system.rollLevel.againstYou.spell.ranged": "Against You: Roll Level with Ranged Spell Attack",

    "system.rollLevel.againstYou.checks.mig": "Against You: Roll Level with Might Checks",
    "system.rollLevel.againstYou.checks.agi": "Against You: Roll Level with Agility Checks",
    "system.rollLevel.againstYou.checks.cha": "Against You: Roll Level with Charisma Checks",
    "system.rollLevel.againstYou.checks.int": "Against You: Roll Level with Inteligence Checks",
    "system.rollLevel.againstYou.checks.att": "Against You: Roll Level with Attack Check",
    "system.rollLevel.againstYou.checks.spe": "Against You: Roll Level with Spell Check",

    "system.rollLevel.againstYou.skills": "Against You: Roll Level with Skill Check",
    "system.rollLevel.againstYou.tradeSkills": "Against You: Roll Level with Trade Check",

    "system.rollLevel.againstYou.saves.mig": "Against You: Roll Level with Might Saves",
    "system.rollLevel.againstYou.saves.agi": "Against You: Roll Level with Agility Saves",
    "system.rollLevel.againstYou.saves.cha": "Against You: Roll Level with Charisma Saves",
    "system.rollLevel.againstYou.saves.int": "Against You: Roll Level with Inteligence Saves",

    // Events
    "system.events": "Events",
  }
}

function _damageReduction$2() {
  const reduction = {};
  Object.entries(CONFIG.DC20RPG.DROPDOWN_DATA.damageTypes).forEach(([key, dmgLabel]) => {
    if (key !== "true") {
      reduction[`system.damageReduction.damageTypes.${key}.resist`] = `${dmgLabel} - Resistance (X)`;
      reduction[`system.damageReduction.damageTypes.${key}.resistance`] = `${dmgLabel} - Resistance (Half)`;
      reduction[`system.damageReduction.damageTypes.${key}.immune`] = `${dmgLabel} - Resistance (Immune)`;
      reduction[`system.damageReduction.damageTypes.${key}.vulnerable`] = `${dmgLabel} - Vulnerable (X)`;
      reduction[`system.damageReduction.damageTypes.${key}.vulnerability`] = `${dmgLabel} - Vulnerability (Double)`;
    } 
  });
  return reduction;
}

function _statusResistances$1() {
  const statusResistances = {};
  Object.entries(CONFIG.DC20RPG.DROPDOWN_DATA.statusResistances).forEach(([key, condLabel]) => {
    statusResistances[`system.statusResistances.${key}.immunity`] = `${condLabel} Immunity`;
    statusResistances[`system.statusResistances.${key}.resistance`] = `${condLabel} Resistance`;
    statusResistances[`system.statusResistances.${key}.vulnerability`] = `${condLabel} Vulnerability`;
  });
  return statusResistances;
}

function _attributeBonuses() {
  const attributes = {};
  const checks = {};
  const saves = {};
  Object.entries(CONFIG.DC20RPG.TRANSLATION_LABELS.attributes).forEach(([key, atrLabel]) => {
    attributes[`system.attributes.${key}.bonuses.value`] = `Attribute Value Bonus: ${atrLabel}`;
    checks[`system.attributes.${key}.bonuses.check`] = `Attribute Check Bonus: ${atrLabel}`;
    saves[`system.attributes.${key}.bonuses.save`] = `Save Bonus: ${atrLabel}`;
  });
  return {...attributes, ...checks, ...saves};
}

function _skillBonuses() {
  const skills = {};
  Object.entries(CONFIG.DC20RPG.skills)
    .forEach(([key, skillLabel]) => skills[`system.skills.${key}.bonus`] = `${skillLabel} - Skill Check Bonus`);

  Object.entries(CONFIG.DC20RPG.tradeSkills)
    .forEach(([key, skillLabel]) => skills[`system.tradeSkills.${key}.bonus`] = `${skillLabel} - Trade Skill Check Bonus`);

  return skills;
}

function _combatTraining$1() {
  const combatTraining = {};
  Object.entries(CONFIG.DC20RPG.TRANSLATION_LABELS.combatTraining)
    .forEach(([key, trainingLabel]) => combatTraining[`system.combatTraining.${key}`] = `Combat Training: ${trainingLabel}`);
  return combatTraining;
}

class DC20MeasuredTemplateDocument extends MeasuredTemplateDocument {

  prepareData() {
    super.prepareData();

    if (this.flags.dc20rpg?.hideHighlight && canvas.interface) {
      const highlight = canvas.interface.grid.highlightLayers[`MeasuredTemplate.${this.id}`];
      if (highlight) highlight.visible = false;
    }
  }

  async applyEffectsToTokensInTemplate() {
    const flags = this.flags.dc20rpg;
    if (!flags) return;

    const applyEffects = flags.itemData.applyEffects;
    if (!applyEffects?.applyFor) return;
    if (!this.object) return;

    // Determine disposition
    let dispositionOptions = [];
    if (applyEffects.applyFor === "enemy" || applyEffects.applyFor === "ally") {
      let casterDisposition;
      if (flags.itemData.tokenId) {
        const token = canvas.tokens.placeables.find(token => token.id === flags.itemData.tokenId);
        if (token) casterDisposition = token.disposition;
      }
      if (casterDisposition === undefined) {
        const actor = game.actors.get(flags.itemData.actorId);
        if (actor) casterDisposition = actor.prototypeToken.disposition;
      }

      const neutral = game.settings.get("dc20rpg", "neutralDispositionIdentity");
      if (applyEffects.applyFor === "ally") {
        let allyOfNeutralToken = 0;
        if (neutral === "friendly" && casterDisposition >= 0) {
          dispositionOptions.push(0);
          allyOfNeutralToken = 1;
        }
        if (neutral === "hostile" && casterDisposition <= 0) {
          dispositionOptions.push(0);
          allyOfNeutralToken = -1;
        }

        switch(casterDisposition) {
          case -2: break;
          case -1: dispositionOptions.push(-1); break;
          case 0: dispositionOptions.push(allyOfNeutralToken); break;
          case 1: dispositionOptions.push(1); break;
        }
      }
      else {
        if (neutral === "friendly" && casterDisposition < 0) dispositionOptions.push(0);
        if (neutral === "hostile" && casterDisposition > 0) dispositionOptions.push(0);
        if (neutral === "separated" && casterDisposition !== 0) dispositionOptions.push(0);

        switch(casterDisposition) {
          case -2: break;
          case -1: dispositionOptions.push(1); break;
          case 1: dispositionOptions.push(-1); break;
          case 0: 
            if (neutral === "friendly") dispositionOptions.push(-1);
            if (neutral === "hostile") dispositionOptions.push(1);
            if (neutral === "separated") {
              dispositionOptions.push(1);
              dispositionOptions.push(-1);
            }
        }
      }
    }

    const alreadyApplied = new Set(flags.effectAppliedTokens);
    const tokens = getTokensInsideMeasurementTemplate(this.object, dispositionOptions);
    
    // Collect tokens in template that effects should be applied to
    const tokensToConfirm = [];
    const applied = new Set();
    for (const token of Object.values(tokens)) {
      if (alreadyApplied.has(token.id)) applied.add(token.id);
      else {
        applied.add(token.id);
        tokensToConfirm.push(token);
      }
    }
    
    // Confirm effect application
    if (tokensToConfirm.length > 0) {
      const confirmedTokens = applyEffects.applyFor === "selector" ? await getTokenSelector(tokensToConfirm, "Apply Effect to tokens?") : tokensToConfirm;
      for (const token of confirmedTokens) {
        const actor = token.actor;
        for (const effectData of applyEffects.effects) {
          await createEffectOn(effectData, actor);
        }
      }
    }

    // Remove effects from tokens
    const removed = Array.from(alreadyApplied.difference(applied));
    const removedTokens = canvas.tokens.placeables.filter(token => removed.includes(token.id));
    for (const token of removedTokens) {
      const actor = token.actor;
      if (actor) {
        for (const effectData of applyEffects.effects) {
          let effect = getEffectByKey(effectData.flags.dc20rpg?.effectKey, actor);
          if (!effect) effect = getEffectByName(effectData.name, actor);
          if (effect) await deleteEffectFrom(effect.id, actor);
        }
      }
    }

    // Set new applied tokens
    await this.update({["flags.dc20rpg.effectAppliedTokens"]: Array.from(applied)});
  }

  /** @override */
  _onUpdate(changed, options, userId) {
    super._onUpdate(changed, options, userId);
    if (userId === game.user.id) {
      if ((changed.hasOwnProperty("x") || changed.hasOwnProperty("y")) && !changed.skipUpdateCheck) {
        this.applyEffectsToTokensInTemplate();
      }
      if (this.flags.dc20rpg?.hideHighlight) {
        canvas.interface.grid.highlightLayers[`MeasuredTemplate.${this.id}`].visible = false;
      }
    }
  }

  /** @override */
  async _preDelete(options, user) {
    super._preDelete(options, user);
    
    // Remove effects connected to mesured template
    const flags = this.flags.dc20rpg;
    if (!flags) return;
    const applyEffects = flags.itemData.applyEffects;
    if (!applyEffects?.applyFor) return;
    if (!this.object) return;

    // Remove effects from tokens
    const alreadyApplied = flags.effectAppliedTokens;
    const removedTokens = canvas.tokens.placeables.filter(token => alreadyApplied.includes(token.id));
    for (const token of removedTokens) {
      const actor = token.actor;
      if (actor) {
        for (const effectData of applyEffects.effects) {
          let effect = getEffectByKey(effectData.flags.dc20rpg?.effectKey, actor);
          if (!effect) effect = getEffectByName(effectData.name, actor);
          if (effect) await deleteEffectFrom(effect.id, actor);
        }
      }
    }
  }

}

let waitingForRefresh = false;
async function checkMeasuredTemplateWithEffects() {
  if (waitingForRefresh === true) return;

  waitingForRefresh = true;
  await setTimeout(async () => {
    waitingForRefresh = false;
    for (const template of game.scenes.active.templates) {
      await template.applyEffectsToTokensInTemplate();
    }
  }, CONFIG.MeasuredTemplate.TEMPLATE_REFRESH_TIMEOUT);
}

class DC20RpgMeasuredTemplate extends MeasuredTemplate {

  static isDifficultTerrain(x, y) {
    const matches = DC20RpgMeasuredTemplate.getAllTemplatesOnPosition(x, y);
    for (const match of matches) {
      if (match.document.flags?.dc20rpg?.difficult) return true;
    }
    return false;
  }

  static getAllTemplatesOnPosition(x, y) {
    const templates = new Set();
    canvas.templates.documentCollection.forEach(templateDoc => {
      const template = templateDoc.object;
      if (template) {

        // Gridless
        if (canvas.grid.isGridless) {
          const shape = template._getGridHighlightShape();
          const startX = template.document.x;
          const startY = template.document.y;
          const pointX = x;
          const pointY = y;

          // Circle
          if (shape.type === 2) {
            const radius = shape.radius;
            const distanceSquared = (pointX - startX) ** 2 + (pointY - startY) ** 2;
            if (distanceSquared <= radius ** 2) templates.add(template);
          }

          // Ray
          if (shape.type === 0) {
            const shapePoints = shape.points;
            const polygon = [];
            for (let i = 0; i < shapePoints.length; i=i+2) {
              const x = shapePoints[i];
              const y = shapePoints[i+1];
              polygon.push({x: x, y: y});
            }
            if (isPointInPolygon(pointX, pointY, polygon)) templates.add(template);
          }
        }
        // Grid
        else {
          const highlightedSpaces = template.highlightedSpaces;
          highlightedSpaces.forEach(space => {
            if (space[0] === y && space[1] === x) {
              templates.add(template);
            }
          });
        }
      }
    });
    return templates;
  }

  static mapItemAreasToMeasuredTemplates(areas) {
    if (!areas) return {};

    const toSystemTemplate = (type) => {
      switch (type) {
        case "cone": case "arc":
          return CONST.MEASURED_TEMPLATE_TYPES.CONE;
        case "sphere": case "cylinder": case "aura":  case "radius":
          return CONST.MEASURED_TEMPLATE_TYPES.CIRCLE;
        case "line": case "wall": 
          return CONST.MEASURED_TEMPLATE_TYPES.RAY;
        case "cube":
          return CONST.MEASURED_TEMPLATE_TYPES.RECTANGLE; 
      }
    };

    const measurementTemplates = {};
    for (let [key, area] of Object.entries(areas)) {
      const type = area.area;
      const distance = area.distance;
      if (!type || !distance) continue;  // We need those values to be present for templates 

      const width = area.width;
      const angle = type === "arc" ? 180 : 53.13;

      if (type === "area") {
        measurementTemplates[key] = {
          type: type,
          distance: 0.5,
          width: 1,
          systemType: CONST.MEASURED_TEMPLATE_TYPES.CIRCLE,
          label: _createLabelForTemplate(type, distance),
          numberOfFields: distance,
          difficult: area.difficult,
          hideHighlight: area.hideHighlight,
        };
      }
      else {
        measurementTemplates[key] = {
          type: type,
          distance: distance,
          angle: angle,
          width: width,
          systemType: toSystemTemplate(type),
          label: _createLabelForTemplate(type, distance, width),
          difficult: area.difficult,
          hideHighlight: area.hideHighlight,
        };
      }
    }
    return measurementTemplates;
  }
  
  static async createMeasuredTemplates(template, refreshMethod, itemData) {
    if (!template) return [];

    const measuredTemplates = [];
    // Custom Area
    if (template.type === "area") {
      const label = template.label;
      let left = template.numberOfFields;
      template.label = label + ` <${left} Left>`;
      template.selected = true; 
      await refreshMethod();
  
      for(let i = 1; i <= template.numberOfFields; i++) {
        const mT = await DC20RpgMeasuredTemplate.pleacePreview(template.systemType, template, itemData);
        measuredTemplates.push(mT);
        left--;
        if (left) template.label = label + ` <${left} Left>`;
        else template.label = label;
        await refreshMethod();
      }
  
      template.selected = false; 
      await refreshMethod();
    }
    // Aura type
    else if (template.type === "aura") {
      let item = null;
      const actor = getActorFromIds(itemData.actorId, itemData.tokenId);
      if (actor) item = actor.items.get(itemData.itemId);
      if (item.system?.target?.type === "self") {
        const token = getTokenForActor(actor);
        if (token) {
          await DC20RpgMeasuredTemplate.addAuraToToken(template.systemType, token, template, itemData);
        }
      }
      else {
        const selected = await getTokenSelector(canvas.tokens.placeables, "Apply Aura to Tokens");
        for (const token of selected) {
          await DC20RpgMeasuredTemplate.addAuraToToken(template.systemType, token, template, itemData);
        }
      }
    }
    // Predefined type
    else {
      template.selected = true; 
      await refreshMethod();
      const mT = await DC20RpgMeasuredTemplate.pleacePreview(template.systemType, template, itemData);
      measuredTemplates.push(mT);
      template.selected = false; 
      await refreshMethod();
    }
    return measuredTemplates;
  }

  static changeTemplateSpaces(template, numericChange) {
    if (!template) return;

    // Custom Area
    if (template.type === "area") {
      if (template.numberOfFields + numericChange < 0) template.numberOfFields = 0;
      else template.numberOfFields += numericChange;
      template.label = _createLabelForTemplate(template.type, template.numberOfFields);
    }
    // Standard options
    else {
      if (template.distance + numericChange < 0) template.distance = 0;
      else template.distance += numericChange;
      template.label = _createLabelForTemplate(template.type, template.distance, template.width);
    }
  }

  static async pleacePreview(type, config={}, itemData) {
    const angle = config.angle || CONFIG.MeasuredTemplate.defaults.angle;
    let width = config.width || 1;
    let distance = config.distance || 1;

    // We want to replace rectangle with cube shapded ray as it suits better preview purposes
    if (type === "rect") {
      type = CONST.MEASURED_TEMPLATE_TYPES.RAY;
      width = distance;
    }

    const templateData = {
      t: type,
      user: game.user.id,
      x: 0,
      y: 0,
      distance: distance,
      angle: angle,
      width: width,
      direction: 0,
      fillColor: game.user.color,
      flags: {
        dc20rpg: {
          difficult: config.difficult,
          itemData: itemData,
          effectAppliedTokens: [],
          hideHighlight: config.hideHighlight
        },
      }
    };

    const templateDocument = new MeasuredTemplateDocument(templateData, {parent: canvas.scene});
    const preview = new this(templateDocument);
    const initialLayer = canvas.activeLayer;
    preview.draw();
    preview.layer.activate();
    preview.layer.preview.addChild(preview);

    const SNAP = CONST.GRID_SNAPPING_MODES;
    const grid = canvas.grid;

    // Place preview and return created template
    return await new Promise((resolve) => {
      // Moving template preview
      canvas.stage.on("mousemove", (event) => {
        event.stopPropagation();
        const now = Date.now(); // Apply a 30ms throttle
        if ( now - preview.timeFromLastMove <= 30 ) return;

        let mode = grid.isHexagonal 
                        ? SNAP.CENTER | SNAP.VERTEX 
                        : SNAP.CENTER | SNAP.VERTEX | SNAP.CORNER | SNAP.SIDE_MIDPOINT;
        if (event.shiftKey || canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
          mode = 0;
        }

        const point = event.data.getLocalPosition(preview.layer);
        const finalPosition = event.shiftKey 
          ? grid.getSnappedPoint(point, {mode: 0})
          : grid.getSnappedPoint(point, {mode: mode});
    
        // Update the template's position
        preview.document.updateSource(finalPosition);
        preview.refresh();
        preview.timeFromLastMove = now;
      });

      // Rotating template preview
      canvas.app.view.onwheel = (event) => {
        event.stopPropagation();
        const delta = canvas.grid.type > CONST.GRID_TYPES.SQUARE ? 30 : 15;
        const snap = event.shiftKey ? delta : 5;
        const update = {direction: preview.document.direction + (snap * Math.sign(event.deltaY))};
        preview.document.updateSource(update);
        preview.refresh();
      };

      // Creating or canceling template creation
      canvas.stage.on("pointerup", async (event) => {
        event.stopPropagation();
        canvas.stage.off("mousemove");
        canvas.stage.off("pointerup");
        canvas.app.view.onwheel = null;

        // Place template
        if (event.data.button === 0) {
          const templateData = preview.document.toObject();
          const shape = preview.shape;
          preview.destroy();

          const templateDocument = await DC20MeasuredTemplateDocument.create(templateData, {parent: canvas.scene});
          const template = templateDocument.object;
          template.shape = shape;
          templateDocument.applyEffectsToTokensInTemplate();

          initialLayer.activate();
          resolve(template);
        }
        
        // Cancel template pleacement
        if (event.data.button === 2) {
          preview.destroy();
          initialLayer.activate();
          resolve(null);
        }
      });
    });
  }

  static async addAuraToToken(type, token, config={}, itemData) {
    const tokenWidth = token.document.width;
    const tokenSizeMod = tokenWidth > 1 ? tokenWidth/2 : 0;

    const templateData = {
      t: type,
      user: game.user.id,
      x: token.center.x,
      y: token.center.y,
      distance: config.distance + tokenSizeMod,
      direction: 0,
      fillColor: game.user.color,
      flags: {
        dc20rpg: {
          difficult: config.difficult,
          itemData: itemData,
          effectAppliedTokens: [],
          hideHighlight: config.hideHighlight
        },
      }
    };

    const templateDocument = await DC20MeasuredTemplateDocument.create(templateData, {parent: canvas.scene});
    await templateDocument.update({["flags.dc20rpg.linkedToken"]: token.id});
    const template = templateDocument.object;

    // Link Template with token
    const linkedTemplates = token.document.flags.dc20rpg?.linkedTemplates || [];
    linkedTemplates.push(templateDocument.id);
    await token.document.update({["flags.dc20rpg.linkedTemplates"]: linkedTemplates});

    templateDocument.applyEffectsToTokensInTemplate();
    return template;
  }

  get highlightedSpaces() {
    return this._getGridHighlightPositions().map(position => {
      const range = canvas.grid.getOffsetRange(position);
      // All those positions are 1x1 so startX === endX, we dont need both;
      return [range[1], range[0]];
    });
  }
}

function _createLabelForTemplate(type, distance, width, unit) {
  const widthLabel = width && type === "line" ? ` x ${width}` : "";
  const unitLabel = game.i18n.localize("dc20rpg.measurement.spaces");
  
  let label = game.i18n.localize(`dc20rpg.measurement.${type}`);
  label += ` [${distance}${widthLabel} ${unitLabel}]`;
  return label;
}

function isStackable(statusId) {
  const status = CONFIG.statusEffects.find(e => e.id === statusId);
  if (status) return status.stackable;
  else return false;
}

async function addStatusWithIdToActor(actor, id, extras) {
  actor.toggleStatusEffect(id, { active: true, extras: extras });
}

async function removeStatusWithIdFromActor(actor, id) {
  actor.toggleStatusEffect(id, { active: false, extras: {} });
}

function toggleStatusOn(statusId, owner, addOrRemove) {
  if (addOrRemove === 1) addStatusWithIdToActor(owner, statusId);
  if (addOrRemove === 3) removeStatusWithIdFromActor(owner, statusId);
}

function hasStatusWithId(actor, statusId) {
  for ( const status of actor.statuses) {
    if (status.id === statusId) return true;
  }
  return false;
}

function getStatusWithId(actor, statusId) {
  for ( const status of actor.statuses) {
    if (status.id === statusId) return status;
  }
  return null;
}

function enhanceStatusEffectWithExtras(effect, extras) {
  if (!extras) return effect;
  const changes = effect.changes;

  if (extras.mergeDescription) {
    effect.description += extras.mergeDescription;
  }
  if (extras.untilFirstTimeTriggered) {
    changes.forEach(change => _enhnanceRollLevel(change));
    changes.push(_newEvent("targetConfirm", effect.name, extras.actorId)); 
  }
  if (extras.untilTargetNextTurnStart) {
    changes.push(_newEvent("turnStart", effect.name, extras.actorId));
  }
  if (extras.untilTargetNextTurnEnd) {
    changes.push(_newEvent("turnEnd", effect.name, extras.actorId));
  }
  if (extras.untilYourNextTurnStart) {
    changes.push(_newEvent("actorWithIdStartsTurn", effect.name, extras.actorId));
  }
  if (extras.untilYourNextTurnEnd) {
    const activeCombat = game.combats.active;
    if (activeCombat && activeCombat.started) {
      const isCurrent = activeCombat.isActorCurrentCombatant(extras.actorId);
      if (isCurrent) changes.push(_newEvent("actorWithIdEndsNextTurn", effect.name, extras.actorId));
      else changes.push(_newEvent("actorWithIdEndsTurn", effect.name, extras.actorId));
    }
    else {
      changes.push(_newEvent("actorWithIdEndsTurn", effect.name, extras.actorId));
    }
  }
  if (extras.repeatedSave && extras.repeatedSaveKey !== "") {
    changes.push(_repeatedSave(effect.name, extras.repeatedSaveKey, extras.against, extras.id));
  }
  if (extras.forOneMinute) {
    effect.duration.rounds = 5;
    if (!effect.flags.dc20rpg) {
      effect.flags.dc20rpg = {
        duration: {
          useCounter: true,
          onTimeEnd: "delete"
        }
      };
    }
    else {
      effect.flags.dc20rpg.duration.useCounter = true;
      effect.flags.dc20rpg.duration.onTimeEnd = "delete";
    }
  }
  effect.changes = changes;
  return effect;
}

function _newEvent(trigger, label, actorId) {
  let change = `
  "trigger": "${trigger}",
  "eventType": "basic", 
  "label": "${label}",
  "postTrigger": "delete"
  `;
  if (actorId) change = `"actorId": "${actorId}",` + change;
  return {
    key: "system.events",
    mode: 2,
    priority: null,
    value: change
  }
}

function _repeatedSave(label, checkKey, against, statusId) {
  const change = `
  "eventType": "saveRequest", 
  "trigger": "turnEnd", 
  "label": "${label} - Repeated Save", 
  "checkKey": "${checkKey}", 
  "against": "${against}", 
  "statuses": ["${statusId}"], 
  "onSuccess": "delete"
  `;
  return {
    key: "system.events",
    mode: 2,
    priority: null,
    value: change
  }
}

function _enhnanceRollLevel(change) {
  if (change.key.includes("system.rollLevel.")) {
    change.value = '"afterRoll": "delete", ' + change.value;
  }
}

function fullyStunnedCheck(actor) {
  if (!actor.hasStatus("stunned")) return;
  const stunned = actor.statuses.find(status => status.id === "stunned");
  if (!stunned) return;

  // Add Fully Stunned condition
  if (stunned.stack >= 4) {
    if (actor.hasStatus("fullyStunned")) return;
    actor.toggleStatusEffect("fullyStunned", { active: true });
  } 
  
  // Remove Fully Stunned condition
  if (stunned.stack < 4) {
    if (!actor.hasStatus("fullyStunned")) return;
    actor.toggleStatusEffect("fullyStunned", { active: false });
  }
}

function exhaustionCheck(actor) {
  // Add Dead condition
  if (actor.exhaustion >= 6) {
    if (actor.hasStatus("dead")) return;
    actor.toggleStatusEffect("dead", { active: true });
  }
}

function dazedCheck(actor) {
  if (actor.hasStatus("dazed")) {
    const sustained = actor.system.sustain;
    for (const sustain of sustained) {
      sendDescriptionToChat(actor, {
        rollTitle: `${sustain.name} - Sustain dropped [Dazed]`,
        image: sustain.img,
        description: `You are no longer sustaining '${sustain.name}' - You can't Sustain an effect while Dazed`,
      });
    }
    actor.update({["system.sustain"]: []});
  }
}

function healthThresholdsCheck(currentHP, actor) {
  const maxHP = actor.system.resources.health.max;
  const deathThreshold = actor.type === "character" ? actor.system.death.treshold : 0;

  _checkStatus("bloodied", currentHP, Math.floor(maxHP/2), actor);
  _checkStatus("wellBloodied", currentHP, Math.floor(maxHP/4), actor);
  if (actor.type === "character") _checkStatus("deathsDoor", currentHP, 0, actor);
  _checkStatus("dead", currentHP, deathThreshold, actor);
}

function _checkStatus(statusId, currentHP, treshold, actor) {
  if (actor.hasStatus(statusId)) {
    if (currentHP > treshold) removeStatusWithIdFromActor(actor, statusId); 
  }
  else {
    if (currentHP <= treshold) addStatusWithIdToActor(actor, statusId);
  }
}

function enhanceOtherRolls(winningRoll, otherRolls, checkDetails) {
  if (checkDetails?.checkDC && checkDetails?.againstDC) {
    _degreeOfSuccess(checkDetails.checkDC, winningRoll, otherRolls);
  }
}

//========================================
//           TARGET PREPARATION          =
//========================================
/**
 * Add informations such as hit/miss and expected damage/healing done.
 */
function enhanceTarget(target, rolls, details, applierId) {
  const actionType = details.actionType;
  const winner = rolls.winningRoll;
  
  // Prepare Common Data
  const data = {
    isAttack: actionType === "attack",
    isCheck: actionType === "check",
    canCrit: details.canCrit,
    rollTotal: winner?._total,
    isCritHit: winner?.crit,
    isCritMiss: winner?.fail,
    conditionals: details.conditionals,
    applierId: applierId
  };
  // Prepare Attack Data
  if (actionType === "attack") {
    data.defenceKey = details.targetDefence;
    data.halfDmgOnMiss = details.halfDmgOnMiss;
    data.skipFor = details.skipBonusDamage;
    if (!target.noTarget) target.attackOutcome = getAttackOutcome(target, data);
  }
  // Prepare Check Data
  if (actionType === "check") {
    data.checkDC = details.checkDetails?.checkDC;
    data.againstDC = details.checkDetails?.againstDC;
  }

  // Prepare target specific rolls
  rolls = collectTargetSpecificFormulas(target, data, rolls);
  if (game.settings.get("dc20rpg", "mergeDamageTypes")) _mergeFormulasByType(rolls);

  // Prepare final damage and healing
  target.dmg = _prepareRolls(rolls.dmg, target, data, true);
  target.heal = _prepareRolls(rolls.heal, target, data, false);

  // Prepare additional target specific fields
  target.effects = collectTargetSpecificEffects(target, data);
  target.rollRequests = collectTargetSpecificRollRequests(target, data);
}

function _prepareRolls(rolls, target, data, isDamage) {
  const prepared = {};
  for (const rll of rolls) {
    const showModified = Object.keys(prepared).length === 0; // By default only 1st roll should be modified
    const roll = _formatRoll(rll);
    const calculateData = {...data, isDamage: isDamage, isHealing: !isDamage};
    
    let defenceOverriden = false;
    delete calculateData.hit; // We always want to calculate hit value in the calculateForTarget function
    if (rll.modified.overrideDefence && rll.modified.overrideDefence !== calculateData.defenceKey) {
      calculateData.defenceKey = rll.modified.overrideDefence;
      defenceOverriden = true;
    }

    const finalRoll = target.noTarget ? calculateNoTarget(roll, calculateData) : calculateForTarget(target, roll, calculateData);
    const key = generateKey();
    finalRoll.showModified = showModified;
    finalRoll.targetSpecific = rll.targetSpecific;
    if (defenceOverriden) finalRoll.overridenDefence = rll.modified.overrideDefence;
    prepared[key] = finalRoll;
  }
  return prepared;
}

function _formatRoll(roll) {
  return {
    modified: {
      value: roll.modified._total,
      source: roll.modified.modifierSources,
      type: roll.modified.type,
      each5Value: roll.modified.each5Roll?.total || 0,
      failValue: roll.modified.failRoll?.total || 0,
    },
    clear: {
      value: roll.clear._total,
      source: roll.clear.modifierSources,
      type: roll.clear.type
    }
  }
}

function _degreeOfSuccess(checkDC, winningRoll, rolls, otherRoll) {
  const checkValue = winningRoll._total;
  const natOne = winningRoll.fail;

  if (rolls) rolls.forEach(roll => {
    const modified = roll ;
    // Check Failed
    if (natOne || (checkValue < checkDC)) {
      const failRoll = modified.failRoll;
      if (failRoll) {
        modified.modifierSources = modified.modifierSources.replace("Base Value", "Check Failed");        modified._formula = failRoll.formula;
        modified._total = failRoll.total;
        modified.terms = failRoll.terms;
      }
    }
    // Check succeed by 5 or more
    else if (checkValue >= checkDC + 5) {
      const each5Roll = modified.each5Roll;
      if (each5Roll) {
        const degree = Math.floor((checkValue - checkDC) / 5);
        const formula = degree > 1 ? `(${degree} * ${each5Roll.formula})` : each5Roll.formula;

        modified.modifierSources = modified.modifierSources.replace("Base Value", `Check Succeeded over ${(degree * 5)}`);
        modified._formula += ` + ${formula}`;
        modified._total += (degree * each5Roll.total);
      }
    }
    roll = modified;
  });
}

function _mergeFormulasByType(rolls) {
  const dmgByType = new Map();
  const healByType = new Map();

  // Damage Rolls
  for (const roll of rolls.dmg) {
    if (roll.modified.dontMerge) {
      dmgByType.set(generateKey(), roll);
      continue;
    }

    const type = roll.modified.type;
    if (dmgByType.has(type)) {
      const rollByType = dmgByType.get(type);
      rollByType.modified._total += roll.clear._total;  // We want to add roll without modifications
      rollByType.clear._total += roll.clear._total;     // in both cases (clear and modified)
      rollByType.modified.modifierSources += ` + ${roll.clear.modifierSources}`;
      rollByType.clear.modifierSources += ` + ${roll.clear.modifierSources}`;
    }
    else dmgByType.set(type, roll);
  }

  // Healing Rolls
  for (const roll of rolls.heal) {
    if (roll.modified.dontMerge) {
      healByType.set(generateKey(), roll);
      continue;
    }

    const type = roll.modified.type;
    if (healByType.has(type)) {
      const rollByType = healByType.get(type);
      rollByType.modified._total += roll.clear._total;  // We want to add roll without modifications
      rollByType.clear._total += roll.clear._total;     // in both cases (clear and modified)
      rollByType.modified.modifierSources += ` + ${roll.clear.modifierSources}`;
      rollByType.clear.modifierSources += ` + ${roll.clear.modifierSources}`;
    }
    else healByType.set(type, roll);
  }

  rolls.dmg = Array.from(dmgByType.values());
  rolls.heal = Array.from(healByType.values());
}

//========================================
//            ROLL PREPARATION           =
//========================================
function prepareRollsInChatFormat(rolls) {
  const coreRoll = rolls.core ? _packedRoll(rolls.core) : null;
  const otherRolls = [];
  const dmgRolls = [];
  const healingRolls = [];
  if (rolls.formula) {
    rolls.formula.forEach(roll => {
      if (roll.modified.category === "other") {
        otherRolls.push(_packedRoll(roll.modified));
      }
      if (roll.clear.category === "damage") {
        dmgRolls.push({
          modified: _packedRoll(roll.modified),
          clear: _packedRoll(roll.clear)
        });
      }
      if (roll.clear.category === "healing") {
        healingRolls.push({
          modified: _packedRoll(roll.modified),
          clear: _packedRoll(roll.clear)
        });
      }
    });
  }
  return {
    core: coreRoll,
    other: otherRolls,
    dmg: dmgRolls,
    heal: healingRolls
  }
}
// We need to pack rolls in order to contain them after rendering message.
function _packedRoll(roll) {
  return {...roll};
}

class DC20ChatMessage extends ChatMessage {

  /** @overriden */
  prepareDerivedData() {
    super.prepareDerivedData();
    if (this.system.chatFormattedRolls?.core) this._prepareRolls();
    const system = this.system;
    if (!system.hasTargets) return;

    // Initialize applyToTargets flag for the first time
    if (system.applyToTargets === undefined) {
      if (system.targetedTokens.length > 0) system.applyToTargets = true;
      else system.applyToTargets = false;
    }
    this._prepareDisplayedTargets();
    this._prepareMeasurementTemplates();
  }

  _prepareRolls() {
    const rollLevel = this.system.rollLevel;
    const chatRolls = this.system.chatFormattedRolls;
    let winner = chatRolls.core;
    const extraRolls = this.system.extraRolls;

    // Check if any extra roll should repleace winner
    if (extraRolls) {
      winner.ignored = true;
      extraRolls.forEach(roll => {
        roll.ignored = true;
        if (rollLevel > 0) {
          if (roll._total > winner._total) winner = roll;
        }
        else {
          if (roll._total < winner._total) winner = roll;
        }
      });
    }

    winner.ignored = false;
    chatRolls.winningRoll = winner;
    this.system.coreRollTotal = winner._total;

    // If there were any "other" rolls we need to enhance those
    if (chatRolls?.other) enhanceOtherRolls(winner, chatRolls.other, this.system.checkDetails);
  }

  _prepareMeasurementTemplates() {
    const areas = this.system.areas;
    if (!areas) return;
    const measurementTemplates = DC20RpgMeasuredTemplate.mapItemAreasToMeasuredTemplates(areas);
    if (Object.keys(measurementTemplates).length > 0) {
      this.system.measurementTemplates = measurementTemplates;
    }
  }

  _prepareDisplayedTargets(startWrapped) {
    this.noTargetVersion = false;
    const system = this.system;
    const rolls = system.chatFormattedRolls;

    let targets = [];
    if (system.applyToTargets) targets = this._tokensToTargets(this._fetchTokens(system.targetedTokens));   // From targets
    else if (game.user.isGM) targets = this._tokensToTargets(getSelectedTokens());      // From selected tokens (only for the GM)
    else {                                                                          
      targets = this._noTargetVersion();                        // No targets (only for the Player)
      this.noTargetVersion = true;                              // We always want to show damage/healing for No Target version
    }                                         

    const displayedTargets = {};
    targets.forEach(target => {
      enhanceTarget(target, rolls, system, this.speaker.actor);
      target.hideDetails = startWrapped;
      displayedTargets[target.id] = target;
    });
    system.targets = displayedTargets;
  }

  _fetchTokens(targetedTokens) {
    if (!game.canvas.tokens) return [];
    const tokens = [];
    for (const tokenId of targetedTokens) {
      const token = game.canvas.tokens.get(tokenId);
      if (token) tokens.push(token);
    }
    return tokens;
  }

  _tokensToTargets(tokens) {
    if (!tokens) return [];
    const targets = [];
    tokens.forEach(token => targets.push(tokenToTarget(token)));
    return targets;
  }

  _noTargetVersion() {
    return [{
      name: "No target selected",
      img: "icons/svg/mystery-man.svg",
      id: generateKey(),
      noTarget: true,
      effects: [],
    }];
  }

  /** @overriden */
  async getHTML() {
    // We dont want "someone rolled privately" messages.
    if (!this.isContentVisible) return "";

    const system = this.system;
    // Prepare content depending on messageType
    switch(system.messageType) {
      case "damage": case "healing": case "temporary": case "effectRemoval":
        this.content = await this._eventRevert();
        break;
      
      case "roll": case "description": 
        this.content = await this._rollAndDescription();
        break;
    }

    const html = await super.getHTML();
    this._activateListeners(html);        // Activete listeners on rendered template
    return html;
  }

  async _eventRevert() {
    const system = this.system;
    const contentData = {
      ...system,
      userIsGM: game.user.isGM
    };
    const templateSource = "systems/dc20rpg/templates/chat/event-revert-message.hbs";
    return await renderTemplate(templateSource, contentData);
  }

  async _rollAndDescription() {
    const system = this.system;
    const shouldShowDamage = (game.user.isGM || system.showDamageForPlayers || this.noTargetVersion);
    const canUserModify = this.canUserModify(game.user, "update");
    const applicableStatuses = this._prepareApplicableStatuses();
    const specialStatuses = this._prepareSpecialStatuses();

    const hasActionType = system.actionType ? true : false;
    const isHelpAction = system.actionType === "help";
    const userIsGM = game.user.isGM;
    const hasAnyEffectsToApply = system.applicableEffects?.length > 0 || applicableStatuses.length > 0 || specialStatuses.length > 0;
    const showEffectApplier = (userIsGM || hasAnyEffectsToApply || this._getNumberOfRollRequests()) && (hasActionType && !isHelpAction);
    
    const contentData = {
      ...system,
      userIsGM: userIsGM,
      shouldShowDamage: shouldShowDamage,
      canUserModify: canUserModify,
      applicableStatuses: applicableStatuses,
      specialStatuses: specialStatuses,
      hasAnyEffectsToApply: hasAnyEffectsToApply,
      showEffectApplier: showEffectApplier
    };
    const templateSource = "systems/dc20rpg/templates/chat/roll-chat-message.hbs";
    return await renderTemplate(templateSource, contentData);
  }

  _prepareApplicableStatuses() {
    const againstStatuses = this.system.againstStatuses;
    if (!againstStatuses) return [];

    const applicableStatuses = [];
    againstStatuses.forEach(againstStatus => {
      if (againstStatus.supressFromChatMessage) return;
      const status = CONFIG.statusEffects.find(e => e.id === againstStatus.id);
      if (status) applicableStatuses.push({
        img: status.img,
        name: status.name,
        status: againstStatus.id,
      });
    });
    return applicableStatuses;
  }

  _prepareSpecialStatuses() {
    const againstStatuses = this.system.againstStatuses;
    if (!againstStatuses) return [];

    const specialStatuses = [];
    againstStatuses.forEach(againstStatus => {
      if (againstStatus.supressFromChatMessage) return;
    });
    return specialStatuses;
  }

  _activateListeners(html) {
    // Basic functionalities
    html.find('.activable').click(ev => this._onActivable(datasetOf(ev).path));

    // Show/Hide description
    html.find('.expand-row').click(ev => {
      ev.preventDefault();
      const description = ev.target.closest(".chat_v2").querySelector(".expandable-row");
      if(description) description.classList.toggle('expand');
    });

    // Swap targeting mode
    html.find('.token-selection').click(() => this._onTargetSelectionSwap());
    html.find('.run-check-for-selected').click(ev => {
      ev.stopPropagation();
      this._prepareDisplayedTargets();
      ui.chat.updateMessage(this);
    });
    html.find('.wrap-target').click(ev => {
      const targetKey = datasetOf(ev).key;
      const targets = this.system.targets;
      if (targets) {
        const target = targets[targetKey];
        if (target) {
          target.hideDetails = !target.hideDetails;
          ui.chat.updateMessage(this);
        }
      }
    });

    // Templates
    html.find('.create-template').click(ev => this._onCreateMeasuredTemplate(datasetOf(ev).key));
    html.find('.add-template-space').click(ev => this._onAddTemplateSpace(datasetOf(ev).key));
    html.find('.reduce-template-space').click(ev => this._onReduceTemplateSpace(datasetOf(ev).key));
    
    //Rolls
    html.find('.roll-save').click(ev => this._onSaveRoll(datasetOf(ev).target, datasetOf(ev).key, datasetOf(ev).dc, datasetOf(ev).selectedNow));
    html.find('.roll-check').click(ev => this._onCheckRoll(datasetOf(ev).target, datasetOf(ev).key, datasetOf(ev).against, datasetOf(ev).selectedNow));
    html.find('.roll-check-selected').click(ev => this._onCheckRollSelected(datasetOf(ev).key, datasetOf(ev).against));
    html.find('.roll-save-selected').click(ev => this._onSaveRollSelected(datasetOf(ev).key, datasetOf(ev).dc));

    // Appliers
    html.find('.apply-damage').mousedown(ev => this._onApplyDamage(datasetOf(ev).target, datasetOf(ev).roll, datasetOf(ev).modified, ev.which === 3));
    html.find('.apply-damage').contextmenu(ev => {ev.stopPropagation(); ev.preventDefault();});
    html.find('.apply-healing').click(ev => this._onApplyHealing(datasetOf(ev).target, datasetOf(ev).roll, datasetOf(ev).modified));
    html.find('.apply-effect').click(ev => this._onApplyEffect(datasetOf(ev).index, [datasetOf(ev).target], datasetOf(ev).selectedNow));
    html.find('.apply-effect-target-specific').click(ev => this._onApplyTargetSpecificEffect(datasetOf(ev).index, [datasetOf(ev).target]));
    html.find('.apply-status').click(ev => this._onApplyStatus(datasetOf(ev).status, [datasetOf(ev).target], datasetOf(ev).selectedNow));

    // GM Menu
    html.find('.add-selected-to-targets').click(() => this._onAddSelectedToTargets());
    html.find('.remove-target').click(ev => this._removeFromTargets(ev));
    html.find('.target-confirm').click(() => this._onTargetConfirm());
    html.find('.apply-all').click(() => this._onApplyAll());
    html.find('.send-all-roll-requests').click(() => this._onSendRollAll());
    html.find('.apply-all-effects-fail').click(() => this._onApplyAllEffects(true));
    html.find('.apply-all-effects').click(() => this._onApplyAllEffects(false));
    html.find('.modify-roll').click(ev => this._onModifyRoll(datasetOf(ev).direction, datasetOf(ev).modified, datasetOf(ev).path));
    
    html.find('.revert-button').click(ev => {
      ev.stopPropagation();
      if (this.system.messageType === "effectRemoval") this._onRevertEffect();
      else this._onRevertHp();
    });

    // Modify rolls
    html.find('.add-roll').click(async ev => {ev.stopPropagation(); await this._addRoll(datasetOf(ev).type);});
    html.find('.remove-roll').click(ev => {ev.stopPropagation(); this._removeRoll(datasetOf(ev).type);});

    // Drag and drop
    html[0].addEventListener('dragover', ev => ev.preventDefault());
    html[0].addEventListener('drop', async ev => await this._onDrop(ev));
  }

  _onActivable(path) {
     let value = getValueFromPath(this, path);
     setValueForPath(this, path, !value);
     ui.chat.updateMessage(this);
  }

  _onTargetSelectionSwap() {
    const system = this.system;
    if (system.targetedTokens.length === 0) return;
    system.applyToTargets = !system.applyToTargets;
    this._prepareDisplayedTargets();
    ui.chat.updateMessage(this);
  }

  _onApplyEffect(index, targetIds, selectedNow) {
    const targets = this._getExpectedTargets(selectedNow);
    const effects = this.system.applicableEffects;
    if (Object.keys(targets).length === 0) return;
    
    const effect = effects[index];
    if (!effect) return;

    if (targetIds[0] === undefined) targetIds = [];
    // We dont want to modify original effect so we copy its data.
    const effectData = {...effect};
    this._replaceWithSpeakerId(effectData);
    const rollingActor = getActorFromIds(this.speaker.actor, this.speaker.token);
    injectFormula(effectData, rollingActor);
    effectData.flags.dc20rpg.applierId = this.speaker.actor;
    Object.values(targets).forEach(target => {
      if (targetIds.length > 0 && !targetIds.includes(target.id)) return;
      const actor = this._getActor(target);
      if (actor) createEffectOn(effectData, actor);
    });
  }

  _onApplyTargetSpecificEffect(index, targetIds) {
    const targets = this._getExpectedTargets();
    if (Object.keys(targets).length === 0) return;
    if (targetIds[0] === undefined) targetIds = [];

    Object.values(targets).forEach(target => {
      if (targetIds.length > 0 && !targetIds.includes(target.id)) return;

      const actor = this._getActor(target);
      if (!actor) return;

      const effects = index === -1 ? target.effects : [target.effects[index]];
      for (const effectData of effects) {
        this._replaceWithSpeakerId(effectData);
        const rollingActor = getActorFromIds(this.speaker.actor, this.speaker.token);
        injectFormula(effectData, rollingActor);
        effectData.flags.dc20rpg.applierId = this.speaker.actor;
        createEffectOn(effectData, actor);
      }
    });
  }

  _onApplyStatus(statusId, targetIds, selectedNow) {
    const targets = this._getExpectedTargets(selectedNow);
    if (Object.keys(targets).length === 0) return;

    if (targetIds[0] === undefined) targetIds = [];
    const againstStatus = this.system.againstStatuses.find(eff => eff.id === statusId);
    const extras = {...againstStatus, actorId: this.speaker.actor, ...this._repeatedSaveExtras()};
    Object.values(targets).forEach(target => {
      if (targetIds.length > 0 && !targetIds.includes(target.id)) return;
      const actor = this._getActor(target);
      if (actor) addStatusWithIdToActor(actor, statusId, extras);
    });
  }

  _repeatedSaveExtras() {
    const rollingActor = getActorFromIds(this.speaker.actor, this.speaker.token);
    const saveDC = rollingActor.system.saveDC.value;
    return {
      against: Math.max(saveDC.spell, saveDC.martial),
    }
  }

  _replaceWithSpeakerId(effect) {
    for (let i = 0; i < effect.changes.length; i++) {
      let changeValue = effect.changes[i].value;
      if (changeValue.includes("#SPEAKER_ID#")) {
        effect.changes[i].value = changeValue.replaceAll("#SPEAKER_ID#", this.speaker.actor);
      }
    }
  }

  _getExpectedTargets(selectedNow) {
    if (selectedNow !== "true") return this.system.targets; 
    const targets = {};
    this._tokensToTargets(getSelectedTokens()).forEach(target => targets[target.id] = target);
    return targets;
  }

  _onCheckRollSelected(key, against) {
    const targets = this._getExpectedTargets("true");
    Object.values(targets).forEach(target => this._onCheckRoll(target, key, against));
  }

  _onSaveRollSelected(key, dc) {
    const targets = this._getExpectedTargets("true");
    Object.values(targets).forEach(target => this._onSaveRoll(target, key, dc));
  }

  _onApplyAll() {
    const targets = this.system.targets;
    if (!targets) return;

    Object.entries(targets).forEach(([targetKey, target]) => {
      const targetDmg = target.dmg;
      const targetHeal = target.heal;

      // Apply Damage
      Object.entries(targetDmg).forEach(([dmgKey, dmg]) => {
        this._onApplyDamage(targetKey, dmgKey, dmg.showModified);
      });
      // Apply Healing
      Object.entries(targetHeal).forEach(([healKey, heal]) => {
        this._onApplyHealing(targetKey, healKey, heal.showModified);
      });
    });
  }

  _onSendRollAll() {
    const targets = this.system.targets;
    if (!targets) return;
    const numberOfRequests = this._getNumberOfRollRequests();
    if (numberOfRequests === 0) return;
    const rollRequests = this.system.rollRequests;

    if (numberOfRequests > 1) {
      ui.notifications.warn("There is more that one Roll Request. Cannot send automatic Request.");
      return;
    }

    Object.entries(targets).forEach(([targetKey, target]) => {
      if (rollRequests.saves) {
        Object.values(rollRequests.saves).forEach(save => this._onSaveRoll(targetKey, save.saveKey, save.dc));
      }
      if (rollRequests.contests) {
        Object.values(rollRequests.contests).forEach(contest => this._onCheckRoll(targetKey, contest.contestedKey, this.system.coreRollTotal));
      }
    });
  }

  _getNumberOfRollRequests() {
    const rollRequests = this.system.rollRequests;
    if (!rollRequests) return 0;

    const numberOfSaves = Object.keys(rollRequests.saves).length;
    const numberOfContests = Object.keys(rollRequests.contests).length;
    const numberOfRequests = numberOfSaves + numberOfContests;
    return numberOfRequests;
  }

  _onApplyAllEffects(failOnly) {
    const targetIds = [];
    if (failOnly) {
      const targets = this.system.targets;
      if (targets) {
        Object.values(targets).forEach(target => {
          const outcome = target.rollOutcome;
          if (outcome !== undefined && !outcome.success) {
            targetIds.push(target.id);
          }
        });
        // By default we check all targets if there is no Ids but
        // in this case we want to check none so we need to send any Id
        if (targetIds.length === 0) targetIds.push("NONE");
      }
    }
    // Apply Effects
    for (let i = 0; i < this.system.applicableEffects?.length || 0; i++) {
      this._onApplyEffect(i, targetIds);
    }
    this._onApplyTargetSpecificEffect(-1, targetIds);
    // Apply Statuses
    for (const status of this.system.againstStatuses) {
      this._onApplyStatus(status.id, targetIds);
    }
  }

  async _removeFromTargets(event) {
    event.stopPropagation();
    event.preventDefault();
    const targetKey = datasetOf(event).key;
    const newTargets = [];
    let applyToTargets = true;
    this.system.targetedTokens.forEach(target => {if (target !== targetKey) newTargets.push(target);});

    if (newTargets.length === 0) applyToTargets = false;
    await this.update({
      ["system.targetedTokens"]: newTargets,
      ["system.applyToTargets"]: applyToTargets,
    });
  }

  async _onAddSelectedToTargets() {
    const selected = getSelectedTokens().map(token => token.id);
    let applyToTargets = true;

    const newTargets = [...this.system.targetedTokens, ...selected];
    if (newTargets.length === 0) applyToTargets = false;
    await this.update({
      ["system.targetedTokens"]: newTargets,
      ["system.applyToTargets"]: applyToTargets,
    });
  }

  _onTargetConfirm() {
    const targets = this.system.targets;
    if (!targets) return;
    
    Object.values(targets).forEach(target => {
      const token = targetToToken(target);
      if (token) runEventsFor("targetConfirm", token.actor, triggerOnlyForIdFilter(this.speaker.actor));
    });
  }

  async _onCreateMeasuredTemplate(key) {
    const template = this.system.measurementTemplates[key];
    if (!template) return;

    const actor = getActorFromIds(this.speaker.actor, this.speaker.token);
    const item = getItemFromActor(this.flags.dc20rpg.itemId, actor);
    const applyEffects = getMesuredTemplateEffects(item, this.system.applicableEffects);
    const itemData = {itemId: this.flags.dc20rpg.itemId, actorId: this.speaker.actor, tokenId: this.speaker.token, applyEffects: applyEffects};
    const measuredTemplates = await DC20RpgMeasuredTemplate.createMeasuredTemplates(template, () => ui.chat.updateMessage(this), itemData);
    
    // We will skip Target Selector if we are using selector for applying effects
    if (applyEffects.applyFor === "selector") return;

    let tokens = {};
    for (let i = 0; i < measuredTemplates.length; i++) {
      const collectedTokens = getTokensInsideMeasurementTemplate(measuredTemplates[i]);
      tokens = {
        ...tokens,
        ...collectedTokens
      };
    }
    
    if (Object.keys(tokens).length > 0) tokens = await getTokenSelector(tokens, "Select Targets");
    if (tokens.length > 0) {
      const newTargets = tokens.map(token => token.id);
      await this.update({
        ["system.targetedTokens"]: newTargets,
        ["system.applyToTargets"]: true,
      });
    }
  }

  _onAddTemplateSpace(key) {
    const template = this.system.measurementTemplates[key];
    if (!template) return;
    DC20RpgMeasuredTemplate.changeTemplateSpaces(template, 1);
    ui.chat.updateMessage(this);
  }

  _onReduceTemplateSpace(key) {
    const template = this.system.measurementTemplates[key];
    if (!template) return;
    DC20RpgMeasuredTemplate.changeTemplateSpaces(template, -1);
    ui.chat.updateMessage(this);
  }

  _onModifyRoll(direction, modified, path) {
    modified = modified === "true"; // We want boolean
    const extra = direction === "up" ? 1 : -1;
    const source = (direction === "up" ? " + 1 " : " - 1 ") + "(Manual)";

    const toModify = getValueFromPath(this, path);
    if (modified) {
      toModify.modified.value += extra;
      toModify.modified.source += source;
    }
    else {
      toModify.clear.value += extra;
      toModify.clear.source += source;
    }
    ui.chat.updateMessage(this);
  }

  async _onApplyDamage(targetKey, dmgKey, modified, half) {
    const system = this.system;
    const target = system.targets[targetKey];
    const actor = this._getActor(target);
    if (!actor) return;

    const dmgModified = (modified === "true" || modified === true) ? "modified" : "clear";
    const dmg = target.dmg[dmgKey][dmgModified];
    const finalDmg = half ? {source: dmg.source + " - Half Damage", value: Math.ceil(dmg.value/2), type: dmg.type} : dmg;
    await applyDamage(actor, finalDmg, {messageId: this.id});
  }

  async _onApplyHealing(targetKey, healKey, modified) {
    const system = this.system;
    const target = system.targets[targetKey];
    const actor = this._getActor(target);
    if (!actor) return;

    const healModified = modified === "true" ? "modified" : "clear";
    const heal = target.heal[healKey][healModified];
    
    // Check if should allow for overheal
    const rollingActor = getActorFromIds(this.speaker.actor, this.speaker.token);
    heal.allowOverheal = rollingActor.system.globalModifier.allow.overheal;
    await applyHealing(actor, heal, {messageId: this.id});
  }

  async _onSaveRoll(targetKey, key, dc, againstStatuses) {
    const system = this.system;
    const target = typeof targetKey === 'string' ? system.targets[targetKey] : targetKey;
    const actor = this._getActor(target);
    if (!actor) return;

    if (!againstStatuses) againstStatuses = this.system.againstStatuses;
    const details = prepareSaveDetailsFor(key, dc, againstStatuses);
    this._rollAndUpdate(target, actor, details);
  }

  async _onCheckRoll(targetKey, key, against) {
    const system = this.system;
    const target = typeof targetKey === 'string' ? system.targets[targetKey] : targetKey;
    const actor = this._getActor(target);
    if (!actor) return;

    const againstStatuses = this.system.againstStatuses;
    if (["phy", "men", "mig", "agi", "int", "cha"].includes(key)) {
      this._onSaveRoll(targetKey, key, against, againstStatuses);
      return;
    }
    const details = prepareCheckDetailsFor(key, against, againstStatuses);
    this._rollAndUpdate(target, actor, details);
  }

  async _rollAndUpdate(target, actor, details) {
    let roll = null;
    if (game.user.isGM) roll = await promptRollToOtherPlayer(actor, details);
    else roll = await promptRoll(actor, details);

    if (!roll || !roll.hasOwnProperty("_total")) return;
    let rollOutcome = {
      success: "",
      label: ""
    };
    if (roll.crit) {
      rollOutcome.success = true;
      rollOutcome.label = "Critical Success";
    }
    else if (roll.fail) {
      rollOutcome.success = false;
      rollOutcome.label = "Critical Fail";
    }
    else {
      const rollTotal = roll._total;
      const rollSuccess = roll._total >= details.against;
      rollOutcome.success = rollSuccess;
      rollOutcome.label = (rollSuccess ? "Succeeded with " : "Failed with ") + rollTotal;
    }
    target.rollOutcome = rollOutcome;
    ui.chat.updateMessage(this);
  }

  _getActor(target) { // TODO move it to usage getActorFromIds
    if (!target) return;
    const token = game.canvas.tokens.get(target.id);
    if (!token) return;
    const actor = token.actor;
    return actor;
  }

  _onRevertHp() {
    const system = this.system;
    const type = system.messageType;
    const amount = system.amount;
    const uuid = system.actorUuid;

    const actor = fromUuidSync(uuid);
    if (!actor) return;

    const health = actor.system.resources.health;
    let newValue = health;
    if (type === "damage") newValue = health.value + amount;
    else newValue = health.value - amount;
    actor.update({["system.resources.health.value"]: newValue});
    this.delete();
  }

  _onRevertEffect() {
    const system = this.system;
    const effectData = system.effect;

    const uuid = system.actorUuid;
    const actor = fromUuidSync(uuid);
    if (!actor) return;

    createEffectOn(effectData, actor);
    this.delete();
  }

  async modifyCoreRoll(formula, modifyingActor, updateInfoMessage) {
    const coreRoll = this.system.chatFormattedRolls?.core;
    if (!coreRoll) return false;

    const rollData = modifyingActor ? modifyingActor.getRollData() : {};
    const roll = await evaluateFormula(formula, rollData);

    // Add new roll to core roll
    coreRoll._formula += ` + (${formula})`;
    coreRoll._total += roll.total;
    coreRoll.terms.push(...roll.terms);

    // Add new roll to extra rolls
    const extraRolls = this.system.extraRolls;
    if (extraRolls) {
      for (const extra of extraRolls) {
        extra._formula += ` + (${formula})`;
        extra._total += roll.total;
        extra.terms.push(...roll.terms);
      }
    }

    const updateData = {
      system: {
        chatFormattedRolls: {
          core: coreRoll
        },
        extraRolls: extraRolls
      }
    };

    if (this.canUserModify(game.user, "update")) {
      await this.update(updateData);
    }
    else {
      const activeGM = game.users.activeGM;
      if (!activeGM) {
        ui.notifications.error("There needs to be an active GM to proceed with that operation");
        return false;
      }
      emitSystemEvent("updateChatMessage", {
        messageId: this.id, 
        gmUserId: activeGM.id, 
        updateData: updateData
      });
    }

    if (updateInfoMessage) {
      if (!modifyingActor) {
        modifyingActor = getActorFromIds(this.speaker.actor, this.speaker.token);
      }
      sendDescriptionToChat(modifyingActor, {
        description: `${updateInfoMessage} (with value: ${roll.total})`,
        rollTitle: `${this.system.rollTitle} ${game.i18n.localize("dc20rpg.chat.wasModified")}`,
        image: modifyingActor.img
      });
    }
    return true;
  } 

  async _addHelpDiceToRoll(helpDice) {
    const actorId = helpDice.actorId;
    const tokenId = helpDice.tokenId;
    const helpDiceOwner = getActorFromIds(actorId, tokenId);
    if (!helpDiceOwner) return;

    const messageTitle = helpDice.customTitle || game.i18n.localize("dc20rpg.sheet.help.help");
    const success = await this.modifyCoreRoll(helpDice.formula, helpDiceOwner, messageTitle);
    if (success) await clearHelpDice(helpDiceOwner, helpDice.key);
  }

  async _addRoll(rollType) {
    const winningRoll = this.system.chatFormattedRolls.winningRoll;
    if (!winningRoll) return;

    // We need to make sure that user is not rolling to fast, because it can cause roll level bug
    if (this.rollInProgress) return;
    this.rollInProgress = true;

    // Advantage/Disadvantage is only a d20 roll
    const d20Roll = await new Roll("d20", null).evaluate(); 
    // Making Dice so Nice display that roll
    if (game.dice3d) await game.dice3d.showForRoll(d20Roll, this.user, true, null, false);

    // Now we want to make some changes to duplicated roll to match how our rolls look like
    const newRoll = this._mergeExtraRoll(d20Roll, winningRoll);
    const extraRolls = this.system.extraRolls || [];
    extraRolls.push(newRoll);

    // Determine new roll Level
    let newRollLevel = this.system.rollLevel;
    if (rollType === "adv") newRollLevel++;
    if (rollType === "dis") newRollLevel--;

    const updateData = {
      system: {
        extraRolls: extraRolls,
        rollLevel: newRollLevel
      }
    };
    await this.update(updateData);
    this.rollInProgress = false;
  }

  _removeRoll(rollType) {
    // There is nothing to remove, only one dice left
    if (this.system.rollLevel === 0) return;

    const extraRolls = this.system.extraRolls;
    // First we need to remove extra rolls
    if (extraRolls && extraRolls.length !== 0) this._removeExtraRoll(rollType);
    // If there are no extra rolls we need to remove one of real rolls
    else this._removeLastRoll(rollType);
  }

  _removeExtraRoll(rollType) {
    const extraRolls = this.system.extraRolls;
    // Remove last extra roll
    extraRolls.pop();

    // Determine new roll Level
    let newRollLevel = this.system.rollLevel;
    if (rollType === "adv") newRollLevel--;
    if (rollType === "dis") newRollLevel++;

    const updateData = {
      system: {
        extraRolls: extraRolls,
        rollLevel: newRollLevel
      }
    };
    this.update(updateData);
  }

  _removeLastRoll(rollType) {
    if (!rollType) return;
    let rollLevel = this.system.rollLevel;
    const absLevel = Math.abs(rollLevel);

    const winner = this.system.chatFormattedRolls.winningRoll;
    const d20Dices = winner.terms[0].results;
    d20Dices.pop();

    // We need to chenge some values for that roll
    const rollMods = winner._total - winner.flatDice;
    const valueOnDice = this._getNewBestValue(d20Dices, rollType);
    if (!valueOnDice) return;
    
    winner._formula = winner._formula.replace(`${absLevel + 1}d20`, `${absLevel}d20`);
    winner.number = absLevel;
    winner.terms[0].number = absLevel;
    winner.flatDice = valueOnDice;
    winner._total = valueOnDice + rollMods;
    winner.crit = valueOnDice === 20 ? true : false;
    winner.fail = valueOnDice === 1 ? true : false;

    const coreRoll = winner;

    // Determine new roll Level
    if (rollType === "adv") rollLevel--;
    if (rollType === "dis") rollLevel++;

    const updateData = {
      system: {
        rollLevel: rollLevel,
        ["chatFormattedRolls.core"]: coreRoll
      }
    };
    this.update(updateData);
  }

  _getNewBestValue(d20Dices, rollType) {
    // Get highest
    if (rollType === "adv") {
      let highest = d20Dices[0];
      for(let i = 1; i < d20Dices.length; i++) {
        if (d20Dices[i].result > highest.result) highest = d20Dices[i];
      }
      return highest?.result;
    }

    // Get lowest
    if (rollType === "dis") {
      let lowest = d20Dices[0];
      for(let i = 1; i < d20Dices.length; i++) {
        if (d20Dices[i].result < lowest.result) lowest = d20Dices[i];
      }
      return lowest?.result;
    }
  }

  _mergeExtraRoll(d20Roll, oldRoll) {
    const dice = d20Roll.terms[0];
    const valueOnDice = dice.results[0].result;

    // We want to extract old roll modifiers
    const rollMods = oldRoll._total - oldRoll.flatDice;

    const newRoll = foundry.utils.deepClone(oldRoll);
    newRoll.terms[0] = dice;
    newRoll.flatDice = valueOnDice;
    newRoll._total = valueOnDice + rollMods;
    newRoll.crit = valueOnDice === 20 ? true : false;
    newRoll.fail = valueOnDice === 1 ? true : false;
    newRoll._formula = `d20 + ${rollMods}`;
    return newRoll;
  }

  async _onDrop(event) {
    event.preventDefault();

    const droppedData  = event.dataTransfer.getData('text/plain');
    if (!droppedData) return;
    
    const helpDice = JSON.parse(droppedData);
    if (helpDice.type !== "help") return;

    await this._addHelpDiceToRoll(helpDice);
  }
}

/**
 * Creates chat message for given rolls.
 * 
 * @param {Object} rolls        - Separated in 3 categories: coreRolls (Array of Rolls), formulaRolls (Array of Rolls), winningRoll (Roll).
 * @param {DC20RpgActor} actor  - Speaker.
 * @param {Object} details      - Informations about labels, descriptions and other details.
 */
async function sendRollsToChat(rolls, actor, details, hasTargets, item) {
  const rollsInChatFormat = prepareRollsInChatFormat(rolls);
  const targets = [];
  if (hasTargets) game.user.targets.forEach(token => targets.push(token.id));

  const system = {
    ...details,
    hasTargets: hasTargets,
    targetedTokens: targets,
    roll: rolls.winningRoll,
    chatFormattedRolls: rollsInChatFormat,
    rolls: _rollsObjectToArray(rolls),
    messageType: "roll"
  };

  const message = await DC20ChatMessage.create({
    speaker: DC20ChatMessage.getSpeaker({ actor: actor }),
    rollMode: game.settings.get('core', 'rollMode'),
    rolls: _rollsObjectToArray(rolls),
    sound: CONFIG.sounds.dice,
    system: system,
    flags: {dc20rpg: {itemId: item?.id}}
  });
  if (item) await runTemporaryItemMacro(item, "postChatMessageCreated", actor, {chatMessage: message});
}

function _rollsObjectToArray(rolls) {
  const array = [];
  if (rolls.core) array.push(rolls.core);
  if (rolls.formula) {
    rolls.formula.forEach(roll => {
      array.push(roll.clear);
      array.push(roll.modified);
    });
  }
  return array;
}

async function sendDescriptionToChat(actor, details, item) {
  const system = {
      ...details,
      messageType: "description"
  };
  const message = await DC20ChatMessage.create({
    speaker: DC20ChatMessage.getSpeaker({ actor: actor }),
    sound: CONFIG.sounds.notification,
    system: system,
    flags: {dc20rpg: {itemId: item?.id}}
  });
  if (item) await runTemporaryItemMacro(item, "postChatMessageCreated", actor, {chatMessage: message});
}

function sendHealthChangeMessage(actor, amount, source, messageType) {
  const gmOnly = !game.settings.get("dc20rpg", "showEventChatMessage");
  const system = {
    actorName: actor.name,
    image: actor.img,
    actorUuid: actor.uuid,
    amount: amount,
    source: source,
    messageType: messageType
  };

  DC20ChatMessage.create({
    speaker: DC20ChatMessage.getSpeaker({ actor: actor }),
    sound: CONFIG.sounds.notification,
    system: system,
    whisper: gmOnly ? DC20ChatMessage.getWhisperRecipients("GM") : []
  });
}

function sendEffectRemovedMessage(actor, effect) {
  const gmOnly = !game.settings.get("dc20rpg", "showEventChatMessage");
  const system = {
    actorName: actor.name,
    image: actor.img,
    actorUuid: actor.uuid,
    effectImg: effect.img,
    messageType: "effectRemoval",
    source: `${effect.name} ${game.i18n.localize('dc20rpg.chat.effectRemovalDesc')} ${actor.name}`,
    effect: effect.toObject()
  };

  DC20ChatMessage.create({
    speaker: DC20ChatMessage.getSpeaker({ actor: actor }),
    sound: CONFIG.sounds.notification,
    system: system,
    whisper: gmOnly ? DC20ChatMessage.getWhisperRecipients("GM") : []
  });
}

/**
 * Returns ture if useCondition is fulfilled.
 * 
 * '&&' - AND
 * '||' - OR
 * 
 * Use Condition is built with:
 * - pathToValue - path to value that should be validated
 * - equal sign "="
 * - value - fulfilling condition value either a string or and array of strings
 * 
 * Examples of useConditions:
 * - system.weaponStyle=["axe","sword"];system.weaponType="melee" -> item must be both melee type and axe or sword style
 * - system.name="Endbreaker" -> item must have a name of "Endbreaker"
 * - system.weaponStyle=["axe","sword"];system.weaponType="melee"|system.weaponStyle=["bow"];system.weaponType="ranged" -> item must be either (both melee type and axe or sword style) OR (ranged type and bow)
 */
function itemMeetsUseConditions(useCondition, item) {
  if (!useCondition) return false;
  if (useCondition === "true") return true;
  const OR = useCondition.split('||');
  for (const orConditions of OR) {
    const AND = orConditions.split('&&');
    if(_checkAND(AND, item)) return true;
  }
  return false;
}

function _checkAND(combinations, item) {
  for (const combination of combinations) {
    const pathValue = combination.trim().split('=');
    const value = getValueFromPath(item, pathValue[0]);
    if (value === undefined || value === "") return false;
    try {
      const conditionMet = eval(pathValue[1]).includes(value);
      if (!conditionMet) return false;
    } catch (e) {
      return false;
    }
  }  return true;
}

function companionShare(actor, keyToCheck) {
  if (actor.type !== "companion") return false;
  if (!actor.companionOwner) return false;
  return getValueFromPath(actor, `system.shareWithCompanionOwner.${keyToCheck}`);
}

//=========================================
//               ROLL LEVEL               =
//=========================================
async function advForApChange(object, which) {
  let adv = object.flags.dc20rpg.rollMenu.adv;
  let apCost = object.flags.dc20rpg.rollMenu.apCost;

  if (which === 1) {  // Add
    if (adv >= 9) return;
    apCost = apCost + 1;
    adv = adv + 1;
  }
  if (which === 3) {  // Subtract
    if (apCost === 0) return;
    apCost = apCost - 1;
    adv = Math.max(adv - 1, 0);
  }
  await object.update({
    ['flags.dc20rpg.rollMenu.apCost']: apCost,
    ['flags.dc20rpg.rollMenu.adv']: adv
  });
}

async function advForGritChange(object, which) {
  let adv = object.flags.dc20rpg.rollMenu.adv;
  let gritCost = object.flags.dc20rpg.rollMenu.gritCost;

  if (which === 1) {  // Add
    if (adv >= 9) return;
    gritCost = gritCost + 1;
    adv = adv + 1;
  }
  if (which === 3) {  // Subtract
    if (gritCost === 0) return;
    gritCost = gritCost - 1;
    adv = Math.max(adv - 1, 0);
  }
  await object.update({
    ['flags.dc20rpg.rollMenu.gritCost']: gritCost,
    ['flags.dc20rpg.rollMenu.adv']: adv
  });
}

let toRemove = [];
async function runItemRollLevelCheck(item, actor) {
  toRemove = [];
  let [actorRollLevel, actorGenesis, actorCrit, actorFail] = [{adv: 0, dis: 0}, []];
  let [targetRollLevel, targetGenesis, targetCrit, targetFail, targetFlanked, targetTqCover, targetHalfCover] = [{adv: 0, dis: 0}, []];

  const actionType = item.system.actionType;
  const specificCheckOptions = {
    range: item.system.range,
    properties: item.system.properties,
    allEnhancements: item.allEnhancements
  };
  let checkKey = "";
  switch (actionType) {
    case "attack":
      const attackFormula = item.system.attackFormula;
      const oldRange = attackFormula.rangeType;
      attackFormula.rangeType = item.flags.dc20rpg.rollMenu?.rangeType || oldRange;
      attackFormula.range = item.system.range.normal;
      checkKey = attackFormula.checkType.substr(0, 3);
      [actorRollLevel, actorGenesis, actorCrit, actorFail] = await _getAttackRollLevel(attackFormula, actor, "onYou", "You");
      [targetRollLevel, targetGenesis, targetCrit, targetFail, targetFlanked, targetTqCover, targetHalfCover] = await _runCheckAgainstTargets("attack", attackFormula, actor, false, specificCheckOptions);
      _runCloseQuartersCheck(attackFormula, actor, actorRollLevel, actorGenesis);
      attackFormula.rangeType = oldRange;
      break;

    case "check":
      const check = item.system.check;
      const respectSizeRules = check.respectSizeRules;
      checkKey = check.checkKey;
      check.type = "skillCheck";
      [actorRollLevel, actorGenesis, actorCrit, actorFail] = await _getCheckRollLevel(check, actor, "onYou", "You");
      [targetRollLevel, targetGenesis, targetCrit, targetFail] = await _runCheckAgainstTargets("check", check, actor, respectSizeRules, specificCheckOptions);
      break;
  }
  const [mcpRollLevel, mcpGenesis] = _respectMultipleCheckPenalty(actor, checkKey, item.flags.dc20rpg.rollMenu);

  const rollLevel = {
    adv: (actorRollLevel.adv + targetRollLevel.adv + mcpRollLevel.adv),
    dis: (actorRollLevel.dis + targetRollLevel.dis + mcpRollLevel.dis)
  };
  const genesis = [...actorGenesis, ...targetGenesis, ...mcpGenesis];
  const autoCrit = {value: actorCrit || targetCrit}; // We wrap it like that so autoCrit 
  const autoFail = {value: actorFail || targetFail}; // and autoFail can be edited by the item macro
  _updateWithRollLevelFormEnhancements(item, rollLevel, genesis);
  await runTemporaryItemMacro(item, "rollLevelCheck", actor, {rollLevel: rollLevel, genesis: genesis, autoCrit: autoCrit, autoFail: autoFail});
  if (toRemove.length > 0) await actor.update({["flags.dc20rpg.effectsToRemoveAfterRoll"]: toRemove});
  return await _updateRollMenuAndReturnGenesis(rollLevel, genesis, autoCrit.value, autoFail.value, item, targetFlanked, targetTqCover, targetHalfCover);
}

async function runSheetRollLevelCheck(details, actor) {
  toRemove = [];
  const [actorRollLevel, actorGenesis, actorCrit, actorFail] = await _getCheckRollLevel(details, actor, "onYou", "You");
  const [targetRollLevel, targetGenesis, targetCrit, targetFail] = await _runCheckAgainstTargets("check", details, actor);
  const [statusRollLevel, statusGenesis, statusCrit] = _getRollLevelAgainsStatuses(actor, details.statuses);
  const [mcpRollLevel, mcpGenesis] = _respectMultipleCheckPenalty(actor, details.checkKey, actor.flags.dc20rpg.rollMenu);

  const rollLevel = {
    adv: (actorRollLevel.adv + targetRollLevel.adv + statusRollLevel.adv + mcpRollLevel.adv),
    dis: (actorRollLevel.dis + targetRollLevel.dis + statusRollLevel.dis + mcpRollLevel.dis)
  };
  const genesis = [...actorGenesis, ...targetGenesis, ...statusGenesis, ...mcpGenesis];
  const autoCrit = actorCrit || targetCrit || statusCrit;
  const autoFail = actorFail || targetFail;
  if (toRemove.length > 0) await actor.update({["flags.dc20rpg.effectsToRemoveAfterRoll"]: toRemove});
  return await _updateRollMenuAndReturnGenesis(rollLevel, genesis, autoCrit, autoFail, actor);
}

async function _getAttackRollLevel(attackFormula, actor, subKey, sourceName, actorAskingForCheck) {
  const rollLevelPath = _getAttackPath(attackFormula.checkType, attackFormula.rangeType);

  if (rollLevelPath) {
    const path = `system.rollLevel.${subKey}.${rollLevelPath}`;
    return await _getRollLevel(actor, path, sourceName, {actorAskingForCheck: actorAskingForCheck});
  }
  return [{adv: 0, dis: 0}, []];
}

async function _getCheckRollLevel(check, actor, subKey, sourceName, actorAskingForCheck, respectSizeRules) {
  let rollLevelPath = "";
  const validationData = {actorAskingForCheck: actorAskingForCheck};
  let [specificSkillRollLevel, specificSkillGenesis, specificSkillCrit, specificSkillFail] = [{adv: 0, dis: 0}, []];
  let [checkRollLevel, checkGenesis, checkCrit, checkFail] = [{adv: 0, dis: 0}, []];
  let [initiativeRollLevel, initiativeGenesis, initiativeCrit, initiativeFail] = [{adv: 0, dis: 0}, []];

  switch (check.type) {
    case "initiative": rollLevelPath = "initiative"; break;
    case "deathSave": rollLevelPath = "deathSave"; break;
    case "save": rollLevelPath = _getSavePath(check.checkKey, actor, actorAskingForCheck); break;
    case "lang": rollLevelPath = _getLangPath(actor, actorAskingForCheck); break;
    case "attributeCheck": case "attackCheck": case "spellCheck":
      rollLevelPath = _getCheckPath(check.checkKey, actor, null, actorAskingForCheck); break;
    case "skillCheck":
      let category = "";
      if (actor.system.skills[check.checkKey]) category = "skills";
      if (actor.type === "character" && actor.system.tradeSkills[check.checkKey]) category = "tradeSkills";
      rollLevelPath = _getCheckPath(check.checkKey, actor, category, actorAskingForCheck);
      
      // Run check for specific skill not just attribute
      const specificSkillPath = `system.rollLevel.${subKey}.${category}`;
      [specificSkillRollLevel, specificSkillGenesis, specificSkillCrit, specificSkillFail] = await _getRollLevel(actor, specificSkillPath, sourceName, {specificSkill: check.checkKey, ...validationData});
  }

  // Run check for attribute
  if (rollLevelPath) {
    const path = `system.rollLevel.${subKey}.${rollLevelPath}`;
    [checkRollLevel, checkGenesis, checkCrit, checkFail] = await _getRollLevel(actor, path, sourceName, validationData);
  }
  const rollLevel = {
    adv: (checkRollLevel.adv + specificSkillRollLevel.adv + initiativeRollLevel.adv),
    dis: (checkRollLevel.dis + specificSkillRollLevel.dis + initiativeRollLevel.dis)
  };
  const genesis = [...checkGenesis, ...specificSkillGenesis, ...initiativeGenesis];
  let autoCrit = checkCrit || specificSkillCrit || initiativeCrit;
  let autoFail = checkFail || specificSkillFail || initiativeFail;

  // Run check for size rules
  if (respectSizeRules) {
    const contestorSize = actorAskingForCheck.system.size.size;
    const targetSize = actor.system.size.size;
    const sizeDif = _sizeDifCheck(contestorSize, targetSize);
    if (sizeDif >= 1) {
      rollLevel.adv++;
      genesis.push({type: "adv", sourceName: sourceName, label: "You are at least 1 size larger", value: 1});
    }
    if (sizeDif === -1) {
      rollLevel.dis++;
      genesis.push({type: "dis", sourceName: sourceName, label: "You are 1 size smaller", value: 1});
    }
    if (sizeDif < -1) {
      autoFail = true;
      genesis.push({autoFail: true, sourceName: sourceName, label: "You are more than 1 size smaller"});
    }
  } 
  return [rollLevel, genesis, autoCrit, autoFail];
}

async function _getRollLevel(actor, path, sourceName, validationData) {
  const levelsToUpdate = {adv: 0, dis: 0};
  const genesis = [];
  let autoCrit = false;
  let autoFail = false;

  const rollLevel = getValueFromPath(actor, path);
  if (!rollLevel) return [levelsToUpdate, genesis];

  const parsed = [];
  for(const json of rollLevel) {
    try {
      const obj = JSON.parse(`{${json}}`);
      parsed.push(obj);
    } catch (e) {
      console.warn(`Cannot parse roll level modification json {${json}} with error: ${e}`);
    }
  }

  for (const modification of parsed) {
    if (await _shouldApply(modification, actor, validationData)) {
      levelsToUpdate[modification.type] += modification.value;
      if (modification.autoCrit) autoCrit = true;
      if (modification.autoFail) autoFail = true;
      if (modification.afterRoll) toRemove.push({
        actorId: actor._id, 
        tokenId: actor.token?.id,
        effectId: modification.effectId, 
        afterRoll: modification.afterRoll
      });
      genesis.push({
        type: modification.type,
        sourceName: sourceName,
        label: modification.label,
        value: modification.value,
        autoCrit: autoCrit,
        autoFail: autoFail
      });
    }
  }
  return [levelsToUpdate, genesis, autoCrit, autoFail];
}

async function _shouldApply(modification, target, validationData) {
  if (_runValidationDataCheck(modification, validationData)) {
    if (modification.confirmation) {
      return getSimplePopup("confirm", {header: `Should "${modification.label}" be applied for an Actor named "${target.name}"?`})
    }
    else return true;
  }
  return false;
}

function _runValidationDataCheck(modification, validationData) {
  if (!validationData) return true; // Nothing to validate
  return _validateActorAskingForCheck(modification, validationData.actorAskingForCheck) 
        && _validateSpecificSkillKey(modification, validationData.specificSkill);
}

function _validateActorAskingForCheck(modification, actorAskingForCheck) {
  if (!actorAskingForCheck) return true;
  if (!modification.applyOnlyForId) return true;
  return modification.applyOnlyForId === actorAskingForCheck.id;
}

function _validateSpecificSkillKey(modification, specificSkill) {
  if (!specificSkill) return true;
  if (!modification.skill) return true;
  return specificSkill === modification.skill;
}

function _getRollLevelAgainsStatuses(actor, statuses) {
  if (!statuses) return [{adv: 0,dis: 0}, []];
  const levelPerStatus = [];
  const genesisPerStatus = [];
  const autoCritPerStatus = [];
  const autoFailPerStatus = [];

  const statusLevel = actor.system.statusResistances;
  statuses.forEach(statusId => {
    let genesis = [];
    let rollLevel = null;

    let autoCrit =  statusLevel[statusId]?.immunity;
    const resistance = statusLevel[statusId]?.resistance || 0;
    const vulnerability = statusLevel[statusId]?.vulnerability || 0;
    let saveLevel = resistance - vulnerability;
    autoCritPerStatus.push(autoCrit);
    autoFailPerStatus.push(false);
    if (autoCrit) {
      rollLevel = {
        adv: 0,
        dis: 0,
        autoCrit: autoCrit
      };
      const statusLabel = getLabelFromKey(statusId, CONFIG.DC20RPG.DROPDOWN_DATA.statusResistances);
      genesis.push({
        sourceName: "You",
        label: `Immune vs ${statusLabel}`,
        autoCrit: true
      });
    }
    if (saveLevel > 0) {
      if (rollLevel) rollLevel.adv = saveLevel;
      else {
        rollLevel = {
          adv: saveLevel,
          dis: 0
        };
      }
      const statusLabel = getLabelFromKey(statusId, CONFIG.DC20RPG.DROPDOWN_DATA.statusResistances);
      genesis.push({
        type: "adv",
        sourceName: "You",
        label: `Roll vs ${statusLabel}`,
        value: saveLevel
      });
    }
    if (saveLevel < 0) {
      if (rollLevel) rollLevel.dis = Math.abs(saveLevel);
      else {
        rollLevel = {
          adv: 0,
          dis: Math.abs(saveLevel)
        };
      }
      const statusLabel = getLabelFromKey(statusId, CONFIG.DC20RPG.DROPDOWN_DATA.statusResistances);
      genesis.push({
        type: "dis",
        sourceName: "You",
        label: `Roll vs ${statusLabel}`,
        value: Math.abs(saveLevel)
      });
    }
    
    if (rollLevel) {
      levelPerStatus.push(rollLevel);
      genesisPerStatus.push(genesis);
    }
  });
  return _findRollClosestToZeroAndAutoOutcome(levelPerStatus, genesisPerStatus, autoCritPerStatus, autoFailPerStatus, [], [], []);
}

async function _updateRollMenuAndReturnGenesis(levelsToUpdate, genesis, autoCrit, autoFail, owner, flanked, tqCover, halfCover) {
  // Change genesis to text format
  let genesisText = [];
  let unequalRollLevel = false; 
  let ignoredAutoOutcome = false;
  let ignoredFlankOutcome = false;
  let ignoredTqCoverOutcome = false;
  let ignoredHalfCoverOutcome = false;
  genesis.forEach(gen => {
    if (gen.textOnly) genesisText.push(gen.text);
    else {
      if (gen.value > 0) {
        const manualAction = gen.manualAction === "rollLevel" ? game.i18n.localize("dc20rpg.sheet.rollMenu.manualAction") : "";
        const typeLabel = game.i18n.localize(`dc20rpg.sheet.rollMenu.${gen.type}`);
        genesisText.push(`${manualAction}${typeLabel}[${gen.value}] -> (${gen.sourceName}) from: ${gen.label}`);
        if (manualAction) unequalRollLevel = true;
      }
      if (gen.autoCrit) {
        const manualAction = gen.manualAction === "autoCrit" ? game.i18n.localize("dc20rpg.sheet.rollMenu.manualAction") : "";
        const typeLabel = game.i18n.localize("dc20rpg.sheet.rollMenu.crit");
        genesisText.push(`${manualAction}${typeLabel} -> (${gen.sourceName}) from: ${gen.label}`);
        if (manualAction) ignoredAutoOutcome = true;
      }
      if (gen.autoFail) {
        const manualAction = gen.manualAction === "autoFail" ? game.i18n.localize("dc20rpg.sheet.rollMenu.manualAction") : "";
        const typeLabel = game.i18n.localize("dc20rpg.sheet.rollMenu.fail");
        genesisText.push(`${manualAction}${typeLabel} -> (${gen.sourceName}) from: ${gen.label}`);
        if (manualAction) ignoredAutoOutcome = true;
      }
      if (gen.isFlanked) {
        const manualAction = gen.manualAction === "isFlanked" ? game.i18n.localize("dc20rpg.sheet.rollMenu.manualAction") : "";
        const typeLabel = game.i18n.localize("dc20rpg.sheet.rollMenu.isFlanked");
        genesisText.push(`${manualAction}${typeLabel} -> (${gen.sourceName}) from: ${gen.label}`);
        if (manualAction) ignoredFlankOutcome = true;
      }
      if (gen.tqCover) {
        const manualAction = gen.manualAction === "tqCover" ? game.i18n.localize("dc20rpg.sheet.rollMenu.manualAction") : "";
        const typeLabel = game.i18n.localize("dc20rpg.sheet.rollMenu.tqCoverLabel");
        genesisText.push(`${manualAction}${typeLabel} -> (${gen.sourceName}) from: ${gen.label}`);
        if (manualAction) ignoredTqCoverOutcome = true;
      }
      if (gen.halfCover) {
        const manualAction = gen.manualAction === "halfCover" ? game.i18n.localize("dc20rpg.sheet.rollMenu.manualAction") : "";
        const typeLabel = game.i18n.localize("dc20rpg.sheet.rollMenu.halfCoverLabel");
        genesisText.push(`${manualAction}${typeLabel} -> (${gen.sourceName}) from: ${gen.label}`);
        if (manualAction) ignoredHalfCoverOutcome = true;
      }
    }
  });

  if (unequalRollLevel) {
    genesisText.push(game.i18n.localize("dc20rpg.sheet.rollMenu.unequalRollLevel"));
  }
  if (ignoredAutoOutcome) {
    genesisText.push(game.i18n.localize("dc20rpg.sheet.rollMenu.ignoredAutoOutcome"));
  }
  if (ignoredFlankOutcome) {
    genesisText.push(game.i18n.localize("dc20rpg.sheet.rollMenu.ignoredFlankOutcome"));
  }
  if (ignoredTqCoverOutcome) {
    genesisText.push(game.i18n.localize("dc20rpg.sheet.rollMenu.ignoredTqCoverOutcome"));
  }
  if (ignoredHalfCoverOutcome) {
    genesisText.push(game.i18n.localize("dc20rpg.sheet.rollMenu.ignoredHalfCoverOutcome"));
  }
  if (unequalRollLevel || ignoredAutoOutcome || ignoredFlankOutcome || autoFail || ignoredTqCoverOutcome || ignoredHalfCoverOutcome) {
    genesisText.push("FORCE_DISPLAY");
  }

  // Check roll level from ap and grit for adv
  const apCost = owner.flags.dc20rpg.rollMenu.apCost;
  const gritCost = owner.flags.dc20rpg.rollMenu.gritCost;
  if (apCost > 0) levelsToUpdate.adv += apCost;
  if (gritCost > 0) levelsToUpdate.adv += gritCost;

  const updateData = {
    ["flags.dc20rpg.rollMenu"]: levelsToUpdate,
    ["flags.dc20rpg.rollMenu.autoCrit"]: autoCrit,
    ["flags.dc20rpg.rollMenu.autoFail"]: autoFail,
    ["flags.dc20rpg.rollMenu.flanks"]: flanked,
    ["flags.dc20rpg.rollMenu.halfCover"]: halfCover,
    ["flags.dc20rpg.rollMenu.tqCover"]: tqCover,
  };
  await owner.update(updateData);

  if (genesisText.length === 0) return ["No modifications found"];
  return genesisText;
}

async function _runCheckAgainstTargets(rollType, check, actorAskingForCheck, respectSizeRules, specificCheckOptions) {
  const levelPerToken = [];
  const genesisPerToken = [];
  const autoCritPerToken = [];
  const autoFailPerToken = [];
  const isFlankedPerToken = [];
  const tqCoverPerToken = [];
  const halfCoverPerToken = [];
  for (const token of game.user.targets) {
    const [rollLevel, genesis, autoCrit, autoFail] = rollType === "attack" 
                    ? await _getAttackRollLevel(check, token.actor, "againstYou", token.name, actorAskingForCheck)
                    : await _getCheckRollLevel(check, token.actor, "againstYou", token.name, actorAskingForCheck, respectSizeRules);

    const [specificRollLevel, specificGenesis, specificAutoCrit, specificAutoFail, isFlanked, tqCover, halfCover] = _runSpecificTargetChecks(check, token, actorAskingForCheck, specificCheckOptions);
    if (genesis) {
      rollLevel.adv += specificRollLevel.adv;
      rollLevel.dis += specificRollLevel.dis;
      levelPerToken.push(rollLevel);
      genesisPerToken.push([...genesis, ...specificGenesis]);
      autoCritPerToken.push(autoCrit || specificAutoCrit);
      autoFailPerToken.push(autoFail || specificAutoFail);
      isFlankedPerToken.push(isFlanked);
      tqCoverPerToken.push(tqCover);
      halfCoverPerToken.push(halfCover);
    }
  }

  return _findRollClosestToZeroAndAutoOutcome(levelPerToken, genesisPerToken, autoCritPerToken, autoFailPerToken, isFlankedPerToken, tqCoverPerToken, halfCoverPerToken);
}

function _runSpecificTargetChecks(attackFormula, token, actorAskingForCheck, specifics) {
  const rollLevel = {adv: 0, dis: 0};
  const genesis = [];
  let autoCrit = false;
  let autoFail = false;
  let isFlanked = false;
  let tqCover = false;
  let halfCover = false;

  // Flanking
  if (attackFormula.rangeType === "melee" && attackFormula.checkType === "attack") {
    isFlanked = token.isFlanked;
    if (isFlanked) {
      genesis.push({
        isFlanked: isFlanked,
        sourceName: token.name,
        label: "Is Flanked",
      });
    }
  }

  // Cover
  const cover = token.actor.system.globalModifier.provide;
  if (cover.tqCover) {
    tqCover = cover.tqCover;
    genesis.push({
      tqCover: cover.tqCover,
      sourceName: token.name,
      label: "Three-Quarter Cover",
    });
  }
  else if (cover.halfCover) {
    halfCover = cover.halfCover;
    genesis.push({
      halfCover: cover.halfCover,
      sourceName: token.name,
      label: "Half Cover",
    });
  }

  const tokenAskingForCheck = getTokenForActor(actorAskingForCheck);
  if (!tokenAskingForCheck) return [rollLevel, genesis, autoCrit, autoFail, isFlanked, tqCover, halfCover];

  // Unwieldy Property
  const enablePositionCheck = game.settings.get("dc20rpg", "enablePositionCheck");
  if (enablePositionCheck && specifics?.properties?.unwieldy?.active && tokenAskingForCheck.neighbours.has(token.id)) {
    rollLevel.dis++;
    genesis.push({
      type: "dis",
      sourceName: token.name,
      label: "Unwieldy Property",
      value: 1,
    });
  }

  // Item Range Rules
  autoFail = _respectRangeRules(rollLevel, genesis, tokenAskingForCheck, token, attackFormula, specifics);
  return [rollLevel, genesis, autoCrit, autoFail, isFlanked, tqCover, halfCover];
}

function _findRollClosestToZeroAndAutoOutcome(levelPerOption, genesisPerOption, autoCritPerToken, autoFailPerToken, isFlankedPerToken, tqCoverPerToken, halfCoverPerToken) {
  if (levelPerOption.length === 0) return [{adv: 0,dis: 0}, []];

  // We need to find roll level that is closest to 0 so players can manualy change that later
  let lowestLevel = levelPerOption[0];
  for(let i = 1; i < levelPerOption.length; i++) {
    const currentLow = Math.abs(lowestLevel.adv - lowestLevel.dis);
    const newLow = Math.abs(levelPerOption[i].adv - levelPerOption[i].dis);

    if (newLow < currentLow) {
      lowestLevel = levelPerOption[i];
    }
  }

  // Auto crit/fail and flank should be applied only if every target is affected
  const applyAutoCrit = autoCritPerToken.every(x => x === true);
  const applyAutoFail = autoFailPerToken.every(x => x === true);
  const applyFlanked = isFlankedPerToken.every(x => x === true);
  const applyTqCover = tqCoverPerToken.every(x => x === true);
  const applyHalfCover = halfCoverPerToken.every(x => x === true);

  // Now we need to mark which targets requiere some manual modifications to be done, 
  // because those have higher levels of advantages/disadvantages
  genesisPerOption.forEach(genesis => {
    let counter = {
      adv: 0,
      dis: 0,
    };
    if (genesis[0]) {
      if (genesis[0].type === "adv") counter.adv = genesis[0].value;
      if (genesis[0].type === "dis") counter.dis = genesis[0].value;
    } 

    for(let mod of genesis) {
      // Roll Level application
      if (lowestLevel[mod.type] < counter[mod.type]) mod.manualAction = "rollLevel";
      else counter[mod.type] += mod.value;

      // Auto crit/fail application
      if (mod.autoCrit && !applyAutoCrit) mod.manualAction = "autoCrit";
      if (mod.autoFail && !applyAutoFail) mod.manualAction = "autoFail";
      if (mod.isFlanked && !applyFlanked) mod.manualAction = "isFlanked";
      if (mod.tqCover && !applyTqCover) mod.manualAction = "tqCover";
      if (mod.halfCover && !applyHalfCover) mod.manualAction = "halfCover";
    }
  });

  return [lowestLevel, genesisPerOption.flat(), applyAutoCrit, applyAutoFail, applyFlanked, applyTqCover, applyHalfCover];
}

function _getAttackPath(checkType, rangeType) {
  if (checkType === "attack") return `martial.${rangeType}`;
  if (checkType === "spell") return `spell.${rangeType}`;
  return "";
}

function _getCheckPath(checkKey, actor, category, actorAskingForCheck) {
  // When we send actor asking for check it means this is "againstCheck" so we want to 
  // collect skill modifiers from the actor that makes that roll not the one the roll 
  // is being made against 
  const actorToAnalyze = actorAskingForCheck || actor;
  if (["mig", "agi", "cha", "int"].includes(checkKey)) return `checks.${checkKey}`;
  // if (checkKey === "prime") return `checks.${actorToAnalyze.system.details.primeAttrKey}`;
  if (checkKey === "att") return `checks.att`;
  if (checkKey === "spe") return `checks.spe`;
  if (checkKey === "mar") {
    const acr = actorToAnalyze.system.skills.acr;
    const ath = actorToAnalyze.system.skills.ath;
    if (acr && ath) {
      checkKey = acr.modifier >= ath.modifier ? "acr" : "ath";
      category = "skills";
    }
  }
  if (!category) return;

  let attrKey = actorToAnalyze.system[category][checkKey].baseAttribute;
  // if (attrKey === "prime") attrKey = actorToAnalyze.system.details.primeAttrKey;
  return `checks.${attrKey}`;
}

function _getSavePath(saveKey, actor, actorAskingForCheck) {
  // Explained in _getCheckPath method
  const actorToAnalyze = actorAskingForCheck || actor;
  if (saveKey === "phy") {
    const migSave = actorToAnalyze.system.attributes.mig.save;
    const agiSave = actorToAnalyze.system.attributes.agi.save;
    saveKey = migSave >= agiSave ? "mig" : "agi";
  }

  if (saveKey === "men") {
    const intSave = actorToAnalyze.system.attributes.int.save;
    const chaSave = actorToAnalyze.system.attributes.cha.save;
    saveKey = intSave >= chaSave ? "int" : "cha";
  }

  // if (saveKey === "prime") saveKey = actorToAnalyze.system.details.primeAttrKey;
  return `saves.${saveKey}`;
}

function _getLangPath(actor, actorAskingForCheck) {
  // Explained in _getCheckPath method
  const actorToAnalyze = actorAskingForCheck || actor;
  const intSave = actorToAnalyze.system.attributes.int.check;
  const chaSave = actorToAnalyze.system.attributes.cha.check;
  const key = intSave >= chaSave ? "int" : "cha";
  return `checks.${key}`;
}

function _sizeDifCheck(contestor, target) {
  const contestorSize = _sizeNumericValue(contestor);
  const targetSize = _sizeNumericValue(target);
  return contestorSize - targetSize
}

function _sizeNumericValue(size) {
  switch (size) {
    case "tiny": return 0;
    case "small": return 1;
    case "medium": return 2;
    case "mediumLarge": case "large": return 3;
    case "huge": return 4;
    case "gargantuan": return 5;
    case "colossal": return 6;
    case "titanic": return 7;
  }
}

//======================================
//=       MULTIPLE CHECK PENALTY       =
//======================================
function applyMultipleCheckPenalty(actor, distinction, rollMenu) {
  if (!distinction) return;
  if (rollMenu.ignoreMCP) return;
  let actorToUpdate = actor;
  // Companion might share MCP with owner
  if (companionShare(actor, "mcp")) actorToUpdate = actor.companionOwner; 

  // Get active started combat
  const activeCombat = game.combats.active;
  if (!activeCombat || !activeCombat.started) return;

  // If roll was made by actor on his turn apply multiple check penalty
  const combatantId = activeCombat.current.combatantId;
  const combatant = activeCombat.combatants.get(combatantId);
  if (combatant?.actorId === actorToUpdate.id) {
    const mcp = actorToUpdate.system.mcp;
    mcp.push(distinction);
    actorToUpdate.update({["system.mcp"]: mcp});
  }
}

function applyMultipleHelpPenalty(actor, maxDice) {
  let actorToUpdate = actor;
  // Companion might share MCP with owner
  if (companionShare(actor, "mcp")) actorToUpdate = actor.companionOwner; 

  const mcp = actorToUpdate.system.mcp;
  const penalty = mcp.filter(mhp => mhp === "help");
  mcp.push("help");
  actorToUpdate.update({["system.mcp"]: mcp});
  return maxDice - (2 * penalty.length);
}

function clearMultipleCheckPenalty(actor) {
  if (actor.flags.dc20rpg.actionHeld?.isHeld) {
    let mcp = actor.system.mcp;
    if (companionShare(actor, "mcp")) mcp = actor.companionOwner.system.mcp;
    actor.update({["flags.dc20rpg.actionHeld.mcp"]: mcp});
  }
  actor.update({["system.mcp"]: []});
}

function _respectMultipleCheckPenalty(actor, checkKey, rollMenu) {
  if (rollMenu.ignoreMCP) return [{adv: 0, dis: 0}, []];
  let mcp = actor.system.mcp;

  // Companion might share MCP with owner
  if (companionShare(actor, "mcp")) mcp = actor.companionOwner.system.mcp; 

  // If action was held we want to use MCP from last round
  const actionHeld = actor.flags.dc20rpg.actionHeld;
  if (actionHeld?.rollsHeldAction && actionHeld.mcp !== null) {
    mcp = actionHeld.mcp;
  }

  let dis = 0;
  let genesis = [];
  mcp.forEach(check => {
    if (check === checkKey) dis++;
  });
  if (dis > 0) {
    genesis.push({
      type: "dis",
      sourceName: "You",
      label: "Multiple Check Penalty",
      value: dis
    });
  }
  return [{adv: 0, dis: dis}, genesis];
}

//======================================
//=     SPECIAL ROLL LEVEL CHECKS      =
//======================================
function _updateWithRollLevelFormEnhancements(item, rollLevel, genesis) {
  item.allEnhancements.values().forEach(enh => {
    if (enh.number > 0) {
      if (enh.modifications.rollLevelChange && enh.modifications.rollLevel?.value) {
        const type = enh.modifications.rollLevel.type;
        const value = enh.modifications.rollLevel.value;
        rollLevel[type] += (value * enh.number);
        genesis.push({
          type: type,
          sourceName: "You",
          label: enh.name,
          value: (value * enh.number),
        });
      }
    }
  });
}

function _runCloseQuartersCheck(attackFormula, actor, rollLevel, genesis) {
  if (!game.settings.get("dc20rpg", "enablePositionCheck")) return;
  if (actor.system.globalModifier.ignore.closeQuarters) return;
  if (!attackFormula.range || attackFormula.rangeType !== "ranged") return;
  
  // Close Quarters - Ranged Attacks are done with disadvantage if there is someone within 1 Space
  const actorToken = getTokenForActor(actor);
  if (!actorToken) return;
  if (actorToken.enemyNeighbours.size > 0) {
    let closeQuarters = false;
    actorToken.enemyNeighbours.values().forEach(token => {if (!token.actor.hasAnyStatus("incapacitated", "dead")) closeQuarters = true;});

    if (closeQuarters) {
      rollLevel.dis++;
      genesis.push({
        type: "dis",
        sourceName: "You",
        label: "Close Quarters - Enemy next to you",
        value: 1,
      });
    }
  }
}

function _respectRangeRules(rollLevel, genesis, actorToken, targetToken, attackFormula, specifics) {
  if (!game.settings.get("dc20rpg", "enableRangeCheck")) return false;
  if (!specifics) return false;

  const range = specifics?.range;
  const properties = specifics?.properties; 
  let meleeRange = range.melee || 1;
  let normalRange = properties ? 1 : null;
  let maxRange = properties ? 5 : null; // If has properties it means it is a weapon

  meleeRange += _bonusRangeFromEnhancements(specifics?.allEnhancements, "melee") + _bonusFromGlobalModifier(actorToken, "melee");
  if (properties?.reach?.active) meleeRange += properties.reach.value;
  if (range.normal) normalRange = range.normal + _bonusRangeFromEnhancements(specifics?.allEnhancements, "normal") + _bonusFromGlobalModifier(actorToken, "normal");
  if (range.max) maxRange = range.max + _bonusRangeFromEnhancements(specifics?.allEnhancements, "max") + _bonusFromGlobalModifier(actorToken, "max");

  if (attackFormula.rangeType === "melee") {
    if (!actorToken.isTokenInRange(targetToken, meleeRange)) return _outOfRange(genesis, targetToken);
  }

  if (attackFormula.rangeType === "ranged") {
    if (normalRange && maxRange && normalRange < maxRange) {
      if (!actorToken.isTokenInRange(targetToken, maxRange)) return _outOfRange(genesis, targetToken);
      if (!actorToken.isTokenInRange(targetToken, normalRange)) return _longRange(rollLevel, genesis, targetToken, actorToken.actor);
    }
    else if (normalRange) {
      if (!actorToken.isTokenInRange(targetToken, normalRange)) return _outOfRange(genesis, targetToken);
    }
  }
  return false;
}

function _bonusRangeFromEnhancements(enhancements, rangeKey) {
  if (!enhancements) return 0;
  let bonus = 0;
  enhancements.values().forEach(enh => {
    if (enh.number > 0) {
      const bonusRange = enh.modifications.bonusRange?.[rangeKey];
      if (enh.modifications.addsRange && bonusRange) {
        bonus += (bonusRange || 0) * enh.number;
      }
    }
  });
  return bonus;
}

function _bonusFromGlobalModifier(actorToken, rangeType) {
  const actor = actorToken.actor; 
  if (!actor) return 0;
  return actor.system.globalModifier.range[rangeType] || 0;
}

function _outOfRange(genesis, token) {
  genesis.push({
    autoFail: true,
    sourceName: token.name,
    label: "Out of Range",
  });
  return true;
}

function _longRange(rollLevel, genesis, token, actor) {
  if (actor.system.globalModifier.ignore.longRange) return false;
  rollLevel.dis++;
  genesis.push({
    type: "dis",
    sourceName: token.name,
    label: "Long Range",
    value: 1,
  });
  return false;
}

//==========================================
//             Roll From Sheet             =
//==========================================
async function rollFromSheet(actor, details) {
  return await _rollFromFormula(details.roll, details, actor);
}

/**
 * Creates new Roll instance from given formula. Returns result of that roll.
 * If roll was done with advantage or disadvantage only winning roll will be returned.
 * 
 * @param {String} formula      - Formula used to create roll.
 * @param {Object} details      - Object containing extra informations about that roll. Ex. type, description, label.
 * @param {DC20RpgActor} actor  - Actor which roll data will be used for creating that roll.
 * @param {Boolean} sendToChat  - If true, creates chat message showing rolls results.
 * @returns {Roll} Winning roll.
 */
async function _rollFromFormula(formula, details, actor, sendToChat) {
  const rollMenu = actor.flags.dc20rpg.rollMenu;
  const rollLevel = _determineRollLevel(rollMenu);
  const rollData = actor.getRollData();

  // 1. Subtract Cost
  if (details.costs) {
    for (const cost of details.costs) {
      if (canSubtractBasicResource(cost.key, actor, cost.value)) {
        subtractBasicResource(cost.key, actor, cost.value, "true");
      }
      else return;
    }
  }

  // 2. Pre Item Roll Events
  if (["attackCheck", "spellCheck", "attributeCheck", "skillCheck", "initiative"].includes(details.type)) await runEventsFor("rollCheck", actor);
  if (["save"].includes(details.type)) await runEventsFor("rollSave", actor);

  // 3. Prepare Core Roll Formula
  let d20roll = "d20";
  if (rollLevel !== 0) d20roll = `${Math.abs(rollLevel)+1}d20${rollLevel > 0 ? "kh" : "kl"}`;
  // If the formula contains d20 we want to replace it.
  if (formula.includes("d20")) formula = formula.replaceAll("d20", d20roll);

  const globalMod = _extractGlobalModStringForType(details.type, actor);
  const helpDices = _collectHelpDices(rollMenu);
  formula += " " + globalMod.value + helpDices;

  let source = details.type === "save" ? "Save Formula" : "Check Formula";
  if (globalMod.source !== "") source += ` + ${globalMod.source}`;
  if (helpDices !== "") source += ` + Help Dice`;

  // 4. Roll Formula
  const roll = _prepareCoreRoll(formula, rollData, details.label);
  await _evaluateCoreRollAndMarkCrit(roll, {rollLevel: rollLevel, rollMenu: rollMenu});
  roll.source = source;

  // 5. Send chat message
  {
    const label = details.label || `${actor.name} : Roll Result`;
    const rollTitle = details.rollTitle || label;
    const messageDetails = {
      label: label,
      image: actor.img,
      description: details.description,
      against: details.against,
      rollTitle: rollTitle,
      rollLevel: rollLevel
    };
    sendRollsToChat({core: roll}, actor, messageDetails, false);
  }

  // 6. Cleanup
  if (actor.inCombat && ["attributeCheck", "attackCheck", "spellCheck", "skillCheck"].includes(details.type)) {
    applyMultipleCheckPenalty(actor, details.checkKey, rollMenu);
  }
  _runCritAndCritFailEvents(roll, actor, rollMenu);
  if (!details.initiative) _respectNat1Rules(roll, actor, details.type, null, rollMenu);
  resetRollMenu(rollMenu, actor);
  _deleteEffectsMarkedForRemoval(actor);
  reenablePreTriggerEvents();

  // 7. Return Core Roll
  return roll;
}

//===================================
//          Roll From Item          =
//===================================
/**
 * Creates new Roll instances from given item formulas. Returns result of core formula.
 * If roll was done with advantage or disadvantage only winning roll will be returned.
 * 
 * @param {String} itemId       - The ID of the item from which the rolls will be created.
 * @param {DC20RpgActor} actor  - Actor which roll data will be used for creating those rolls.
 * @param {Boolean} sendToChat  - If true, creates chat message showing rolls results.
 * @returns {Roll} Winning roll.
 */
async function rollFromItem(itemId, actor, sendToChat=true) {
  if (!actor) return;
  const item = actor.items.get(itemId);
  if (!item) return;
  
  const rollMenu = item.flags.dc20rpg.rollMenu;

  // 1. Subtract Cost
  const costsSubracted = rollMenu.free ? true : await respectUsageCost(actor, item);
  if (!costsSubracted) {
    resetEnhancements(item, actor);
    resetRollMenu(rollMenu, item);
    return;
  }
  
  // 2. Pre Item Roll Events and macros
  await runTemporaryItemMacro(item, "preItemRoll", actor);
  await _runEnancementsMacro(item, "preItemRoll", actor);
  const actionType = item.system.actionType;
  if (actionType === "attack") await runEventsFor("attack", actor);
  if (actionType === "check") await runEventsFor("rollCheck", actor);
  await runEventsFor("rollItem", actor);

  // 3. Item Roll
  const rollLevel = _determineRollLevel(rollMenu);
  const rollData = await item.getRollData();
  const rolls = await _evaluateItemRolls(actionType, actor, item, rollData, rollLevel);
  if (actionType === "help") {
    let ignoreMHP = item.system.help?.ignoreMHP;
    if (!ignoreMHP) ignoreMHP = rollMenu.ignoreMCP;
    prepareHelpAction(actor, {ignoreMHP: ignoreMHP, subtract: item.system.help?.subtract, doNotExpire: item.system.help?.doNotExpire});
  }

  // 4. Post Item Roll
  await runTemporaryItemMacro(item, "postItemRoll", actor, {rolls: rolls});
  await _runEnancementsMacro(item, "postItemRoll", actor, {rolls: rolls});

  // 5. Send chat message
  if (sendToChat && !item.doNotSendToChat) {
    const messageDetails = _prepareMessageDetails(item, actor, actionType, rolls);

    if (!actionType) {
      sendDescriptionToChat(actor, messageDetails, item);
    }
    else if (actionType === "help") {
      messageDetails.rollTitle += " - Help Action";
      sendDescriptionToChat(actor, messageDetails, item);
    }
    else {
      messageDetails.rollLevel = rollLevel;
      sendRollsToChat(rolls, actor, messageDetails, true, item);
    }
  }

  // 6. Cleanup
  _finishRoll(actor, item, rollMenu, rolls.core);
  if (item.deleteAfter) item.delete();

  // 7. Return Core Roll
  return rolls.core;
}

//=======================================
//           EVALUATE ROLLS             =
//=======================================
async function _evaluateItemRolls(actionType, actor, item, rollData, rollLevel) {
  let coreRoll = null;
  let formulaRolls = [];

  const evalData = {
    rollData: rollData,
    rollLevel: rollLevel,
    helpDices: _collectHelpDices(item.flags.dc20rpg.rollMenu)
  };

  if (actionType === "attack") {
    coreRoll = await _evaluateAttackRoll(actor, item, evalData);
    evalData.attackCheckType = item.system.attackFormula.checkType;
    evalData.attackRangeType = item.system.attackFormula.rangeType;
  }
  if (actionType === "check") {
    coreRoll = await _evaluateCheckRoll(actor, item, evalData);
  }
  formulaRolls = await _evaluateFormulaRolls(item, actor, evalData);
  return {
    core: coreRoll,
    formula: formulaRolls
  }
}

async function _evaluateAttackRoll(actor, item, evalData) {
  evalData.rollMenu = item.flags.dc20rpg.rollMenu;
  const source = {value: "Attack Formula"};
  evalData.rollModifiers = _collectCoreRollModifiers(evalData.rollMenu, source, item.allEnhancements);
  evalData.critThreshold = item.system.attackFormula.critThreshold;
  const coreFormula = _prepareAttackFromula(actor, item.system.attackFormula, evalData, source);
  const label = getLabelFromKey(item.system.attackFormula.checkType, CONFIG.DC20RPG.DROPDOWN_DATA.attackTypes); 
  const coreRoll = _prepareCoreRoll(coreFormula, evalData.rollData, label);

  await _evaluateCoreRollAndMarkCrit(coreRoll, evalData);
  coreRoll.source = source.value;
  return coreRoll;
}

async function _evaluateCheckRoll(actor, item, evalData) {
  evalData.rollMenu = item.flags.dc20rpg.rollMenu;
  const source = {value: "Check Formula"};
  evalData.rollModifiers = _collectCoreRollModifiers(evalData.rollMenu, source, item.allEnhancements);
  const checkKey = item.checkKey;
  const coreFormula = _prepareCheckFormula(actor, checkKey, evalData, source);
  const label = getLabelFromKey(checkKey, CONFIG.DC20RPG.ROLL_KEYS.allChecks);
  const coreRoll = _prepareCoreRoll(coreFormula, evalData.rollData, label);

  await _evaluateCoreRollAndMarkCrit(coreRoll, evalData);
  coreRoll.source = source.value;
  return coreRoll;
}

async function _evaluateFormulaRolls(item, actor, evalData) {
  const formulaRolls = _prepareFormulaRolls(item, actor, evalData);
  for (let i = 0; i < formulaRolls.length; i++) {
    const roll = formulaRolls[i];
    await roll.clear.evaluate();
    await roll.modified.evaluate();

    // We want to evaluate each5 and fail formulas in advance
    if (roll.modified.each5Formula) {
      const each5Roll = await evaluateFormula(roll.modified.each5Formula, evalData.rollData, true);
      if (each5Roll) roll.modified.each5Roll = each5Roll;
    }
    if (roll.modified.failFormula) {
      const failRoll = await evaluateFormula(roll.modified.failFormula, evalData.rollData, true);
      if (failRoll) roll.modified.failRoll = failRoll;
    }
  }
  return formulaRolls;
}

async function _evaluateCoreRollAndMarkCrit(roll, evalData) {
  if (!roll) return;
  let rollLevel = evalData.rollLevel;
  const rollMenu = evalData.rollMenu;
  const critThreshold = evalData.critThreshold;

  const rollOptions = {
    maximize: false,
    minimize: false
  };

  // Apply Auto Roll Outcome 
  if (rollMenu.autoCrit && !rollMenu.autoFail) {
    rollOptions.maximize = true;
    roll.terms[0].modifiers = [];
    roll.terms[0].number = 1;
    rollLevel = 1;
    roll.label += " (Auto Crit)";
  }
  if (!rollMenu.autoCrit && rollMenu.autoFail) {
    rollOptions.minimize = true;
    roll.terms[0].modifiers = [];
    roll.terms[0].number = 1;
    rollLevel = 1;
    roll.label += " (Auto Fail)";
  }

  // Roll
  await roll.evaluate(rollOptions);
  roll.flatDice = roll.dice[0].total; // We will need that later

  // Determine crit of crit fail
  roll.crit = false;
  roll.fail = false;
  let critNo = 0;
  let failNo = 0;

  // Only d20 can crit
  roll.terms.forEach(term => {
    if (term.faces === 20) {
      const fail = 1;
      const crit = critThreshold || 20;

      term.results.forEach(result => {
        if (result.result >= crit) critNo++; 
        if (result.result === fail) failNo++; 
      });
    }
  });

  if (rollLevel >= 0 && critNo > 0) roll.crit = true;
  else if (rollLevel <= 0 && failNo > 0) roll.fail = true;
  else if (rollLevel <= 0 && critNo > Math.abs(rollLevel)) roll.crit = true;
  else if (rollLevel >= 0 && failNo > rollLevel) roll.fail = true;
}

function _collectHelpDices(rollMenu) {
  let helpDicesFormula = "";
  if (rollMenu.d8 !== 0) helpDicesFormula += `+ ${rollMenu.d8}d8`;
  if (rollMenu.d6 !== 0) helpDicesFormula += `+ ${rollMenu.d6}d6`;
  if (rollMenu.d4 !== 0) helpDicesFormula += `+ ${rollMenu.d4}d4`;
  return helpDicesFormula;
}

function _collectCoreRollModifiers(rollMenu, source, enhancements) {
  let formulaModifiers = "";
  if (rollMenu.versatile) {
    formulaModifiers = "+ 2";
    source.value += " + Versatile";
  }
  if (rollMenu.flanks) {
    formulaModifiers += "+ 2";
    source.value += " + Flanked";
  }
  if (rollMenu.halfCover) {
    formulaModifiers += "- 2";
    source.value += " - Half Cover";
  }
  if (rollMenu.tqCover) {
    formulaModifiers += "- 5";
    source.value += " - 3/4 Cover";
  }
  if (enhancements) {
    enhancements.values().forEach(enh => {
      const enhMod = enh.modifications;
      if (enhMod.modifiesCoreFormula && enhMod.coreFormulaModification) {
        for (let i = 0; i < enh.number; i++) {
          const modification = (enhMod.coreFormulaModification.includes("+") || enhMod.coreFormulaModification.includes("-")) ? enhMod.coreFormulaModification : ` + ${enhMod.coreFormulaModification}`;
          formulaModifiers += modification;
          source.value += ` + ${enh.name}`;
        }
      }    });
  }
  return formulaModifiers;
}

//=======================================
//            PREPARE ROLLS             =
//=======================================
function _prepareCoreRoll(coreFormula, rollData, label) {
  if (coreFormula) {
    const coreRoll = new Roll(coreFormula, rollData);
    coreRoll.coreFormula = true;
    coreRoll.label = label;
    return coreRoll;
  }
  return null;
}

function _prepareFormulaRolls(item, actor, evalData) {
  const rollData = evalData.rollData;
  const enhancements = item.allEnhancements;
  const formulas = _collectAllFormulasForAnItem(item, enhancements);

  // Check if damage type should be overriden
  let overridenDamage = "";
  if (enhancements) {
    enhancements.values().forEach(enh => {
      if (enh.number > 0) {
        const enhMod = enh.modifications;
        // Override Damage Type
        if (enhMod.overrideDamageType && enhMod.damageType) {
          overridenDamage = enhMod.damageType;
        }      }
    });
  }

  if (formulas) {
    const damageRolls = [];
    const healingRolls = [];
    const otherRolls = [];

    for (const [key, formula] of Object.entries(formulas)) {
      const clearRollFromula = formula.formula; // formula without any modifications
      const modified = _modifiedRollFormula(formula, actor, enhancements, evalData); // formula with all enhancements applied
      const roll = {
        clear: new Roll(clearRollFromula, rollData),
        modified: new Roll(modified.rollFormula, rollData)
      };
      const commonData = {
        id: key,
        coreFormula: false,
        label: "",
        category: formula.category,
        dontMerge: formula.dontMerge,
        overrideDefence: formula.overrideDefence
      };
      roll.clear.clear = true;
      roll.modified.clear = false;
      roll.clear.modifierSources = formula.enhName || "Base Value";
      roll.modified.modifierSources = modified.modifierSources;

      if (formula.each5) roll.modified.each5Formula = formula.each5Formula;
      if (formula.fail) roll.modified.failFormula = formula.failFormula;

      switch (formula.category) {
        case "damage":
          if (overridenDamage) formula.type = overridenDamage;
          let damageTypeLabel = getLabelFromKey(formula.type, CONFIG.DC20RPG.DROPDOWN_DATA.damageTypes);
          commonData.label = "Damage - " + damageTypeLabel;
          commonData.type = formula.type;
          commonData.typeLabel = damageTypeLabel;
          _fillCommonRollProperties(roll, commonData);
          damageRolls.push(roll);
          break;
        case "healing":
          let healingTypeLabel = getLabelFromKey(formula.type, CONFIG.DC20RPG.DROPDOWN_DATA.healingTypes);
          commonData.label = "Healing - " + healingTypeLabel;
          commonData.type = formula.type;
          commonData.typeLabel = healingTypeLabel;
          _fillCommonRollProperties(roll, commonData);
          healingRolls.push(roll);
          break;
        case "other":
          commonData.label = formula.label || "Other Roll";
          _fillCommonRollProperties(roll, commonData);
          // We want only modified rolls
          roll.clear = new Roll("0", rollData);
          otherRolls.push(roll);
          break;
      }
    }
    return [...damageRolls, ...healingRolls, ...otherRolls];
  }
  return [];
}

function _collectAllFormulasForAnItem(item, enhancements) {
  // Item formulas
  let formulas = item.system.formulas;

  // If item is a using weapon as part of an attack we collect those formulas
  const actor = item.actor;
  const useWeapon = item.system.usesWeapon;
  if (actor && useWeapon?.weaponAttack) {
    const weaponId = useWeapon.weaponId;
    const weapon = actor.items.get(weaponId);
    if (weapon) {
      const weaponFormulas = weapon.system.formulas;
      formulas = {...formulas, ...weaponFormulas};
    }
  }
  
  // Some enhancements can provide additional formula
  if (!enhancements) enhancements = item.allEnhancements;
  if (enhancements) {
    let fromEnhancements = {};
    enhancements.values().forEach(enh => {
      for (let i = 0; i < enh.number; i++) {
        const enhMod = enh.modifications;
        // Add formula from enhancement;
        if (enhMod.addsNewFormula) {
          let key = "";
          do {
            key = generateKey();
          } while (formulas[key]);
          fromEnhancements[key] = enhMod.formula;
          fromEnhancements[key].enhName = enh.name;
        }
      }

    });
    formulas = {...formulas, ...fromEnhancements};
  }

  return formulas;
}

function _fillCommonRollProperties(roll, commonData) {
  return {
    clear: foundry.utils.mergeObject(roll.clear, commonData),
    modified: foundry.utils.mergeObject(roll.modified, commonData)
  };
}

function _modifiedRollFormula(formula, actor, enhancements, evalData, item) {
  let rollFormula = formula.formula;
  let failFormula = formula.fail ? formula.failFormula : null;
  let modifierSources = formula.enhName || "Base Value";

  // Apply active enhancements
  if (enhancements) {
    enhancements.values().forEach(enh => {
      const enhMod = enh.modifications;
      if (enhMod.hasAdditionalFormula && enhMod.additionalFormula) {
        for (let i = 0; i < enh.number; i++) {
          const additional = (enhMod.additionalFormula.includes("+") || enhMod.additionalFormula.includes("-")) ? enhMod.additionalFormula : ` + ${enhMod.additionalFormula}`;
          rollFormula += additional;
          if (failFormula !== null) failFormula += additional;
          modifierSources += ` + ${enh.name}`;
        }
      }
    });
  }

  // Global Formula Modifiers
  const attackCheckType = evalData.attackCheckType;
  const rangeType = evalData.attackRangeType;
  let globalModKey = "";
  // Apply global modifiers (some buffs to damage or healing etc.)
  if (formula.category === "damage" && attackCheckType) {
    if (attackCheckType === "attack") globalModKey = `attackDamage.martial.${rangeType}`;
    if (attackCheckType === "spell") globalModKey = `attackDamage.spell.${rangeType}`;
  }
  else if (formula.category === "healing") globalModKey = "healing";
  
  const globalMod = _extractGlobalModStringForType(globalModKey, actor);
  if (globalMod.value) {
    rollFormula += ` + (${globalMod.value})`;
    if (failFormula !== null) failFormula += ` + (${globalMod.value})`;
    modifierSources += ` + ${globalMod.source}`;
  }

  if (failFormula !== null) formula.failFormula = failFormula;
  return {
    rollFormula: rollFormula,
    modifierSources: modifierSources
  };
}

function _prepareCheckFormula(actor, checkKey, evalData, source) {
  const rollLevel = evalData.rollLevel;
  const helpDices = evalData.helpDices;
  const rollModifiers = evalData.rollModifiers;

  const [d20roll, rollType] = prepareCheckFormulaAndRollType(checkKey, rollLevel);
  const globalMod = _extractGlobalModStringForType(rollType, actor);
  
  if (globalMod.source !== "") source.value += ` + ${globalMod.source}`;
  if (helpDices !== "") source.value += ` + Help Dice`;
  return `${d20roll} ${globalMod.value} ${helpDices} ${rollModifiers}`;
}

function _prepareAttackFromula(actor, attackFormula, evalData, source) {
  const rollLevel = evalData.rollLevel;
  const helpDices = evalData.helpDices;
  const rollModifiers = evalData.rollModifiers;

  // We need to consider advantages and disadvantages
  let d20roll = "d20";
  if (rollLevel !== 0) d20roll = `${Math.abs(rollLevel)+1}d20${rollLevel > 0 ? "kh" : "kl"}`;
  const formulaMod = attackFormula.formulaMod;
  const rollType = attackFormula.checkType === "attack" ? "attackCheck" : "spellCheck";
  const globalMod = _extractGlobalModStringForType(rollType, actor);
  
  if (globalMod.source !== "") source.value += ` + ${globalMod.source}`;
  if (helpDices !== "") source.value += ` + Help Dice`;
  return `${d20roll} ${formulaMod} ${globalMod.value} ${helpDices} ${rollModifiers}`;
}

//=======================================
//           PREPARE DETAILS            =
//=======================================
function _prepareMessageDetails(item, actor, actionType, rolls) {
  const description = !item.system.statuses || item.system.statuses.identified
          ? item.system.description
          : "<b>Unidentified</b>";
  const itemDetails = !item.system.statuses || item.system.statuses.identified
          ? itemDetailsToHtml(item)
          : "";
  const conditionals = _prepareConditionals(actor.system.conditionals, item);

  const messageDetails = {
    itemId: item._id,
    image: item.img,
    description: description,
    details: itemDetails,
    rollTitle: item.name,
    actionType: actionType,
    conditionals: conditionals,
    showDamageForPlayers: game.settings.get("dc20rpg", "showDamageForPlayers"),
    areas: item.system.target?.areas,
    againstStatuses: _prepareAgainstStatuses(item),
    rollRequests: _prepareRollRequests(item),
    applicableEffects: _prepareEffectsFromItems(item, item.system.effectsConfig?.addToChat) // addToChat left for BACKWARD COMPATIBILITY - remove in the future
  };

  if (actionType === "attack") {
    messageDetails.targetDefence = _prepareTargetDefence(item);
    messageDetails.halfDmgOnMiss = item.system.attackFormula.halfDmgOnMiss;
    messageDetails.skipBonusDamage = item.system.attackFormula.skipBonusDamage;
    messageDetails.canCrit = true;
  }
  if (actionType === "check") {
    messageDetails.checkDetails = _prepareCheckDetails(item, rolls.core, rolls.formula);    messageDetails.canCrit = item.system.check.canCrit;
  }
  return messageDetails;
}

function _prepareTargetDefence(item) {
  let targetDefence = item.system.attackFormula.targetDefence;
  item.activeEnhancements.values().forEach(enh => {
    if (enh.modifications.overrideTargetDefence && enh.modifications.targetDefenceType) {
      targetDefence = enh.modifications.targetDefenceType;
    }
  });
  return targetDefence;
}

function _prepareAgainstStatuses(item) {
  const againstStatuses = item.system.againstStatuses ? Object.values(item.system.againstStatuses) : [];
  item.allEnhancements.values().forEach(enh => {
    if (enh.number > 0) {
      if (enh.modifications.addsAgainstStatus && enh.modifications.againstStatus?.id) {
        againstStatuses.push(enh.modifications.againstStatus);
      }
    }
  });
  return againstStatuses;
}

function _prepareRollRequests(item) {
  const saves = {};
  const contests = {};
  const rollRequests = item.system.rollRequests;
  if (!rollRequests) return {saves: {}, contests: {}};

  // From the item itself
  for (const request of Object.values(rollRequests)) {
    if (request?.category === "save") {
      const requestKey = `save#${request.dc}#${request.saveKey}`;
      saves[requestKey] = request;
      saves[requestKey].label = getLabelFromKey(request.saveKey, CONFIG.DC20RPG.ROLL_KEYS.saveTypes);
    }
    if (request?.category === "contest") {
      const requestKey = `contest#${request.contestedKey}`;
      contests[requestKey] = request;
      contests[requestKey].label = getLabelFromKey(request.contestedKey, CONFIG.DC20RPG.ROLL_KEYS.contests);
    }
  }

  // From the active enhancements
  const enhancements = item.allEnhancements;
  for (const enh of enhancements.values()) {
    if (enh.number && enh.modifications.addsNewRollRequest) {
      const request = enh.modifications.rollRequest;
      if (request?.category === "save") {
        const requestKey = `save#${request.dc}#${request.saveKey}`;
        saves[requestKey] = request;
        saves[requestKey].label = getLabelFromKey(request.saveKey, CONFIG.DC20RPG.ROLL_KEYS.saveTypes);
      }
      if (request?.category === "contest") {
        const requestKey = `contest#${request.contestedKey}`;
        contests[requestKey] = request;
        contests[requestKey].label = getLabelFromKey(request.contestedKey, CONFIG.DC20RPG.ROLL_KEYS.contests);
      }
    }
  }
  return {saves: saves, contests: contests};
}

function _prepareCheckDetails(item) {
  const check = item.system.check;
  const checkKey = check.checkKey;
  return {
    rollLabel: getLabelFromKey(checkKey, CONFIG.DC20RPG.ROLL_KEYS.checks),
    checkDC: item.system.check.checkDC,
    againstDC: item.system.check.againstDC,
    actionType: item.system.actionType,
  }
}

function _prepareEffectsFromItems(item, forceAddToChat) {
  const effects = [];
  // From Item itself
  if (item.effects.size !== 0) {
    item.effects.forEach(effect => {
      const addToChat = effect.flags.dc20rpg?.addToChat;
      if (forceAddToChat || addToChat) {
        const requireEnhancement = effect.flags.dc20rpg?.requireEnhancement;
        if (requireEnhancement) {
          const number = item.allEnhancements.get(requireEnhancement)?.number;
          if (number > 0) effects.push(effect.toObject());
        }
        else {
          effects.push(effect.toObject());
        }
      }
    });
  }
  // From Active Enhancements 
  for (const enh of item.allEnhancements.values()) {
    if (enh.number > 0) {
      const effectData = enh.modifications.addsEffect;
      if (effectData) effects.push(effectData);
    }
  }
  return effects;
}

function _prepareConditionals(conditionals, item) {
  const prepared = [];
  conditionals.forEach(conditional => {
    if (itemMeetsUseConditions(conditional.useFor, item)) {
      prepared.push(conditional);
    }
  });
  return prepared;
}

//=======================================
//              FINISH ROLL             =
//=======================================
function _finishRoll(actor, item, rollMenu, coreRoll) {
  const checkKey = item.checkKey;
  if (checkKey) {
    if (actor.inCombat) applyMultipleCheckPenalty(actor, checkKey, rollMenu);
    _respectNat1Rules(coreRoll, actor, checkKey, item, rollMenu);
  }
  _addSpellToSustain(item, actor);
  _runCritAndCritFailEvents(coreRoll, actor, rollMenu);
  resetRollMenu(rollMenu, item);
  resetEnhancements(item, actor, true);
  _toggleItem(item);
  _deleteEffectsMarkedForRemoval(actor);
  reenablePreTriggerEvents();
}

function resetRollMenu(rollMenu, owner) {
  rollMenu.dis = 0;
  rollMenu.adv = 0;
  rollMenu.apCost = 0;
  rollMenu.gritCost = 0;
  rollMenu.d8 = 0;
  rollMenu.d6 = 0;
  rollMenu.d4 = 0;
  if (rollMenu.free) rollMenu.free = false;
  if (rollMenu.versatile) rollMenu.versatile = false;
  if (rollMenu.ignoreMCP) rollMenu.ignoreMCP = false;
  if (rollMenu.flanks) rollMenu.flanks = false;
  if (rollMenu.halfCover) rollMenu.halfCover = false;
  if (rollMenu.tqCover) rollMenu.tqCover = false;
  if (rollMenu.initiative) rollMenu.initiative = false;
  if (rollMenu.autoCrit) rollMenu.autoCrit = false;
  if (rollMenu.autoFail) rollMenu.autoFail = false;
  owner.update({['flags.dc20rpg.rollMenu']: rollMenu});
}

function resetEnhancements(item, actor, itemRollFinished) {
  if (!item.allEnhancements) return;
  
  item.allEnhancements.forEach((enh, key) => { 
    if (enh.number !== 0) {
      const enhOwningItem = actor.items.get(enh.sourceItemId);
      if (enhOwningItem) {
        runTemporaryItemMacro(enhOwningItem, "enhancementReset", actor, {enhancement: enh, itemRollFinished: itemRollFinished, enhKey: key});
        enhOwningItem.update({[`system.enhancements.${key}.number`]: 0});
      }
    }
  });
}

function _runCritAndCritFailEvents(coreRoll, actor, rollMenu) {
  if (!coreRoll) return;
  if (coreRoll.fail && actor.inCombat && !rollMenu.autoFail) {
    runEventsFor("critFail", actor);
  }
  if (coreRoll.crit && actor.inCombat && !rollMenu.autoCrit) {
    runEventsFor("crit", actor);
  }
}

function _respectNat1Rules(coreRoll, actor, rollType, item, rollMenu) {
  if (coreRoll.fail && actor.inCombat) {
    // Only attack and not forced nat 1 should expose the attacker
    if (["attackCheck", "spellCheck", "att", "spe"].includes(rollType) && !rollMenu.autoFail) {
      sendDescriptionToChat(actor, {
        rollTitle: "Critical Fail - exposed",
        image: actor.img,
        description: "You become Exposed (Attack Checks made against it has ADV) against the next Attack made against you before the start of your next turn.",
      });
      actor.toggleStatusEffect("exposed", { active: true, extras: {untilFirstTimeTriggered: true, untilTargetNextTurnStart: true} });
    }

    if (["spellCheck", "spe"].includes(rollType)) {
      if (item && !item.flags.dc20rpg.rollMenu.free) revertUsageCostSubtraction(actor, item);
    }
  }
}

function _toggleItem(item) {
  if (item.system.toggle?.toggleable && item.system.toggle.toggleOnRoll) {
    item.update({["system.toggle.toggledOn"]: true});
  }
}

function _deleteEffectsMarkedForRemoval(actor) {
  if (!actor.flags.dc20rpg.effectsToRemoveAfterRoll) return;
  actor.flags.dc20rpg.effectsToRemoveAfterRoll.forEach(toRemove => {
    if (game.user.isGM) {
      effectsToRemovePerActor(toRemove);
    }
    else {
      const activeGM = game.users.activeGM;
      if (!activeGM) {
        ui.notifications.error("There needs to be an active GM to remove effects from other actors");
      }
      else {
        emitSystemEvent("removeEffectFrom", {toRemove: toRemove, gmUserId: activeGM.id});
      }
    }
  });
  actor.update({["flags.dc20rpg.effectsToRemoveAfterRoll"]: []}); // Clear effects to remove
} 

//=======================================
//            OTHER FUNCTIONS           =
//=======================================
function _determineRollLevel(rollMenu) {
  const disLevel = rollMenu.dis;
  const advLevel = rollMenu.adv;
  return advLevel - disLevel;
}

function _extractGlobalModStringForType(path, actor) {
  const globalModJson = getValueFromPath(actor.system.globalFormulaModifiers, path) || [];
  let globalMod = {
    value: "",
    source: ""
  };
  for(let json of globalModJson) {
    if (!json) continue;
    try {
      const mod = JSON.parse(`{${json}}`);
      globalMod.value += mod.value;
      if (globalMod.source === "") globalMod.source += `${mod.source}`;
      else globalMod.source += ` + ${mod.source}`;
    } catch (e) {
      console.warn(`Cannot parse global formula modifier json {${json}} with error: ${e}`);
    }
  }
  return globalMod;
}

async function _runEnancementsMacro(item, macroKey, actor, additionalFields) {
  const enhancements = item.activeEnhancements;
  if (!enhancements) return; 

  for (const [enhKey, enh] of enhancements.entries()) {
    const macros = enh.modifications.macros;
    if (!macros) continue;

    const command = macros[macroKey];
    if (command && command !== "") {
      await runTemporaryMacro(command, item, {item: item, actor: actor, enh: enh, enhKey: enhKey, ...additionalFields});
    }
  }
}

async function _addSpellToSustain(item, actor) {
  if (item.system.duration.type !== "sustain") return;

  const activeCombat = game.combats.active;
  const notInCombat = !(activeCombat && activeCombat.started && actor.inCombat);
  if (notInCombat) return;

  const currentSustain = actor.system.sustain;
  currentSustain.push({
    name: item.name,
    img: item.img,
    itemId: item.id,
    description: item.system.description
  });
  await actor.update({[`system.sustain`]: currentSustain});
}

//===============================
//=      TEMPORARY MACROS       =
//===============================
function createTemporaryMacro(command, object, flagsToSet={}) {
  const flags = {
    dc20rpg: {
      temporaryMacro: true,
      ...flagsToSet
    }
  };

  try {
    return new Macro({
      name: object.name,
      type: "script",
      img: object.img,
      command: command,
      flags: flags
    });
  }
  catch(e) {
    ui.notifications.error(`Your macro had validation errors and was reseted, reason: '${e}'`);
    return new Macro({
      name: object.name,
      type: "script",
      img: object.img,
      command: "",
      flags: flags
    });
  }
}

async function runTemporaryItemMacro(item, trigger, actor, additionalFields) {
  if (!actor) return;
  const macros = item?.system?.macros;
  if (!macros) return;
  
  for (const macro of Object.values(macros)) {
    if (macro.trigger === trigger && !macro.disabled) {
      const command = macro.command;
      if (command) {
        await runTemporaryMacro(command, item, {item: item, actor: actor, ...additionalFields});
      }
    }
  }
}

async function runTemporaryMacro(command, object, additionalFields) {
  if (!command) return;
  const macro = createTemporaryMacro(command, object);
  if (additionalFields) {
    for (const [key, field] of Object.entries(additionalFields)) {
      macro[key] = field;
    }
  }
  await macro.execute(macro);
}

//=================================
//=     CUSTOM MACRO TRIGGERS     =
//=================================
function registerItemMacroTrigger(trigger, displayedLabel) {
  CONFIG.DC20RPG.macroTriggers[trigger] = displayedLabel;
}

//=============================
//=       HOTBAR MACROS       =
//=============================
/**
 * Create a Macro from an Item drop.
 * Get an existing item macro if one exists, otherwise create a new one.
 * @param {Object} data     The dropped data
 * @param {number} slot     The hotbar slot to use
 * @returns {Promise}
 */
async function createItemHotbarDropMacro(data, slot) {
  // First, determine if this is a valid owned item.
  if (data.type !== "Item") return;
  if (!data.uuid.includes('Actor.') && !data.uuid.includes('Token.')) {
    return ui.notifications.warn("You can only create roll macro for owned Items");
  }
  // If it is, retrieve it based on the uuid.
  const item = await Item.fromDropData(data);

  // Create the macro command using the uuid.
  const command = `game.dc20rpg.rollItemWithName("${item.name}");`;
  const matchingMacros = game.macros.filter(m => (m.name === item.name) && (m.command === command));
  let macro = undefined;
  for (const match of matchingMacros) {
    if (match.isOwner) macro = match;
  }
  if (!macro) {
    macro = await Macro.create({
      name: item.name,
      type: "script",
      img: item.img,
      command: command,
      flags: { "dc20rpg.itemMacro": true }
    });
  }
  game.user.assignHotbarMacro(macro, slot);
}

async function rollItemWithName(itemName) {
  const seletedTokens = getSelectedTokens();
  if (!seletedTokens) return ui.notifications.warn(`No selected or assigned actor could be found to target with macro.`);

  for (let token of seletedTokens) {
    const actor = await token.actor;
    const item = await actor.items.getName(itemName);
    if (!item) {
      ui.notifications.warn(`Actor '${actor.name}' does not own item named '${itemName}'.`);
      continue;
    }

    promptItemRoll(actor, item);
  }
}

//============================================
//              Item Usage Costs             =
//============================================
/**
 * Return item costs data formatted to be used in html files.
 */
function getItemUsageCosts(item, actor) {
  if (!item.system.costs) return {};
  const usageCosts = {};
  usageCosts.resources = _getItemResources(item);
  usageCosts.otherItem = _getOtherItem(item, actor);
  return usageCosts;
}

function _getItemResources(item) {
  const resourcesCosts = item.system.costs.resources;

  let counter = 0;
  let costs = {
    actionPoint: {cost: resourcesCosts.actionPoint},
    stamina: {cost: resourcesCosts.stamina},
    mana: {cost: resourcesCosts.mana},
    health: {cost: resourcesCosts.health},
    grit: {cost: resourcesCosts.grit},
    restPoints: {cost: resourcesCosts.restPoints},
    custom: {}
  };
  counter += resourcesCosts.actionPoint || 0;
  counter += resourcesCosts.stamina || 0;
  counter += resourcesCosts.mana || 0;
  counter += resourcesCosts.health || 0;
  counter += resourcesCosts.restPoints || 0;

  Object.entries(resourcesCosts.custom).forEach(([key, customCost]) => {
    counter += customCost.value || 0;
    costs.custom[key] = {
      img: customCost.img,
      name: customCost.name,
      value: customCost.value
    };
  });

  return {
    counter: counter,
    costs: costs
  };
}

function _getOtherItem(item, actor) {
  const otherItem = item.system.costs.otherItem;
  if(!actor) return {};

  const usedItem = actor.items.get(otherItem.itemId);
  if (!usedItem) return {};

  return {
    amount: otherItem.amountConsumed,
    consumeCharge: otherItem.consumeCharge,
    name: usedItem.name,
    image: usedItem.img,
  }
}

//============================================
//          Resources Manipulations          =
//============================================
function subtractAP(actor, amount) {
  if (typeof amount !== 'number') return true;
  if (canSubtractBasicResource("ap", actor, amount)) {
    subtractBasicResource("ap", actor, amount);
    return true;
  }
  return false;
}

function subtractGrit(actor, amount) {
  if (typeof amount !== 'number') return true;
  if (canSubtractBasicResource("grit", actor, amount)) {
    subtractBasicResource("grit", actor, amount);
    return true;
  }
  return false;
}

async function spendRpOnHp(actor, amount) {
  if (canSubtractBasicResource("restPoints", actor, amount)) {
    await subtractBasicResource("restPoints", actor, amount);
    await regainBasicResource("health", actor, amount, true);
    return true;
  }
  return false;
}

function refreshAllActionPoints(actor) {
  actor = _checkIfShouldSubtractFromCompanionOwner(actor, "ap");
  const max = actor.system.resources.ap.max;
  actor.update({["system.resources.ap.value"] : max});
}

async function subtractBasicResource(key, actor, amount, boundary) {
  amount = parseInt(amount);
  if (amount <= 0) return;

  actor = _checkIfShouldSubtractFromCompanionOwner(actor, key);
  const resources = actor.system.resources;
  if (!resources.hasOwnProperty(key)) return;

  const current = resources[key].value;
  const newAmount = (boundary === "true" || boundary === true) ? Math.max(current - amount, 0) : current - amount;

  await actor.update({[`system.resources.${key}.value`] : newAmount});
}

async function regainBasicResource(key, actor, amount, boundary) {
  amount = parseInt(amount);
  if (amount <= 0) return;

  actor = _checkIfShouldSubtractFromCompanionOwner(actor, key);
  const resources = actor.system.resources;
  if (!resources.hasOwnProperty(key)) return;

  const valueKey = key === "health" ? "current" : "value";
  const current = resources[key][valueKey];
  const max = resources[key].max;
  const newAmount = (boundary === "true" || boundary === true) ? Math.min(current + amount, max) : current + amount;

  await actor.update({[`system.resources.${key}.${valueKey}`] : newAmount});
}

async function subtractCustomResource(key, actor, amount, boundary) {
  amount = parseInt(amount);
  if (amount <= 0) return;

  const custom = actor.system.resources.custom[key];
  if (!custom) return;

  const current = custom.value;
  const newAmount = (boundary === "true" || boundary === true) ? Math.max(current - amount, 0) : current - amount;
  await actor.update({[`system.resources.custom.${key}.value`] : newAmount});
}

async function regainCustomResource(key, actor, amount, boundary) {
  amount = parseInt(amount);
  if (amount <= 0) return;

  const custom = actor.system.resources.custom[key];
  if (!custom) return;

  const current = custom.value;
  const max = custom.max;
  const newAmount = (boundary === "true" || boundary === true) ? Math.min(current + amount, max) : current + amount;
  await actor.update({[`system.resources.custom.${key}.value`] : newAmount});
}

//===========================================
//        Item Charges Manipulations        =
//===========================================
function changeCurrentCharges(value, item) {
  let changedValue = parseInt(value);
  let maxCharges = parseInt(item.system.costs.charges.max);
  if (isNaN(changedValue)) changedValue = 0;
  if (changedValue < 0) changedValue = 0;
  if (changedValue > maxCharges) changedValue = maxCharges;
  item.update({["system.costs.charges.current"] : changedValue});
}

//=============================================
//        Item Usage Costs Subtraction        =
//=============================================
/**
 * Checks if all resources used by item are available for actor. 
 * If so subtracts those from actor current resources.
 */
async function respectUsageCost(actor, item) {
  // First check if weapon needs reloading
  const weaponWasLoaded = runWeaponLoadedCheck(item);
  if (!weaponWasLoaded) return false;

  if (!item.system.costs) return true;
  let basicCosts = item.system.costs.resources;
  basicCosts = _costsAndEnhancements(actor, item);
  basicCosts = _costFromAdvForApAndGrit(item, basicCosts);

  // Held action ignore AP cost as it was subtracted before
  if (actor.flags.dc20rpg.actionHeld?.rollsHeldAction) {
    basicCosts.actionPoint = null;
  }

  // Enhacements can cause charge to be subtracted
  let [charges] = _collectCharges(item);

  const costs = {charges: charges, basicCosts: basicCosts};
  const skip = await runTemporaryItemMacro(item, "preItemCost", actor, {costs: costs});
  if (skip) return true;

  if(_canSubtractAllResources(actor, item, costs.basicCosts, costs.charges) 
        && _canSubtractFromOtherItem(actor, item)
        && _canSubtractFromEnhLinkedItems(actor, item)
  ) {
    await _subtractAllResources(actor, item, costs.basicCosts, costs.charges);
    _subtractFromOtherItem(actor, item);
    _subtractFromEnhLinkedItems(actor, item);
    if (weaponWasLoaded) unloadWeapon(item, actor);
    return true;
  }
  return false;
}

function collectExpectedUsageCost(actor, item) {
  if (!item.system.costs) return [{}, {}];

  let basicCosts = item.system.costs.resources;
  basicCosts = _costsAndEnhancements(actor, item);
  basicCosts = _costFromAdvForApAndGrit(item, basicCosts);

  // Held action ignore AP cost as it was subtracted before
  if (actor.flags.dc20rpg.actionHeld?.rollsHeldAction) {
    basicCosts.actionPoint = null;
  }

  const [charges, chargesFromOtherItems] = _collectCharges(item);

  return [basicCosts, charges, chargesFromOtherItems];
}

async function revertUsageCostSubtraction(actor, item) {
  if (!item.system.costs) return;
  let basicCosts = item.system.costs.resources;
  basicCosts = _costsAndEnhancements(actor, item);

  basicCosts.actionPoint = 0;
  basicCosts.stamina = 0;
  basicCosts.mana = 0;
  basicCosts.health = 0;
  for (let [key, custom] of Object.entries(basicCosts.custom)) {
    if (custom) basicCosts.custom[key].value = -custom.value;
  }
  await _subtractAllResources(actor, item, basicCosts, 0);
}

function _costsAndEnhancements(actor, item) {
  const enhancements = item.allEnhancements;  
  
  let costs = foundry.utils.deepClone(item.system.costs.resources);
  if (!enhancements) return costs;

  for (let enhancement of enhancements.values()) {
    if (enhancement.number) {
      // Core Resources
      for (let [key, resource] of Object.entries(enhancement.resources)) {
        if (key !== 'custom') costs[key] += enhancement.number * resource;
      }

      // Custom Resources
      for (let [key, custom] of Object.entries(enhancement.resources.custom)) {
        // If only enhancement is using that custom resource we want to add it to costs here
        if(!costs.custom[key]) {
          // We need to copy that enhancement
          costs.custom[key] = foundry.utils.deepClone(custom);
          // And then check its cost depending on number of uses
          costs.custom[key].value = enhancement.number * custom.value;
        }
        else {
          costs.custom[key].value += enhancement.number * custom.value;
        }
      }
    }
  }

  const outsideOfCombatRule = game.settings.get("dc20rpg", "outsideOfCombatRule");
  if (outsideOfCombatRule) {
    const activeCombat = game.combats.active;
    const notInCombat = !(activeCombat && activeCombat.started && actor.inCombat);
    if (notInCombat) {
      // No AP is being used outside of combat
      if (costs.actionPoint > 0) costs.actionPoint = 0;

      // No stamina is being used outside of combat
      if (costs.stamina > 0) costs.stamina = 0;

      // Mana usage is one less outside of combat (no less than 1)
      if (costs.mana > 1) costs.mana = costs.mana - 1;
    }
  } 

  return costs;
}

function _canSubtractAllResources(actor, item, costs, charges) {
  let canSubtractAllResources = [
    canSubtractBasicResource("ap", actor, costs.actionPoint),
    canSubtractBasicResource("stamina", actor, costs.stamina),
    canSubtractBasicResource("mana", actor, costs.mana),
    canSubtractBasicResource("health", actor, costs.health),
    canSubtractBasicResource("grit", actor, costs.grit),
    canSubtractBasicResource("restPoints", actor, costs.restPoints),
    _canSubtractCustomResources(actor, costs.custom),
    _canSubtractCharge(item, charges),
    _canSubtractQuantity(item, 1),
  ];
  return arrayOfTruth(canSubtractAllResources);
}

async function _subtractAllResources(actor, item, costs, charges) {
  const oldResources = actor.system.resources;

  let [newResources, resourceMax] = _copyResources(oldResources);
  newResources = _prepareBasicResourceModification("ap", costs.actionPoint, newResources, resourceMax, actor);
  newResources = _prepareBasicResourceModification("stamina", costs.stamina, newResources, resourceMax, actor);
  newResources = _prepareBasicResourceModification("mana", costs.mana, newResources, resourceMax, actor);
  newResources = _prepareBasicResourceModification("health", costs.health, newResources, resourceMax, actor);
  newResources = _prepareBasicResourceModification("grit", costs.grit, newResources, resourceMax, actor);
  newResources = _prepareBasicResourceModification("restPoints", costs.restPoints, newResources, resourceMax, actor);
  newResources = _prepareCustomResourcesModification(costs.custom, newResources, resourceMax);
  await _subtractActorResources(actor, newResources);
  _subtractCharge(item, charges);
  _subtractQuantity(item, 1, true);
}

async function _subtractActorResources(actor, newResources) {
  await actor.update({['system.resources'] : newResources});
}

function _copyResources(old) {
  const nev = {
    ap: {},
    stamina: {},
    mana: {},
    health: {},
    grit: {},
    restPoints: {},
    custom: {}
  };
  const max = {
    ap: {},
    stamina: {},
    mana: {},
    health: {},
    grit: {},
    restPoints: {},
    custom: {}
  };

  // Standard Resources
  for (const [key, resource] of Object.entries(old)) {
    if(key === "custom") continue;

    if(key === "health") nev[key].current = resource.current;
    else nev[key].value = resource.value;
    
    max[key].max = resource.max;
  }

  // Custom Resources
  for (const [key, resource] of Object.entries(old.custom)) {
    if (!nev.custom[key]) nev.custom[key] = {}; // If no object with key found create new object
    if (!max.custom[key]) max.custom[key] = {}; // If no object with key found create new object

    nev.custom[key].value = resource.value;
    max.custom[key].max = resource.max;
  }
  
  return [nev, max];
}

//================================
//        Basic Resources        =
//================================
function canSubtractBasicResource(key, actor, cost) {
  if (cost <= 0) return true;

  actor = _checkIfShouldSubtractFromCompanionOwner(actor, key);
  const resources = actor.system.resources;
  if (!resources.hasOwnProperty(key)) return true;
  
  const current = key === "health" ? resources[key].current : resources[key].value;
  const newAmount = current - cost;

  if (newAmount < 0) {
    let errorMessage = `Cannot subract ${cost} ${key} from ${actor.name}. Not enough ${key} (Current amount: ${current}).`;
    ui.notifications.error(errorMessage);
    return false;
  }

  // AP Spend Limit 
  if (key === "ap") {
    const spendLimit = actor.system.globalModifier.prevent.goUnderAP;
    if (newAmount < spendLimit) {
      if (spendLimit >= resources[key].max) ui.notifications.error(`You cannot spend AP`);
      else ui.notifications.error(`You cannot go under ${spendLimit} AP`);
      return false;
    }
  }
  return true;
}

function _costFromAdvForApAndGrit(actor, basicCosts) {
  const apCostFromAdv = actor.flags.dc20rpg.rollMenu.apCost;
  if (basicCosts.actionPoint) basicCosts.actionPoint += apCostFromAdv;
  else basicCosts.actionPoint = apCostFromAdv;

  const gritCostFromAdv = actor.flags.dc20rpg.rollMenu.gritCost;
  if (basicCosts.grit) basicCosts.grit += gritCostFromAdv;
  else basicCosts.grit = gritCostFromAdv;
  return basicCosts;
}

function _prepareBasicResourceModification(key, cost, newResources, resourceMax, actor) {
  if (companionShare(actor, key)) {
    const subKey = key === "health" ? "current" : "value"; 
    const currentValue = actor.companionOwner.system.resources[key][subKey];
    actor.companionOwner.update({[`system.resources.${key}.${subKey}`]: currentValue - cost});
    return newResources; // We dont modify value of companion because we subtract from owner
  }
  if (!newResources.hasOwnProperty(key)) return newResources;
  if(key === "health") {
    const newAmount = newResources[key].current - cost;
    newResources[key].current = newAmount > resourceMax[key].max ? resourceMax[key].max : newAmount;
  }
  else {
    const newAmount = newResources[key].value - cost;
    newResources[key].value = newAmount > resourceMax[key].max ? resourceMax[key].max : newAmount;
  }
  return newResources;
}

//=================================
//        Custom Resources        =
//=================================
function canSubtractCustomResource(key, actor, cost) {
  const customResource = actor.system.resources.custom[key];
  if (!customResource) return true;
  if (cost.value <= 0) return true;

  const current = customResource.value;
  const newAmount = current - cost.value;

  if (newAmount < 0) {
    let errorMessage = `Cannot subract ${cost.value} charges of custom resource ${cost.name} from ${actor.name}. Current amount: ${current}.`;
    ui.notifications.error(errorMessage);
    return false;
  }
  return true;
}

function _canSubtractCustomResources(actor, customCosts) {
  for (const [key, cost] of Object.entries(customCosts)) {
    if (!canSubtractCustomResource(key, actor, cost)) return false;
  }
  return true;
}

function _prepareCustomResourcesModification(customCosts, newResources, resourceMax) {
  const customResources = newResources.custom;
  const maxResources = resourceMax.custom;

  for (const [key, cost] of Object.entries(customCosts)) {
    if (!customResources[key]) continue;

    const current = customResources[key].value;
    const newAmount = current - cost.value;
    newResources.custom[key].value = newAmount > maxResources[key].max ? maxResources[key].max : newAmount;
  }
  return newResources;
}

//===============================
//        Item Resources        =
//===============================
function _canSubtractFromOtherItem(actor, item) {
  const otherItemUsage = item.system.costs.otherItem;
  if (!otherItemUsage.itemId) return true;

  const otherItem = actor.items.get(otherItemUsage.itemId);
  if (!otherItem) {
    let errorMessage = `Item used by ${item.name} doesn't exist.`;
    ui.notifications.error(errorMessage);
    return false;
  }

  return otherItemUsage.consumeCharge 
    ? _canSubtractCharge(otherItem, otherItemUsage.amountConsumed) 
    : _canSubtractQuantity(otherItem, otherItemUsage.amountConsumed);
}

function _subtractFromOtherItem(actor, item) {
  const otherItemUsage = item.system.costs.otherItem;
  if (otherItemUsage.itemId) {
    const otherItem = actor.items.get(otherItemUsage.itemId);
    otherItemUsage.consumeCharge 
     ? _subtractCharge(otherItem, otherItemUsage.amountConsumed) 
     : _subtractQuantity(otherItem, otherItemUsage.amountConsumed, false);
  }
}

function _canSubtractFromEnhLinkedItems(actor, item) {
  const chargesPerItem = _collectEnhLinkedItemsWithCharges(item, actor);

  for (let original of Object.values(chargesPerItem)) {
    if (!_canSubtractCharge(original.item, original.amount)) return false;
  }
  return true;
}

function _subtractFromEnhLinkedItems(actor, item) {
  const chargesPerItem = _collectEnhLinkedItemsWithCharges(item, actor);

  for (let original of Object.values(chargesPerItem)) {
    _subtractCharge(original.item, original.amount);
  }
}

function _canSubtractCharge(item, subtractedAmount) {
  let max = item.system.costs.charges.max;
  if (!max) return true;

  let current = item.system.costs.charges.current;
  let newAmount = current - subtractedAmount;

  if (newAmount < 0) {
    let errorMessage = `Cannot use ${item.name}. Not enough charges.`;
    ui.notifications.error(errorMessage);
    return false;
  }
  return true;
}

function _subtractCharge(item, subtractedAmount) {
  let max = item.system.costs.charges.max;
  if (!max) return;

  let current = item.system.costs.charges.current;
  let newAmount = current - subtractedAmount;

  item.update({["system.costs.charges.current"] : newAmount});
}

function _canSubtractQuantity(item, subtractedAmount) {
  if (item.type !== "consumable") return true; // It is not consumable
  if (!item.system.consume) return true; // It doesn't consume item on use

  let current = item.system.quantity;
  let newAmount = current - subtractedAmount;

  if (current <= 0) {
    let errorMessage = `Cannot use ${item.name}. No more items.`;
    ui.notifications.error(errorMessage);
    return false;
  }

  if (newAmount < 0) {
    let errorMessage = `Cannot use ${item.name}. No enough items.`;
    ui.notifications.error(errorMessage);
    return false;
  }

  return true;
}

function _subtractQuantity(item, subtractedAmount, markToRemoval) {
  if (item.type !== "consumable") return;
  if (!item.system.consume) return;

  let deleteOnZero = item.system.deleteOnZero;
  let current = item.system.quantity;
  let newAmount = current - subtractedAmount;

  if (newAmount === 0 && deleteOnZero) {
    if(markToRemoval) item.deleteAfter = true; // Mark item to removal
    else item.delete();
  } 
  else {
    item.update({["system.quantity"] : newAmount});
  }
}

//===============================
//            Helpers           =
//===============================
function _collectEnhLinkedItemsWithCharges(item, actor) {
  const chargesPerItem = {};

  // Collect how many charges you need to use
  for (const enhancement of item.allEnhancements.values()) {
    if (enhancement.number) {
      const charges = enhancement.charges;
      if (charges?.consume && charges.fromOriginal && enhancement.sourceItemId !== item.id) {
        const original = actor.items.get(enhancement.sourceItemId);
        if (original) {
          const alreadyExist = chargesPerItem[enhancement.sourceItemId];
          if (alreadyExist) {
            alreadyExist.amount += enhancement.number;
          }
          else {
            chargesPerItem[enhancement.sourceItemId] = {
              item: original,
              amount: enhancement.number
            };
          }
        }
      }
    }
  }
  return chargesPerItem;
}

function _collectCharges(item) {
  // If item has max charges we want to remove one for sure;
  const itemCharges = item.system.costs.charges;
  let charges = itemCharges.max ? (itemCharges.subtract || 0) : 0;
  let chargesFromOtherItems = 0;
 
  // Collect how many charges you need to use
  for (let enhancement of item.allEnhancements.values()) {
    if (enhancement.number) {
      if (enhancement.charges?.consume) {
        if (enhancement.charges.fromOriginal && enhancement.sourceItemId !== item.id) {
          chargesFromOtherItems += enhancement.number;
        }
        else {
          charges += enhancement.number;
        }
      }
    }
  }
  return [charges, chargesFromOtherItems];
}

function _checkIfShouldSubtractFromCompanionOwner(actor, key) {
  if (companionShare(actor, key)) return actor.companionOwner;
  return actor;
}

function sortMapOfItems(context, mapOfItems) {  
  const sortedEntries = [...mapOfItems.entries()].sort(([, a], [, b]) => a.sort - b.sort);

  if (!sortedEntries) return mapOfItems; // No entries, map is empty

  sortedEntries.forEach(entry => mapOfItems.delete(entry[0])); // we want to remove all original entries because those are not sorted
  sortedEntries.forEach(entry => mapOfItems.set(entry[0], entry[1])); // we put sorted entries to map
  context.items = mapOfItems;
}

function onSortItem(event, itemData, actor) {
  // Get the drag source and drop target
  const items = actor.items;
  const source = items.get(itemData._id);

  let dropTarget = event.target.closest("[data-item-id]");

  // We dont want to change tableName if item is sorted on Attacks table
  const itemRow = event.target.closest("[data-item-attack]");
  const isAttack = itemRow ? true : false;

  // if itemId not found we want to check if user doesn't dropped item on table header
  if (!dropTarget) {
    dropTarget = event.target.closest("[data-table-name]"); 
    if (!dropTarget || isAttack) return;
    source.update({["flags.dc20rpg.tableName"]: dropTarget.dataset.tableName});
    return;
  }

  const target = items.get(dropTarget.dataset.itemId);

  // Don't sort on yourself
  if ( source.id === target.id ) return;

  // Identify sibling items based on adjacent HTML elements
  const siblings = [];
  for ( let el of dropTarget.parentElement.children ) {
    const siblingId = el.dataset.itemId;
    if ( siblingId && (siblingId !== source.id) ) {
      siblings.push(items.get(el.dataset.itemId));
    } 
  }

  // Perform the sort
  const sortUpdates = SortingHelpers.performIntegerSort(source, {target, siblings});
  const updateData = sortUpdates.map(u => {
    const update = u.update;
    update._id = u.target._id;
    return update;
  });

  // Change items tableName to targets one, skip this if item was sorted on attack row
  if (!isAttack) {
    source.update({["flags.dc20rpg.tableName"]: target.flags.dc20rpg.tableName});
  }

  // Perform the update
  return actor.updateEmbeddedDocuments("Item", updateData);
}

function prepareItemsForCharacter(context, actor) {
  const headersOrdering = context.flags.dc20rpg?.headersOrdering;
  if (!headersOrdering) return;

  const inventory = _sortAndPrepareTables(headersOrdering.inventory);
  const features = _sortAndPrepareTables(headersOrdering.features);
  const techniques = _sortAndPrepareTables(headersOrdering.techniques);
  const spells = _sortAndPrepareTables(headersOrdering.spells);
  const favorites = _sortAndPrepareTables(headersOrdering.favorites);
  const basic = _sortAndPrepareTables(headersOrdering.basic);

  const itemChargesAsResources = {};
  const itemQuantityAsResources = {};

  for (const item of context.items) {
    const isFavorite = item.flags.dc20rpg.favorite;
    _prepareItemUsageCosts$1(item, actor);
    prepareItemFormulas(item, actor);
    _prepareItemAsResource(item, itemChargesAsResources, itemQuantityAsResources);
    _checkIfItemIsIdentified(item);
    item.img = item.img || DEFAULT_TOKEN;

    switch (item.type) {
      case 'weapon': case 'equipment': case 'consumable': case 'loot':
        _addItemToTable(item, inventory); 
        if (isFavorite) _addItemToTable(item, favorites, "inventory");
        break;
      case 'feature': 
        _addItemToTable(item, features, item.system.featureType); 
        if (isFavorite) _addItemToTable(item, favorites, "feature");
        break;
      case 'technique': 
        _addItemToTable(item, techniques, item.system.techniqueType); 
        if (isFavorite) _addItemToTable(item, favorites, "technique");
        break;
      case 'spell': 
        _addItemToTable(item, spells, item.system.spellType); 
        if (isFavorite) _addItemToTable(item, favorites, "spell");
        break;
      case 'basicAction': 
        _addItemToTable(item, basic, item.system.category);
        if (isFavorite) _addItemToTable(item, favorites, "basic");
        break;
      
      case 'class': context.class = item; break;
      case 'subclass': context.subclass = item; break;
      case 'ancestry': context.ancestry = item; break;
      case 'background': context.background = item; break;
    }
  }

  context.inventory = _filterItems(actor.flags.dc20rpg.headerFilters?.inventory, inventory);
  context.features = _filterItems(actor.flags.dc20rpg.headerFilters?.features, features);
  context.techniques = _filterItems(actor.flags.dc20rpg.headerFilters?.techniques, techniques);
  context.spells = _filterItems(actor.flags.dc20rpg.headerFilters?.spells, spells);
  context.basic = _filterItems(actor.flags.dc20rpg.headerFilters?.basic, basic);
  context.favorites = _filterItems(actor.flags.dc20rpg.headerFilters?.favorites, favorites);
  context.itemChargesAsResources = itemChargesAsResources;
  context.itemQuantityAsResources = itemQuantityAsResources;
}

function prepareItemsForNpc(context, actor) {
  const headersOrdering = context.flags.dc20rpg?.headersOrdering;
  if (!headersOrdering) return;
  const main = _sortAndPrepareTables(headersOrdering.main);
  const basic = _sortAndPrepareTables(headersOrdering.basic);

  const itemChargesAsResources = {};
  const itemQuantityAsResources = {};

  for (const item of context.items) {
    _prepareItemUsageCosts$1(item, actor);
    prepareItemFormulas(item, actor);
    _prepareItemAsResource(item, itemChargesAsResources, itemQuantityAsResources);
    item.img = item.img || DEFAULT_TOKEN;

    if (["weapon", "equipment", "consumable", "loot"].includes(item.type)) {
      const itemCosts = item.system.costs;
      if (itemCosts && itemCosts.resources.actionPoint !== null) _addItemToTable(item, main, "action");
      else _addItemToTable(item, main, "inventory");
    }
    else if (item.type === "basicAction") {
      _addItemToTable(item, basic, item.system.category);
      if (item.flags.dc20rpg.favorite) _addItemToTable(item, main, "action");
    }
    else if (["class", "subclass", "ancestry", "background"].includes(item.type)) ; // NPCs shouldn't have those items anyway
    else {
      _addItemToTable(item, main); 
    }
  }
 
  context.main = _filterItems(actor.flags.dc20rpg.headerFilters?.main, main);
  context.basic = _filterItems(actor.flags.dc20rpg.headerFilters?.basic, basic);
  context.itemChargesAsResources = itemChargesAsResources;
  context.itemQuantityAsResources = itemQuantityAsResources;
}

function prepareCompanionTraits(context, actor) {
  let choicePointsSpend = 0;

  const uniqueActive = [];
  const repeatableActive = [];
  const uniqueInactive = [];
  const repeatableInactive = [];

  for (const [key, trait] of Object.entries(actor.system.traits)) {
    trait.key = key;

    if (trait.active > 0) {
      const pointsCost = trait.itemData?.system?.choicePointCost || 1;
      choicePointsSpend += pointsCost * trait.active; // Cost * number of times trait was taken

      if (trait.repeatable) repeatableActive.push(trait);
      else uniqueActive.push(trait);
    }
    else {
      if (trait.repeatable) repeatableInactive.push(trait);
      else uniqueInactive.push(trait);
    }
  } 

  context.traits = {
    uniqueActive: uniqueActive,
    repeatableActive: repeatableActive,
    uniqueInactive: uniqueInactive,
    repeatableInactive: repeatableInactive
  };
  context.choicePointsSpend = choicePointsSpend;
}

function _prepareItemAsResource(item, charages, quantity) {
  _prepareItemChargesAsResource(item, charages);
  _prepareItemQuantityAsResource(item, quantity);
}

function _prepareItemChargesAsResource(item, charages) {
  if (!item.system.costs) return;
  if (!item.system.costs.charges.showAsResource) return;

  const itemCharges = item.system.costs.charges;
  charages[item.id] = {
    img: item.img,
    name: item.name,
    value: itemCharges.current,
    max: itemCharges.max
  };
}

function _prepareItemQuantityAsResource(item, quantity) {
  if (item.type !== "consumable") return;
  if (item.system.quantity === undefined) return;
  if (!item.system.showAsResource) return;

  quantity[item.id] = {
    img: item.img,
    name: item.name,
    quantity: item.system.quantity
  };
}

function _checkIfItemIsIdentified(item) {
  const identified = item.system.statuses ? item.system.statuses.identified : true;
  if (!identified) {
    item.unidefined = true;
    item.name = game.i18n.localize("dc20rpg.item.sheet.unidentified");
    item.system.description = game.i18n.localize("dc20rpg.item.sheet.unidentifiedDescription");
  }
  else {
    item.unidefined = false;
  }
}

function _sortAndPrepareTables(tables) {
  const sorted = Object.entries(tables).sort((a, b) => a[1].order - b[1].order);
  const headers = {};
  
  for(let i = 0; i < sorted.length; i++) {
    const siblingBefore = sorted[i-1] ? sorted[i-1][0] : undefined;
    const siblingAfter = sorted[i+1] ? sorted[i+1][0] : undefined;

    headers[sorted[i][0]] = {
      name: sorted[i][1].name,
      custom: sorted[i][1].custom,
      items: {},
      siblings: {
        before: siblingBefore,
        after: siblingAfter
      }
    };
  }
  return headers;
}

function _prepareItemUsageCosts$1(item, actor) {
  item.usageCosts = getItemUsageCosts(item, actor);
  _prepareEnhUsageCosts(item);
}

function _prepareEnhUsageCosts(item) {
  const enhancements = item.allEnhancements;
  if (!enhancements) return;

  enhancements.values().forEach(enh => {
    let counter = 0;
    counter += enh.resources.actionPoint || 0;
    counter += enh.resources.stamina || 0;
    counter += enh.resources.mana || 0;
    counter += enh.resources.health || 0;
    enh.enhCosts = counter;
  });
}

function prepareItemFormulas(item, actor) {
  let formulas = item.system.formulas;

  // If selected collect Used Weapon Enhancements 
  const usesWeapon = item.system.usesWeapon;
  if (usesWeapon?.weaponAttack) {
    const weapon = actor.items.get(usesWeapon.weaponId);
    if (weapon) {
      formulas = {
        ...formulas,
        ...weapon.system.formulas
      };
    }
  }
  
  if (!formulas) item.formulas = {};
  else item.formulas = formulas;
}

function _addItemToTable(item, headers, fallback) {
  const headerName = item.flags.dc20rpg.tableName;

  if (!headerName || !headers[headerName]) {
    if (headers[fallback]) headers[fallback].items[item.id] = item;
    else headers[item.type].items[item.id] = item;
  }
  else headers[headerName].items[item.id] = item;
}

function _filterItems(filter, items) {
  if (!filter) return items;
  
  const tableKeys = Object.keys(items);
  for (const table of tableKeys) {
    let itemEntries = Object.entries(items[table].items);
    itemEntries = itemEntries.filter(([key, item]) => item.name.toLowerCase().includes(filter.toLowerCase()));
    items[table].items = Object.fromEntries(itemEntries);
  }
  return items;
}

/**
 * Dialog window for rolling saves and check requested by the DM.
 */
class RollPromptDialog extends Dialog {

  constructor(actor, data, quickRoll, fromGmHelp, dialogData = {}, options = {}) {
    super(dialogData, options);
    this.actor = actor;
    // We want to clear effects to remove when we open new roll prompt
    actor.update({["flags.dc20rpg.effectsToRemoveAfterRoll"]: []}); 
    if (data.documentName === "Item") {
      this.itemRoll = true;
      this.item = data;
      this.menuOwner = this.item;
      if (!fromGmHelp) {
        this._prepareAttackRange();
        this._prepareHeldAction();
      } 
      this._prepareMeasurementTemplates();
    }
    else {
      this.itemRoll = false;
      this.details = data;
      this.menuOwner = this.actor;
    }
    this.promiseResolve = null;

    const autoRollLevelCheck = game.settings.get("dc20rpg", "autoRollLevelCheck");
    if (autoRollLevelCheck && !fromGmHelp) {
      this._rollRollLevelCheck(false, quickRoll);
    }
    else {
      this.rollLevelChecked = fromGmHelp;
      if (quickRoll) this._onRoll();
    }
  }

  static get defaultOptions() {
    return foundry.utils.mergeObject(super.defaultOptions, {
      classes: ["dc20rpg", "dialog"],
      width: 500
    });
  }

  /** @override */
  _getHeaderButtons() {
    const buttons = super._getHeaderButtons();
    if (!game.user.isGM && this.itemRoll) {
      buttons.unshift({
        label: "GM Help",
        class: "ask-gm-for-help",
        icon: "fas fa-handshake-angle",
        tooltip: "Ask GM for Help",
        onclick: () => this._onAskForHelp()
      });
    }
    return buttons;
  }

  _onAskForHelp() {
    const gm = game.users.activeGM;
    if (!gm) {
      ui.notifications.notify("No GM currently active");
      return;
    }

    emitSystemEvent("askGmForHelp", {
      actorId: this.actor._id,
      itemId: this.item._id,
      gmUserId: gm._id
    });
  }

  /** @override */
  get template() {
    const sheetType = this.itemRoll ? "item" : "sheet";
    return `systems/dc20rpg/templates/dialogs/roll-prompt/${sheetType}-roll-prompt.hbs`;
  }

  _prepareMeasurementTemplates() {
    const areas = this.item.system.target?.areas;
    if (!areas) return;
    const measurementTemplates = DC20RpgMeasuredTemplate.mapItemAreasToMeasuredTemplates(areas);
    if (Object.keys(measurementTemplates).length > 0) {
      this.measurementTemplates = measurementTemplates;
    }
  }

  async _prepareAttackRange() {
    let rangeType = false; 
    const system = this.item.system;
    if (system.actionType === "attack") rangeType = system.attackFormula.rangeType;
    this.item.flags.dc20rpg.rollMenu.rangeType = rangeType;
    await this.item.update({["flags.dc20rpg.rollMenu.rangeType"]: rangeType});
  }

  async _prepareHeldAction() {
    const actionHeld = this.actor.flags.dc20rpg.actionHeld;
    const rollsHeldAction = actionHeld?.rollsHeldAction;
    if (!rollsHeldAction) return;

    // Update enhancements
    const allEnhancements = this.item.allEnhancements;
    for (const [enhKey, enhNumber] of Object.entries(actionHeld.enhancements)) {
      const itemId = allEnhancements.get(enhKey).sourceItemId;
      const itemToUpdate = this.actor.items.get(itemId);
      if (itemToUpdate) await itemToUpdate.update({[`system.enhancements.${enhKey}.number`]: enhNumber});
    }

    // Update roll menu
    await this.item.update({["flags.dc20rpg.rollMenu"]: {
      apCost: actionHeld.apForAdv,
      adv: actionHeld.apForAdv
    }});
    this.render();
  }

  getData() {
    if (this.itemRoll) return this._getDataForItemRoll();
    else return this._getDataForSheetRoll();
  }

  _getDataForSheetRoll() {
    return {
      rollDetails: this.details,
      ...this.actor,
      itemRoll: this.itemRoll,
      rollLevelChecked: this.rollLevelChecked
    };
  }

  _getDataForItemRoll() {
    const itemRollDetails = {
      label: `Roll Item: ${this.item.name}`,
    };

    prepareItemFormulas(this.item, this.actor);
    const [expectedCosts, expectedCharges, chargesFromOtherItems] = collectExpectedUsageCost(this.actor, this.item);
    if (expectedCosts.actionPoint === 0) expectedCosts.actionPoint = undefined;
    const rollsHeldAction = this.actor.flags.dc20rpg.actionHeld?.rollsHeldAction;
    return {
      rollDetails: itemRollDetails,
      item: this.item,
      itemRoll: this.itemRoll,
      expectedCosts: expectedCosts,
      expectedCharges: expectedCharges,
      chargesFromOtherItems: chargesFromOtherItems,
      otherItemUse: this._prepareOtherItemUse(),
      enhancements: mapToObject(this.item.allEnhancements),
      rollsHeldAction: rollsHeldAction,
      rollLevelChecked: this.rollLevelChecked,
      measurementTemplates: this.measurementTemplates
    };
  }

  _prepareOtherItemUse() {
    const otherItemUse = this.item.system?.costs?.otherItem;
    const otherItem = this.actor.items.get(otherItemUse?.itemId);
    if (otherItem && otherItemUse.amountConsumed > 0) {
      const use = {
        name: otherItem.name,
        image: otherItem.img,
        amount: otherItemUse.amountConsumed,
        consumeCharge: otherItemUse.consumeCharge
      };
      return use;
    }
    return null
  }

  /** @override */
  activateListeners(html) {
    super.activateListeners(html);
    html.find('.held-action').click(ev => this._onHeldAction(ev));
    html.find('.rollable').click(ev => this._onRoll(ev));
    html.find('.roll-level-check').click(ev => this._onRollLevelCheck(ev));
    html.find('.last-roll-level-check').click(ev => this._displayRollLevelCheckResult());
    html.find('.roll-range').click(() => this._onRangeChange());
    html.find('.ap-for-adv').mousedown(async ev => {
      await advForApChange(this.menuOwner, ev.which);
      this.render();
    });
    html.find('.grit-for-adv').mousedown(async ev => {
      await advForGritChange(this.menuOwner, ev.which);
      this.render();
    });
    html.find('.toggle-numeric').mousedown(async ev => {
      await toggleUpOrDown(datasetOf(ev).path, ev.which, this.menuOwner, 9, 0);
      this.render();
    });
    html.find('.toggle-numeric-minus').mousedown(async ev => {
      await toggleUpOrDown(datasetOf(ev).path, ev.which, this.menuOwner, 9, -9);
      this.render();
    });
    html.find(".item-activable").click(async ev => {
      await changeActivableProperty(datasetOf(ev).path, this.item);
      this.render();
    });
    html.find(".activable").click(async ev => {
      await changeActivableProperty(datasetOf(ev).path, this.actor);
      this.render();
    });
    html.find('.reload-weapon').click(async () => {
      await reloadWeapon(this.item, this.actor);
      this.render();
    });
    html.find('.enh-use-number').mousedown(async ev => {
      await toggleUpOrDown(datasetOf(ev).path, ev.which, this._getItem(datasetOf(ev).itemId), 9, 0);
      const autoRollLevelCheck = game.settings.get("dc20rpg", "autoRollLevelCheck");
      if (autoRollLevelCheck && datasetOf(ev).runCheck === "true") this._rollRollLevelCheck(false);
      this.render();
    });
    html.find('.enh-tooltip').hover(ev => enhTooltip(this._getItem(datasetOf(ev).itemId), datasetOf(ev).enhKey, ev, html), ev => hideTooltip(ev, html));
    html.find('.item-tooltip').hover(ev => itemTooltip(this._getItem(datasetOf(ev).itemId), ev, html), ev => hideTooltip(ev, html));
    html.find('.create-template').click(ev => this._onCreateMeasuredTemplate(datasetOf(ev).key));
    html.find('.add-template-space').click(ev => this._onAddTemplateSpace(datasetOf(ev).key));
    html.find('.reduce-template-space').click(ev => this._onReduceTemplateSpace(datasetOf(ev).key));
  }

  _getItem(itemId) {
    let item = this.item;
    if (itemId !== this.item._id) item = getItemFromActor(itemId, this.actor);
    return item;
  }

  async _onRangeChange() {
    const current = this.item.flags.dc20rpg.rollMenu.rangeType;
    let newRange = current === "melee" ? "ranged" : "melee";
    await this.item.update({["flags.dc20rpg.rollMenu.rangeType"]: newRange});
    const autoRollLevelCheck = game.settings.get("dc20rpg", "autoRollLevelCheck");
    if (autoRollLevelCheck) this._rollRollLevelCheck(false);
    else this.render();
  }

  _onHeldAction(event) {
    event.preventDefault();
    if (!this.itemRoll) return;
    heldAction(this.item, this.actor);
    this.promiseResolve(null);
    this.close();
  }

  async _onRoll(event) {
    if(event) event.preventDefault();
    let roll = null;
    const rollMenu = this.menuOwner.flags.dc20rpg.rollMenu;
    if (this.itemRoll) {
      roll = await rollFromItem(this.item._id, this.actor);
    }
    else if (subtractAP(this.actor, rollMenu.apCost) && subtractGrit(this.actor, rollMenu.gritCost)) {
      roll = await rollFromSheet(this.actor, this.details);
    }
    this.promiseResolve(roll);
    this.close();
  }

  async _onRollLevelCheck(event) {
    event.preventDefault();
    this._rollRollLevelCheck(true);
  }

  _displayRollLevelCheckResult(result) {
    if (result) return getSimplePopup("info", {information: result, header: "Expected Roll Level"});
    if (this.rollLevelCheckResult) return getSimplePopup("info", {information: this.rollLevelCheckResult, header: "Expected Roll Level"})
  }

  async _rollRollLevelCheck(display, quickRoll) {
    this.rollLevelChecked = true;
    let result = [];
    if (this.itemRoll) result = await runItemRollLevelCheck(this.item, this.actor);
    else result = await runSheetRollLevelCheck(this.details, this.actor);

    if (quickRoll) return this._onRoll();
    
    if (result[result.length -1] === "FORCE_DISPLAY") {
      result.pop();
      display = true; // For manual actions we always want to display this popup
    }

    if (display) this._displayRollLevelCheckResult(result);
    this.rollLevelCheckResult = result;
    this.render();
  }

    async _onCreateMeasuredTemplate(key) {
      const template = this.measurementTemplates[key];
      if (!template) return;
  
      const applyEffects = getMesuredTemplateEffects(this.item);
      const itemData = {itemId: this.item.id, actorId: this.actor.id, tokenId: this.actor.token?.id, applyEffects: applyEffects};
      const measuredTemplates = await DC20RpgMeasuredTemplate.createMeasuredTemplates(template, () => this.render(), itemData);

      // We will skip Target Selector if we are using selector for applying effects
      if (applyEffects.applyFor === "selector") return;

      let tokens = {};
      for (let i = 0; i < measuredTemplates.length; i++) {
        const collectedTokens = getTokensInsideMeasurementTemplate(measuredTemplates[i]);
        tokens = {
          ...tokens,
          ...collectedTokens
        };
      }
      
      if (Object.keys(tokens).length > 0) tokens = await getTokenSelector(tokens, "Select Targets");
      if (tokens.length > 0) {
        const user = game.user;
        if (!user) return;

        user.targets.forEach(target => {
          target.setTarget(false, { user: user });
        });

        for (const token of tokens) {
          token.setTarget(true, { user: user, releaseOthers: false });
        }

        const autoRollLevelCheck = game.settings.get("dc20rpg", "autoRollLevelCheck");
        if (autoRollLevelCheck) this._rollRollLevelCheck(false);
      }
    }
  
    _onAddTemplateSpace(key) {
      const template = this.measurementTemplates[key];
      if (!template) return;
      DC20RpgMeasuredTemplate.changeTemplateSpaces(template, 1);
      this.render();
    }
  
    _onReduceTemplateSpace(key) {
      const template = this.measurementTemplates[key];
      if (!template) return;
      DC20RpgMeasuredTemplate.changeTemplateSpaces(template, -1);
      this.render();
    }

  static async create(actor, data, quickRoll, fromGmHelp, dialogData = {}, options = {}) {
    const prompt = new RollPromptDialog(actor, data, quickRoll, fromGmHelp, dialogData, options);
    return new Promise((resolve) => {
      prompt.promiseResolve = resolve;
      if (!quickRoll) prompt.render(true); // We dont want to render dialog for auto rolls
    });
  }

  /** @override */
  close(options) {
    if (this.promiseResolve) this.promiseResolve(null);
    super.close(options);
  }

  render(force=false, options={}) {
    super.render(force, options);

    if (!options.dontEmit) {
      // Emit event to refresh roll prompts
      const payload = {
        itemId: this.item?.id,
        actorId: this.actor?.id
      };
      emitSystemEvent("rollPromptRerendered", payload);
    }
  }
}

/**
 * Creates Roll Request dialog for player that triggers it.
 * This one is being used for non-item rolls.
 */
async function promptRoll(actor, details, quickRoll=false, fromGmHelp=false) {
  return await RollPromptDialog.create(actor, details, quickRoll, fromGmHelp, {title: `Roll ${details.label}`});
}

/**
 * Creates Roll Request dialog for player that triggers it.
 * This one is being used for item rolls.
 */
async function promptItemRoll(actor, item, quickRoll=false, fromGmHelp=false) {
  await runTemporaryItemMacro(item, "onRollPrompt", actor);
  const quick = quickRoll || item.system.quickRoll;
  return await RollPromptDialog.create(actor, item, quick, fromGmHelp, {title: `Roll ${item.name}`})
}

/**
 * Creates Roll Request dialog for all owners of given actor.
 * If there are multiple owners, dialog will be created for each but only the first response will be considered.
 * If there is no active owner it will behave the same as promptRoll method.
 */
async function promptRollToOtherPlayer(actor, details, waitForRoll = true, quickRoll=false) {

  // If there is no active actor owner DM will make a roll
  if (_noUserToRoll(actor)) {
    if (waitForRoll) {
      return await promptRoll(actor, details, quickRoll, false);
    }
    else {
      promptRoll(actor, details, quickRoll, false);
      return;
    }
  }

  const payload = {
    actorId: actor.id,
    details: details,
    isToken: actor.isToken
  };
  if (actor.isToken) payload.tokenId = actor.token.id;

  if (waitForRoll) {
    const validationData = {emmiterId: game.user.id, actorId: actor.id};
    const rollPromise = responseListener("rollPromptResult", validationData);
    emitSystemEvent("rollPrompt", payload);
    const roll = await rollPromise;
    return roll;
  }
  else {
    emitSystemEvent("rollPrompt", payload);
    return;
  }
}

function _noUserToRoll(actor) {
  const owners = Object.entries(actor.ownership)
    .filter(([ownerId, ownType]) => ownerId !== game.user.id)
    .filter(([ownerId, ownType]) => ownerId !== "default")
    .filter(([ownerId, ownType]) => ownType === 3);

  let noUserToRoll = true;
  owners.forEach(ownership => {
    const ownerId = ownership[0];
    const owner = game.users.get(ownerId);
    if (owner && owner.active) noUserToRoll = false;
  });
  return noUserToRoll;
}

async function addBasicActions(actor) {
  const actionsData = [];
  for (const [key, uuid] of Object.entries(CONFIG.DC20RPG.SYSTEM_CONSTANTS.JOURNAL_UUID.basicActionsItems)) {
    const action = await fromUuid(uuid);
    const data = action.toObject();
    data.flags.dc20BasicActionsSource = uuid;
    data.flags.dc20BasicActionKey = key;
    actionsData.push(data);
  }

  if (actor.type === "character") {
    const uuid = CONFIG.DC20RPG.SYSTEM_CONSTANTS.JOURNAL_UUID.unarmedStrike;
    const action = await fromUuid(uuid);
    const data = action.toObject();
    data.flags.dc20BasicActionsSource = uuid;
    data.flags.dc20BasicActionKey = "unarmedStrike";
    actionsData.push(data);
  }
  await actor.createEmbeddedDocuments("Item", actionsData);
  await actor.update({["flags.basicActionsAdded"]: true});
}

//===================================
//            HELP ACTION           =
//===================================
/**
 * Performs a help action for the actor. 
 * "options" - all are optional: {
 *  "diceValue": Number - value on a dice (ex 8). If provided MHP will also be skipped.
 *  "ignoreMHP": Boolean - If provided MHP will be skipped.
 *  "subtract": Boolean - If provided help dice will be subtracted from the roll instead.
 *  "doNotExpire": Boolean - If provided help dice wont expire at the start of actor's next turn.
 * }
 */
function prepareHelpAction(actor, options) {
  const activeDice = actor.system.help.active; 
  let maxDice = actor.system.help.maxDice;
  if (options.diceValue) maxDice = options.diceValue;
  else if (actor.inCombat && !options.ignoreMHP) {
    maxDice = Math.max(applyMultipleHelpPenalty(actor, maxDice), 4); 
  }
  const subtract = options.subtract ? "-" : "";
  activeDice[generateKey()] = {
    value: `${subtract}d${maxDice}`,
    doNotExpire: options.doNotExpire
  };
  actor.update({["system.help.active"]: activeDice});
}

async function clearHelpDice(actor, key) {
  if (key) {
    await actor.update({[`system.help.active.-=${key}`]: null});
  }
  else {
    for (const [key, help] of Object.entries(actor.system.help.active)) {
      if (!help.doNotExpire) await actor.update({[`system.help.active.-=${key}`]: null});
    }
  }
}

//===================================
//            MOVE ACTION           =
//===================================
/**
 * Performs a move action for the actor. 
 * "options" - all are optional: {
 *  "movePoints": String - specific number of move points gained
 *  "moveType": String - specific movement type (ex. ground)
 * }
 */
async function makeMoveAction(actor, options={}) {
  const movePointsUseOption = game.settings.get("dc20rpg", "useMovementPoints");
  if (movePointsUseOption === "never") return; // We dont care about move points
  
  let movePoints = options.movePoints;
  if (!movePoints) {
    const moveKey = options.moveType || "ground";
    movePoints = actor.system.movement[moveKey].current;
  }

  const currentMovePoints = actor.system.movePoints || 0;
  const newMovePoints = currentMovePoints + movePoints;
  await actor.update({["system.movePoints"]: newMovePoints});
}

async function clearMovePoints(actor) {
  await actor.update({["system.movePoints"]: 0});
}

async function subtractMovePoints(tokenDoc, amount, options) {
  const movePointsUseOption = game.settings.get("dc20rpg", "useMovementPoints");
  const onTurn = movePointsUseOption === "onTurn";
  const onCombat = movePointsUseOption === "onCombat";
  const never = movePointsUseOption === "never";

  if (never) return true; 

  if (onCombat || onTurn) {
    const activeCombat = game.combats.active;
    if (!activeCombat?.started) return true;
    if (onTurn && !_tokensTurn(tokenDoc, activeCombat)) return true;
  }
  const movePoints = tokenDoc.actor.system.movePoints;
  const newMovePoints = options.isUndo ? movePoints + amount : movePoints - amount;
  if (newMovePoints < -0.1) return Math.abs(newMovePoints);

  await tokenDoc.actor.update({["system.movePoints"]: roundFloat(newMovePoints)});
  return true;
}

function _tokensTurn(tokenDoc, activeCombat) {
  const combatantId = activeCombat.current.combatantId;
  const combatant = activeCombat.combatants.get(combatantId);

  const tokensTurn = combatant?.tokenId === tokenDoc.id;
  if (tokensTurn) return true;

  if (companionShare(tokenDoc.actor, "initiative")) {
    const ownerTurn = combatant?.actorId === tokenDoc.actor.companionOwner.id;
    if (ownerTurn) return true;
  }
  return false;
}

async function spendMoreApOnMovement(actor, missingMovePoints) {
  let moveKey = "ground";
  if (actor.hasOtherMoveOptions) {
    moveKey = await game.dc20rpg.tools.getSimplePopup("select", {selectOptions: CONFIG.DC20RPG.DROPDOWN_DATA.moveTypes, header: game.i18n.localize("dc20rpg.dialog.movementType.title"), preselect: "ground"});
    if (!moveKey) return missingMovePoints;
  }

  const movePoints = actor.system.movement[moveKey].current;
  if (movePoints <= 0) return missingMovePoints; // We need to avoid infinite loops

  let apSpend = 0;
  let movePointsGained = 0;
  while ((missingMovePoints - movePointsGained) > 0) {
    apSpend++;
    movePointsGained += movePoints;
  }
  const movePointsLeft = Math.abs(missingMovePoints - movePointsGained);
  const proceed = await getSimplePopup("confirm", {header: `You need to spend ${apSpend} AP to make this move. After that you will have ${roundFloat(movePointsLeft)} Move Points left. Proceed?`});
  if (proceed && subtractAP(actor, apSpend)) {
    await actor.update({["system.movePoints"]: roundFloat(movePointsLeft)});
    return true;
  }
  return missingMovePoints;
}

function snapTokenToTheClosetPosition(tokenDoc, missingMovePoints, startPosition, endPosition, costFunctionGridless, costFunctionGrid) {
  if (tokenDoc.actor.system.movePoints <= 0) return [missingMovePoints, endPosition];
  if (canvas.grid.isGridless) return _snapTokenGridless(tokenDoc, startPosition, endPosition, costFunctionGridless);
  else return _snapTokenGrid(tokenDoc, startPosition, endPosition, costFunctionGrid);
}

function _snapTokenGrid(tokenDoc, startPosition, endPosition, costFunctionGrid) {
  const disableDifficultTerrain = game.settings.get("dc20rpg", "disableDifficultTerrain");
  const ignoreDifficultTerrain = tokenDoc.actor.system.globalModifier.ignore.difficultTerrain;
  const ignoreDT = disableDifficultTerrain || ignoreDifficultTerrain;
  const movementData = {
    moveCost: tokenDoc.actor.system.moveCost,
    ignoreDT: ignoreDT,
    lastDifficultTerrainSpaces: 0
  };

  const occupiedSpaces = tokenDoc.object.getOccupiedGridSpaces();
  const cords = canvas.grid.getDirectPath([startPosition, endPosition]);
  let movePointsLeft = tokenDoc.actor.system.movePoints;
  let numberOfCordsToStay = 1;
  for (let i = 1; i < cords.length-1; i++) {
    const singleSquareCost = costFunctionGrid(cords[i-1], cords[i], 1, movementData, occupiedSpaces);
    if (singleSquareCost <= movePointsLeft) {
      movePointsLeft = movePointsLeft - singleSquareCost;
      numberOfCordsToStay ++;
    }
    else break;
  }
  const cordsToRemove = cords.length-numberOfCordsToStay;
  for (let i = 0; i < cordsToRemove; i++) cords.pop();

  const lastPoint = cords[cords.length - 1];
  const centered = canvas.grid.getCenterPoint(lastPoint);
  const newEndPosition = tokenDoc.object.getSnappedPosition(centered);
  endPosition.x = newEndPosition.x;
  endPosition.y = newEndPosition.y;
  tokenDoc.actor.update({["system.movePoints"]: Math.abs(movePointsLeft)});
  ui.notifications.info("You don't have enough Move Points to travel full distance - snapped to the closest available position");
  return [true, endPosition];
}

function _snapTokenGridless(tokenDoc, startPosition, endPosition, costFunctionGridless) {
  const disableDifficultTerrain = game.settings.get("dc20rpg", "disableDifficultTerrain");
  const ignoreDifficultTerrain = tokenDoc.actor.system.globalModifier.ignore.difficultTerrain;
  const ignoreDT = disableDifficultTerrain || ignoreDifficultTerrain;
  const movementData = {
    moveCost: tokenDoc.actor.system.moveCost,
    ignoreDT: ignoreDT
  };
  
  const travelPoints = getPointsOnLine(startPosition.x, startPosition.y, endPosition.x, endPosition.y, canvas.grid.size);
  travelPoints.push({x: endPosition.x, y: endPosition.y});
  const from = {i: travelPoints[0].y, j: travelPoints[0].x};
  const movePointsToSpend = tokenDoc.actor.system.movePoints;
  endPosition.x = startPosition.x;
  endPosition.y = startPosition.y;
  let movePointsLeft = movePointsToSpend;
  for (let i = 1; i < travelPoints.length ; i++) {
    const to = {i: travelPoints[i].y, j: travelPoints[i].x};
    const distance = roundFloat(canvas.grid.measurePath([travelPoints[0], travelPoints[i]]).distance);
    const travelCost = costFunctionGridless(from, to, distance, movementData, tokenDoc.width);

    if (travelCost <= movePointsToSpend) {
      movePointsLeft = movePointsToSpend - travelCost;
      endPosition.x = travelPoints[i].x;
      endPosition.y = travelPoints[i].y;
    }
    else break;
  }
  tokenDoc.actor.update({["system.movePoints"]: roundFloat(movePointsLeft)});
  ui.notifications.info("You don't have enough Move Points to travel full distance - snapped to the closest available position");
  return [true, endPosition];
}

//===================================
//            HELD ACTION           =
//===================================
function heldAction(item, actor) {
  const apCost = collectExpectedUsageCost(actor, item)[0].actionPoint;
  if (!subtractAP(actor, apCost)) return;

  const rollMenu = item.flags.dc20rpg.rollMenu;
  const enhancements = {};
  item.allEnhancements.entries().forEach(([key, enh]) => enhancements[key] = enh.number);
  const actionHeld = {
    isHeld: true,
    itemId: item.id,
    itemImg: item.img,
    enhancements: enhancements,
    mcp: null,
    apForAdv: rollMenu.apCost,
    rollsHeldAction: false
  };
  actor.update({["flags.dc20rpg.actionHeld"]: actionHeld});
  resetEnhancements(item, actor);
  resetRollMenu(rollMenu, item);
}

async function triggerHeldAction(actor) {
  const actionHeld = actor.flags.dc20rpg.actionHeld;
  if (!actionHeld.isHeld) return;

  const item = actor.items.get(actionHeld.itemId);
  if (!item) return;
  
  await actor.update({["flags.dc20rpg.actionHeld.rollsHeldAction"]: true});
  const result = await promptItemRoll(actor, item);
  await actor.update({["flags.dc20rpg.actionHeld.rollsHeldAction"]: false});
  if (!result) return;
  clearHeldAction(actor);
}

function clearHeldAction(actor) {
  const clearActionHeld = {
    isHeld: false,
    itemId: null,
    itemImg: null,
    enhancements: null,
    mcp: null,
    apForAdv: 0,
    rollsHeldAction: false
  };
  actor.update({["flags.dc20rpg.actionHeld"]: clearActionHeld});
}

function makeCalculations$1(actor) {
	_skillModifiers(actor);
	_specialRollTypes(actor);
	_maxHp(actor);

	if (actor.type === "character") {
		_maxMana(actor);
		_maxStamina(actor);
		_maxGrit(actor);
		_maxRestPoints(actor);

		_skillPoints(actor);
		_attributePoints(actor);
		_spellsAndTechniquesKnown(actor);
		_weaponStyles(actor);
	}
	if (actor.type === "companion") {
		_actionPoints(actor);
	}
	_currentHp(actor);

	_senses(actor);
	_movement(actor);
	_jump(actor);

	_precisionDefence(actor);
	_areaDefence(actor);
	_damageReduction$1(actor);
	_deathsDoor$1(actor);
	_basicConditionals(actor);
}

function _skillModifiers(actor) {
	const exhaustion = actor.exhaustion;
	const attributes = actor.system.attributes;
	const expertise = new Set([...actor.system.expertise.automated, ...actor.system.expertise.manual]);

	// Calculate skills modifiers
	const overrideMasteryWithOwner = companionShare(actor, "skills");
	for (let [key, skill] of Object.entries(actor.system.skills)) {
		if (overrideMasteryWithOwner) {
			skill.mastery = actor.companionOwner.system.skills[key].mastery;
		}
		skill.modifier = attributes[skill.baseAttribute].value + (2 * skill.mastery) + skill.bonus - exhaustion;
		if (expertise.has(key)) skill.expertise = true;
	}

	// Calculate trade skill modifiers
	if (actor.type === "character") {
		for (let [key, skill] of Object.entries(actor.system.tradeSkills)) {
			skill.modifier = attributes[skill.baseAttribute].value + (2 * skill.mastery) + skill.bonus - exhaustion;
			if (expertise.has(key)) skill.expertise = true;
		}
	}
}

function _specialRollTypes(actor) {
	const special = {};
	const data = actor.system;

	// Physical Save
	const mig = data.attributes.mig;
	const agi = data.attributes.agi;
	special.phySave = Math.max(mig.save, agi.save);
	
	// Mental Save
	const int = data.attributes.int;
	const cha = data.attributes.cha;
	special.menSave = Math.max(int.save, cha.save);

	// Martial Check
	const acr = data.skills.acr;
	const ath = data.skills.ath;
	if (acr && ath) special.marCheck = Math.max(acr.modifier, ath.modifier);

	// Initiative Check
	const CM = actor.system.details.combatMastery;
	special.initiative = agi.check + CM;

	data.special = special;
}

function _actionPoints(actor) {
	if (companionShare(actor, "ap")) {
		actor.system.resources.ap = actor.companionOwner.system.resources.ap;
	}
}

function _maxHp(actor) {
	const details = actor.system.details;
	const health = actor.system.resources.health;
	const might = actor.system.attributes.mig.value;
	const hpFromClass = details.class?.maxHpBonus || 6;
	
	if (health.useFlat) return;
	else {
		health.max = hpFromClass + might + health.bonus;
	}
}

function _maxMana(actor) {
	const mana = actor.system.resources.mana;
	mana.max = evaluateDicelessFormula(mana.maxFormula, actor.getRollData()).total;
}

function _maxStamina(actor) {
	const stamina = actor.system.resources.stamina;
	stamina.max = evaluateDicelessFormula(stamina.maxFormula, actor.getRollData()).total;
}

function _maxGrit(actor) {
	const grit = actor.system.resources.grit;
	grit.max = evaluateDicelessFormula(grit.maxFormula, actor.getRollData()).total;
}

function _maxRestPoints(actor) {
	actor.system.resources.restPoints.max =  actor.system.resources.health.max;
}

function _skillPoints(actor) {
	const int = actor.system.attributes.int.value;
	const spentPoints = _collectSpentPoints(actor);
	Object.entries(actor.system.skillPoints).forEach(([key, type]) => {
		if (key === "skill") type.max += int;
		type.max += type.extra + type.bonus;
		type.spent += spentPoints[key] + type.converted;
		type.left = type.max - type.spent;
	});
}

function _attributePoints(actor) {
	const attributePoints = actor.system.attributePoints;
	if (attributePoints.override) attributePoints.max = attributePoints.overridenMax;
	attributePoints.max += attributePoints.extra + attributePoints.bonus;
	Object.entries(actor.system.attributes)
						.filter(([key, atr]) => key !== "prime")
						.forEach(([key, atr]) => {
								attributePoints.spent += atr.current +2;
							// players start with -2 in the attribute and spend points from there
						});
	attributePoints.left = attributePoints.max - attributePoints.spent;
}

function _spellsAndTechniquesKnown(actor) {
	const items = actor.items;
	if (items.size <= 0) return;

	const known = actor.system.known;
	const maxCantrips = known.cantrips.max;
	let spells = 0;
	let cantrips = 0;
	let maneuvers = 0;
	let techniques = 0;
	actor.items
		.filter(item => item.system.knownLimit)
		.forEach(item => {
			if (item.type === "technique") {
				if (item.system.techniqueType === "maneuver") maneuvers++;
				else techniques++;
			}
			else if (item.type === "spell") {
				if (item.system.spellType === "cantrip" && cantrips < maxCantrips) cantrips++;
				else spells++;
			}
		});

	known.spells.current = spells;
	known.cantrips.current = cantrips;
	known.maneuvers.current = maneuvers;
	known.techniques.current = techniques;
}

function _collectSpentPoints(actor) {
	const actorSkills = actor.system.skills;
	const actorTrades = actor.system.tradeSkills;
	const actorLanguages = actor.system.languages;
	const manualExpertise = new Set(actor.system.expertise.manual);
	const collected = {
		skill: 0,
		trade: 0,
		language: 0
	};

	// We need to collect skills and expertise (but only from manual)
	Object.entries(actorSkills)
		.forEach(([key, skill]) => {
			collected.skill += skill.mastery;
			if (manualExpertise.has(key)) collected.skill++;
		});

	Object.entries(actorTrades)
		.forEach(([key, trade]) => {
			collected.trade += trade.mastery;
			if (manualExpertise.has(key)) collected.trade++;
		});

	Object.entries(actorLanguages)
		.filter(([key, lang]) => key !== "com")
		.forEach(([key, lang]) => collected.language += lang.mastery);

	return collected;
}

function _currentHp(actor) {
	if (companionShare(actor, "health")) {
		actor.system.resources.health = actor.companionOwner.system.resources.health;
	}
	else {
		const health = actor.system.resources.health;
		if (health.current > health.max) health.current = health.max;
		health.value = health.current + health.temp;
	}
}

function _senses(actor) {
	const sensesTypes = actor.system.senses;

	for (const sense of Object.values(sensesTypes)) {
		let range = sense.override ? sense.overridenRange : sense.range;
		let bonus = sense.bonus;

		// We need to deal with effects like Subterranean Favorite Terrain feature for Ranger
		if (range > 0) bonus += sense.orOption.bonus;
		else range = sense.orOption.range;
		
		sense.value = range + bonus;
	}
}

function _movement(actor) {
	const exhaustion = actor.exhaustion;
	const movements = actor.system.movement;

	const groundSpeed = companionShare(actor, "speed")
												? actor.companionOwner.system.movement.ground.current - exhaustion
												: movements.ground.value + movements.ground.bonus - exhaustion;
	movements.ground.current = groundSpeed > 0 ? groundSpeed : 0;
	for (const [key, movement] of Object.entries(movements)) {
		if (key === "ground") continue;
		
		if (movement.useCustom) {
			const speed = movement.value + movement.bonus - exhaustion;
			movement.current = speed > 0 ? speed : 0;
		}
		else {
			if (movement.fullSpeed) movement.current = groundSpeed + movement.bonus;
			else if (movement.halfSpeed) movement.current = Math.ceil(groundSpeed/2) + movement.bonus;
			else {
				const speed = movement.bonus - exhaustion;
				movement.current = speed > 0 ? speed : 0;
			}
		}
	}
}

function _jump(actor) {
	const jump = actor.system.jump;
	if (jump.key === "flat") {
		jump.current = jump.value + jump.bonus;
	}
	else {
		const attribute = actor.system.attributes[jump.key].value;
		jump.current = (attribute >= 1 ? attribute : 1) + jump.bonus;
	}
}

function _precisionDefence(actor) {
	const pd = actor.system.defences.precision;
	if (companionShare(actor, "defences.precision")) {
		pd.normal = actor.companionOwner.system.defences.precision.value;
	}
	else if (pd.formulaKey !== "flat") {
		const formula = pd.formulaKey === "custom" ? pd.customFormula : CONFIG.DC20RPG.SYSTEM_CONSTANTS.precisionDefenceFormulas[pd.formulaKey];
		pd.normal = evaluateDicelessFormula(formula, actor.getRollData()).total;
	}

	// Add bonueses to defence deppending on equipped armor
	const details = actor.system.details.armor;
	let bonus = pd.bonuses.always;
	if (!details.armorEquipped) bonus += pd.bonuses.noArmor;
	if (!details.heavyEquipped) bonus += pd.bonuses.noHeavy;
	pd.bonuses.final = bonus;
	
	// Calculate Hit Thresholds
	pd.value = pd.normal + bonus;
	pd.heavy = pd.value + 5;
	pd.brutal = pd.value + 10;
}

function _areaDefence(actor) {
	const ad = actor.system.defences.area;
	if (companionShare(actor, "defences.area")) {
		ad.normal = actor.companionOwner.system.defences.area.value;
	}
	else if (ad.formulaKey !== "flat") {
		const formula = ad.formulaKey === "custom" ? ad.customFormula : CONFIG.DC20RPG.SYSTEM_CONSTANTS.areaDefenceFormulas[ad.formulaKey];
		ad.normal = evaluateDicelessFormula(formula, actor.getRollData()).total;
	}

	// Add bonueses to defence deppending on equipped armor
	const details = actor.system.details.armor;
	let bonus = ad.bonuses.always;
	if (!details.armorEquipped) bonus += ad.bonuses.noArmor;
	if (!details.heavyEquipped) bonus += ad.bonuses.noHeavy;
	ad.bonuses.final = bonus;
	
	// Calculate Hit Thresholds
	ad.value = ad.normal + bonus;
	ad.heavy = ad.value + 5;
	ad.brutal = ad.value + 10;
}

function _damageReduction$1(actor) {
	if (companionShare(actor, "damageReduction.pdr")) actor.system.damageReduction.pdr.active = actor.companionOwner.system.damageReduction.pdr.active;
	if (companionShare(actor, "damageReduction.mdr")) actor.system.damageReduction.mdr.active = actor.companionOwner.system.damageReduction.mdr.active;
	if (companionShare(actor, "damageReduction.edr")) actor.system.damageReduction.edr.active = actor.companionOwner.system.damageReduction.edr.active;
}

function _deathsDoor$1(actor) {
	const death = actor.system.death;
	const currentHp = actor.system.resources.health.current;
	const prime = actor.system.attributes.prime.value;
	const combatMastery = actor.system.details.combatMastery;

	const treshold = -prime - combatMastery - death.bonus;
	death.treshold = treshold < 0 ? treshold : 0;
	if (currentHp <= 0) death.active = true;
	else death.active = false;
}

function _basicConditionals(actor) {
	// Impact property
	actor.system.conditionals.push({
		condition: `hit >= 5`, 
		bonus: '1', 
		useFor: `system.properties.impact.active=[true]`, 
		name: "Impact",
		linkWithToggle: false,
		flags: {
			ignorePdr: false,
			ignoreEdr: false,
			ignoreMdr: false,
			ignoreResistance: {},
			ignoreImmune: {}
		},
		effect: null,
		addsNewRollRequest: false,
    rollRequest: {
      category: "",
      saveKey: "",
      contestedKey: "",
      dcCalculation: "",
      dc: 0,
      addMasteryToDC: true,
      respectSizeRules: false,
    },
	});

	// Impactful Unarmed Strikes
	if (actor.system.details.armor.heavyEquipped) {
		actor.system.conditionals.push({
			condition: `hit >= 5`, 
			bonus: '1', 
			useFor: `system.itemKey=["unarmedStrike"]`, 
			name: "Impactful Unarmed Strikes",
			linkWithToggle: false,
			flags: {
				ignorePdr: false,
				ignoreEdr: false,
				ignoreMdr: false,
				ignoreResistance: {},
				ignoreImmune: {}
			},
			effect: null,
			addsNewRollRequest: false,
			rollRequest: {
				category: "",
				saveKey: "",
				contestedKey: "",
				dcCalculation: "",
				dc: 0,
				addMasteryToDC: true,
				respectSizeRules: false,
			},
		});
	}
}

function _weaponStyles(actor) {
	const conditionals = [
		_conditionBuilder("axe", '["bleeding"]'),
		_conditionBuilder("bow", '["slowed"]'),
		_conditionBuilder("fist", '["grappled"]'),
		_conditionBuilder("hammer", '["dazed", "petrified"]'),
		_conditionBuilder("pick", '["impaired"]'),
		_conditionBuilder("staff", '["hindered"]'),
		_conditionBuilder("sword", '["exposed"]'),
	];
	conditionals.forEach(conditional => actor.system.conditionals.push(conditional));
}

function _conditionBuilder(weaponStyle, conditions) {
	const weaponStyleLabel = getLabelFromKey(weaponStyle, CONFIG.DC20RPG.DROPDOWN_DATA.weaponStyles);
	return {
		condition: `target.hasAnyCondition(${conditions})`, 
		bonus: '1', 
		useFor: `system.weaponStyle=["${weaponStyle}"]&&system.weaponStyleActive=[${true}]`, 
		name: `${weaponStyleLabel} Passive`,
		linkWithToggle: false,
		flags: {
			ignorePdr: false,
			ignoreEdr: false,
			ignoreMdr: false,
			ignoreResistance: {},
			ignoreImmune: {}
		},
		effect: null,
		addsNewRollRequest: false,
    rollRequest: {
      category: "",
      saveKey: "",
      contestedKey: "",
      dcCalculation: "",
      dc: 0,
      addMasteryToDC: true,
      respectSizeRules: false,
    },
	}
}

/**
 * Copies some data from actor's items to make it easier to access it later.
 */
function prepareDataFromItems(actor) {
	const weapon = [];
	const equipment = [];
	const customResources = []; 
	const conditionals = [];
	const itemsWithEnhancementsToCopy = [];

	actor.items.forEach(item => {
		// Inventory
		if (item.type === 'weapon') weapon.push(item);
		if (item.type === 'equipment') equipment.push(item);

		// Custom Resources
		if (item.system.isResource) customResources.push(item);

		// Conditionals
		const conds = item.system.conditionals;
		if (conds && Object.keys(conds).length > 0) conditionals.push(item);

		// Copies Enhacements - we only need those for reference when we run our checks on new item creation/edit
		if (item.system.copyEnhancements?.copy) itemsWithEnhancementsToCopy.push({
			itemId: item.id,
			copyFor: item.system.copyEnhancements.copyFor
		});
	});

	_weapon(weapon, actor);
	_equipment(equipment, actor);
	_customResources(customResources, actor);
	_conditionals(conditionals, actor);
	actor.itemsWithEnhancementsToCopy = itemsWithEnhancementsToCopy;
}

function prepareUniqueItemData(actor) {
	if (actor.type === "character") {
		_background(actor);
		_class(actor);
		_ancestry(actor);
		_subclass(actor);
	}
}

function prepareEquippedItemsFlags(actor) {
	const equippedFlags = {
		armorEquipped: false,
		heavyEquipped: false,
	};

	actor.items.forEach(item => {
		if (item.type === 'equipment' && item.system.statuses.equipped) {
			if (["light", "heavy"].includes(item.system.equipmentType)) {
				equippedFlags.armorEquipped = true;
			}
			if (["heavy"].includes(item.system.equipmentType)) {
				equippedFlags.heavyEquipped = true;
			}
		}
	});
	actor.system.details.armor = {
		armorEquipped: equippedFlags.armorEquipped,
		heavyEquipped: equippedFlags.heavyEquipped
	};
}

/**
 * Some data is expected to be used in item formulas (ex @prime or @combatMastery). 
 * We need to provide those values before we run calculations on items.
 */
function prepareRollDataForItems(actor) {
	_combatMatery(actor);
	_coreAttributes(actor);
	_attackModAndSaveDC(actor);
	_combatTraining(actor);
}

function _background(actor) {
	const details = actor.system.details;
	const skillPoints = actor.system.skillPoints;

	if (skillPoints.skill.override) skillPoints.skill.max = skillPoints.skill.overridenMax;
	if (skillPoints.trade.override) skillPoints.trade.max = skillPoints.trade.overridenMax;
	if (skillPoints.language.override) skillPoints.language.max = skillPoints.language.overridenMax;

	const background = actor.items.get(details.background.id);
	if (!background) return;

	if (!skillPoints.skill.override) skillPoints.skill.max = background.system.skillPoints || 0;
	if (!skillPoints.trade.override) skillPoints.trade.max = background.system.tradePoints || 0;
	if (!skillPoints.language.override) skillPoints.language.max = background.system.langPoints || 0;
}

function _class(actor) {
	const details = actor.system.details;
	const skillPoints = actor.system.skillPoints;
	const attributePoints = actor.system.attributePoints;
	const known = actor.system.known;
	const combatTraining = actor.system.combatTraining;
  const scaling = actor.system.scaling;

	const clazz = actor.items.get(details.class.id);
	if (!clazz) return;

  // Level
  const level = clazz.system.level;
	details.level = level;

	const classScaling = clazz.system.scaling;

  // Resources for Given Level
	details.class.maxHpBonus = _getAllUntilIndex(classScaling.maxHpBonus.values, level - 1);
  details.class.bonusMana = _getAllUntilIndex(classScaling.bonusMana.values, level - 1);
  details.class.bonusStamina = _getAllUntilIndex(classScaling.bonusStamina.values, level - 1);
	details.class.classKey = clazz.system.itemKey;

  // Custom Resources for Given Level
  Object.entries(clazz.system.scaling)
    .forEach(([key, sca]) => scaling[key] = sca.values[level - 1]);

  // Class Category
  details.martial = clazz.system.martial;
	details.spellcaster = clazz.system.spellcaster;

	// Combat Training
	Object.entries(clazz.system.combatTraining).forEach(([key, training]) => combatTraining[key] = training);

	// Skill Points from class 
	skillPoints.skill.max += _getAllUntilIndex(classScaling.skillPoints.values, level - 1);
	skillPoints.trade.max += _getAllUntilIndex(classScaling.tradePoints.values, level - 1);

	// Attribute Point from class
	attributePoints.max += _getAllUntilIndex(classScaling.attributePoints.values, level - 1);

	// Techniques and Spells Known
	known.cantrips.max = _getAllUntilIndex(classScaling.cantripsKnown.values, level - 1);
	known.spells.max = _getAllUntilIndex(classScaling.spellsKnown.values, level - 1);
	known.maneuvers.max = _getAllUntilIndex(classScaling.maneuversKnown.values, level - 1);
	known.techniques.max = _getAllUntilIndex(classScaling.techniquesKnown.values, level - 1);
}

function _ancestry(actor) {
	const details = actor.system.details;
	const movement = actor.system.movement;

	const ancestry = actor.items.get(details.ancestry.id);
	if (!ancestry) return;

	if (!movement.ground.useCustom) movement.ground.value = ancestry.system.movement.speed;
}

function _subclass(actor) {
	const details = actor.system.details;

	const subclass = actor.items.get(details.subclass.id);
	if (!subclass) return;
}

function _weapon(items, actor) {
	let bonusPD = 0;
	items.forEach(item => {
		if (item.system.properties?.guard.active && item.system.statuses.equipped) bonusPD++;
	});
	actor.system.defences.precision.bonuses.always += bonusPD;
} 

function _equipment(items, actor) {
	let collectedData = {
		adBonus: 0,
		pdBonus: 0,
		shieldBonus: {
			adBonus: 0,
			pdBonus: 0,
			shieldsWielded: 0
		},
		pdr: false,
		edr: false,
		speedPenalty: 0,
		agiCheckDis: 0,
		lackArmorTraining: false,
		lackShieldTraining: false,
	};
	items.forEach(item => _armorData(item, collectedData, actor));
	_implementEquipmentData(actor, collectedData);
}

function _armorData(item, data, actor) {
	if (!item.system.statuses.equipped) return data;

	const combatTraining = actor.system.combatTraining;
	const properties = item.system.properties;
	const equipmentType = item.system.equipmentType;

	if (properties.pdr.active) data.pdr = true;
	if (properties.edr.active) data.edr = true;
	if (properties.bulky.active) data.speedPenalty++;
	if (properties.rigid.active) data.agiCheckDis++;

	if (["light", "heavy"].includes(equipmentType)) {
		if (equipmentType === "heavy") {
			if (!combatTraining.heavyArmor) data.lackArmorTraining = true;
		}
		else {
			if (!combatTraining.lightArmor) data.lackArmorTraining = true;
		}
	}
	if (["lshield", "hshield"].includes(item.system.equipmentType)) {
		if (properties.pdIncrease.active) data.shieldBonus.pdBonus = properties.pdIncrease.value;
		if (properties.adIncrease.active) data.shieldBonus.adBonus = properties.adIncrease.value;
		data.shieldBonus.shieldsWielded++;

		if (equipmentType === "hshield") {
			if (!combatTraining.heavyShield) data.lackShieldTraining = true;
		}
		else {
			if (!combatTraining.lightShield) data.lackShieldTraining = true;
		}
	}
	else {
		if (properties.pdIncrease.active) data.pdBonus += properties.pdIncrease.value;
		if (properties.adIncrease.active) data.adBonus += properties.adIncrease.value;
	}
	return data;
}

function _implementEquipmentData(actor, collectedData) {
	const pd = actor.system.defences.precision;
	const ad = actor.system.defences.area;
	actor.system.details;

	if (collectedData.speedPenalty > 0) actor.system.movement.ground.value -= collectedData.speedPenalty;
	for (let i = 0; i < collectedData.agiCheckDis; i++) {
		actor.system.rollLevel.onYou.checks.agi.push('"value": 1, "type": "dis", "label": "Equipped Armor/Shield"');
	}

	// Lack Shield Training
	if (collectedData.lackShieldTraining) {
		actor.system.rollLevel.onYou.martial.melee.push('"value": 1, "type": "dis", "label": "You lack Combat Training in equipped Shield"');
		actor.system.rollLevel.onYou.martial.ranged.push('"value": 1, "type": "dis", "label": "You lack Combat Training in equipped Shield"');
		actor.system.rollLevel.onYou.spell.melee.push('"value": 1, "type": "dis", "label": "You lack Combat Training in equipped Shield"');
		actor.system.rollLevel.onYou.spell.ranged.push('"value": 1, "type": "dis", "label": "You lack Combat Training in equipped Shield"');
		actor.system.rollLevel.onYou.checks.att.push('"value": 1, "type": "dis", "label": "You lack Combat Training in equipped Shield"');
		actor.system.rollLevel.onYou.checks.spe.push('"value": 1, "type": "dis", "label": "You lack Combat Training in equipped Shield"');
	}

	// Lack Armor Training
	if (collectedData.lackArmorTraining) {
		actor.system.rollLevel.onYou.martial.melee.push('"value": 1, "type": "dis", "label": "You lack Combat Training in equipped Armor"');
		actor.system.rollLevel.onYou.martial.ranged.push('"value": 1, "type": "dis", "label": "You lack Combat Training in equipped Armor"');
		actor.system.rollLevel.onYou.spell.melee.push('"value": 1, "type": "dis", "label": "You lack Combat Training in equipped Armor"');
		actor.system.rollLevel.onYou.spell.ranged.push('"value": 1, "type": "dis", "label": "You lack Combat Training in equipped Armor"');
		actor.system.rollLevel.onYou.checks.att.push('"value": 1, "type": "dis", "label": "You lack Combat Training in equipped Armor"');
		actor.system.rollLevel.onYou.checks.spe.push('"value": 1, "type": "dis", "label": "You lack Combat Training in equipped Armor"');
	}

	// Armor bonus
	pd.bonuses.always += collectedData.pdBonus + collectedData.shieldBonus.pdBonus;
	ad.bonuses.always += collectedData.adBonus + collectedData.shieldBonus.adBonus;

	// Wielding Two Shields makes player ignore flanking
	if (collectedData.shieldBonus.shieldsWielded >= 2) {
		actor.system.globalModifier.ignore.flanking = true;
	}

	// PDR and EDR
	if (collectedData.pdr) actor.system.damageReduction.pdr.active = true;
	if (collectedData.edr) actor.system.damageReduction.edr.active = true;
}

function _customResources(items, actor) {
	const scaling = actor.system.scaling;
	const level = actor.system.details.level;

	items.forEach(item => {
		const resource = item.system.resource;
		scaling[resource.resourceKey] = _getAllUntilIndex(resource.values, level - 1);
	});
}

function _conditionals(items, actor) {
	for (const item of items) {
		for (const cond of Object.values(item.system.conditionals)) {
			if (toggleCheck(item, cond.linkWithToggle)) {
				actor.system.conditionals.push(cond);
			}
		}
	}
}

function _combatMatery(actor) {
	if (companionShare(actor, "combatMastery")) {
		actor.system.details.combatMastery = actor.companionOwner.system.details.combatMastery;
	}
	else {
		const level = actor.system.details.level;
		actor.system.details.combatMastery = Math.ceil(level/2);
	}
}

function _coreAttributes(actor) {
	const exhaustion = actor.exhaustion;
	const attributes = actor.system.attributes;
	const details = actor.system.details;
	
	let primeAttrKey = "mig";
	for (let [key, attribute] of Object.entries(attributes)) {
		if (key === "prime") continue;
		const current = companionShare(actor, `attributes.${key}`) 
											? actor.companionOwner.system.attributes[key].value
											: attribute.current;
		// Final value (after respecting bonuses) (-2 is a lower limit)
		attribute.value = Math.max(current + attribute.bonuses.value, -2);

		// Save Modifier
		if (companionShare(actor, `saves.${key}`)) {
			attribute.saveMastery = actor.companionOwner.system.attributes[key].saveMastery;
		}
		const save = attribute.value + details.combatMastery + attribute.bonuses.save - exhaustion;
		attribute.save = save;

		// Check Modifier
		const check = attribute.value + attribute.bonuses.check - exhaustion;
		attribute.check = check;

		if (attribute.value >= attributes[primeAttrKey].value) primeAttrKey = key;
	}
	const useMaxPrime = game.settings.get("dc20rpg", "useMaxPrime");
	if (useMaxPrime && actor.type === "character") {
		details.primeAttrKey = "maxPrime";
		const level = actor.system.details.level;
		const limit = 3 + Math.floor(level/5);
		attributes.prime = {
			saveMastery: true,
			current: limit,
			value: limit,
			save: limit + details.combatMastery - exhaustion,
			check: limit - exhaustion,
			label: "Prime",
			bonuses: {
				check: 0,
				value: 0,
				save: 0
			}
		};
	}
	else {
		if (companionShare(actor, "prime")) {
			const ownerPrime = actor.companionOwner.system.attributes.prime;
			if (ownerPrime) {
				details.primeAttrKey = "prime";
				attributes.prime = foundry.utils.deepClone(ownerPrime);
			}
			else {
				details.primeAttrKey = primeAttrKey;
				attributes.prime = foundry.utils.deepClone(attributes[primeAttrKey]);
			}
		}
		else {
			details.primeAttrKey = primeAttrKey;
			attributes.prime = foundry.utils.deepClone(attributes[primeAttrKey]);
		}
	}
}

function _attackModAndSaveDC(actor) {
	const exhaustion = actor.exhaustion;
	const prime = actor.system.attributes.prime.value;
	const CM = actor.system.details.combatMastery;

	// Attack Modifier
	const attackMod = actor.system.attackMod;
	const mod = attackMod.value;
	if (companionShare(actor, "attackMod")) {
		mod.martial = actor.companionOwner.system.attackMod.value.martial + attackMod.bonus.martial;
		mod.spell = actor.companionOwner.system.attackMod.value.spell + attackMod.bonus.spell;
	}
	else if (!attackMod.flat) {
		mod.martial = prime + CM + attackMod.bonus.martial;
		mod.spell = prime + CM + attackMod.bonus.spell;
	}
	mod.martial -= exhaustion;
	mod.spell -= exhaustion;

	// Save DC
	const saveDC = actor.system.saveDC;
	const save = saveDC.value;
	if (companionShare(actor, "saveDC")) {
		save.martial = actor.companionOwner.system.saveDC.value.martial + saveDC.bonus.martial;
		save.spell = actor.companionOwner.system.saveDC.value.spell + saveDC.bonus.spell;
	}
	else if (!saveDC.flat) {
		save.martial = 10 + prime + CM + saveDC.bonus.martial;
		save.spell = 10 + prime + CM + saveDC.bonus.spell;
	}
	save.martial -= exhaustion;
	save.spell -= exhaustion;
}

function _getAllUntilIndex(table, index) {
	if (table.length <= 0) return 0;

	let sum = 0;
	for (let i = 0; i <= index; i++) {
		sum += table[i];
	}
	return sum;
}

function _combatTraining(actor) {
	if (companionShare(actor, "combatTraining")) {
		actor.system.combatTraining = actor.companionOwner.system.combatTraining;
	} 
}

function enhanceEffects(actor) {
  for (const effect of actor.allApplicableEffects()) {
    for (const change of effect.changes) {
      const value = change.value;
      
      // formulas start with "<:" and end with ":>"
      if (value.includes("<:") && value.includes(":>")) {
        // We want to calculate that formula and repleace it with value calculated
        const formulaRegex = /<:(.*?):>/g;
        const formulasFound = value.match(formulaRegex);

        formulasFound.forEach(formula => {
          const formulaString = formula.slice(2,-2); // We need to remove <: and :>
          const calculated = evaluateDicelessFormula(formulaString, actor.getRollData(true));
          change.value = change.value.replace(formula, calculated.total); // Replace formula with calculated value
        });
      }
    }
  }
}

function modifyActiveEffects(effects, actor) {
  for ( const effect of effects ) {
    const item = effect.getSourceItem();
    if (item) {
      _checkToggleableEffects(effect, item);
      _checkEquippedAndAttunedEffects(effect, item);
    }
    _checkEffectCondition(effect, actor);
  }
}

function _checkToggleableEffects(effect, item) {
  if (item.system.toggle?.toggleable && item.system.effectsConfig?.linkWithToggle) {
    const toggledOn = item.system.toggle.toggledOn;
    if (toggledOn) effect.enable({ignoreStateChangeLock: true});
    else effect.disable({ignoreStateChangeLock: true});
  }
}

function _checkEquippedAndAttunedEffects(effect, item) {
  if (item.system.toggle?.toggleable && item.system.effectsConfig?.linkWithToggle) return; // Toggle overrides equiped
  if (!item.system.effectsConfig?.mustEquip) return;

  const statuses = item.system.statuses;
  if (!statuses) return;
  const requireAttunement = item.system.properties?.attunement.active;

  let shouldEnable = statuses.equipped;
  if (requireAttunement) shouldEnable = statuses.equipped && statuses.attuned;
  if (shouldEnable) effect.enable({ignoreStateChangeLock: true});
  else effect.disable({ignoreStateChangeLock: true});
}

function _checkEffectCondition(effect, actor) {
  if (effect.disabled === true) return; // If effect is already turned off manually we can skip it
  const disableWhen = effect.flags.dc20rpg?.disableWhen;
  if (disableWhen) {
    const value = getValueFromPath(actor, disableWhen.path);
    const expectedValue = parseFromString(disableWhen.value);
    const has = (value, expected) => {
      if (value.has) return value.has(expected);
      if (value.includes) return value.includes(expected);
      return undefined;
    };

    switch (disableWhen.mode) {
      case "==": effect.disabled = value === expectedValue; break;
      case "!=": effect.disabled = value !== expectedValue; break;
      case ">=": effect.disabled = value >= expectedValue; break;
      case ">": effect.disabled = value > expectedValue; break;
      case "<=": effect.disabled = value <= expectedValue; break;
      case "<": effect.disabled = value < expectedValue; break;
      case "has": effect.disabled = has(value, expectedValue) === true; break;
      case "hasNot": effect.disabled = has(value, expectedValue) === false; break;
    }
  }
}

function suspendDuplicatedConditions(actor) {
  const effects = actor.appliedEffects.sort((a, b) => {
    if (!a.statuses) a.statuses = [];
    if (!b.statuses) b.statuses = [];
    return b.statuses.size - a.statuses.size;
  });

  const uniqueEffectsApplied = new Map();
  effects.forEach(effect => {
    const statusId = effect.system.statusId;
    if (uniqueEffectsApplied.has(statusId)) {
      // We need to check which effect has more changes, we want to have the one with most amount of changes active
      const oldEffect = uniqueEffectsApplied.get(statusId);
      if (effect.changes.length <= oldEffect.changes.length) {
        effect.disabled = true;
        effect.suspended = true;
        effect.suspendedBy = oldEffect.name;
      }
      else {
        effect.disabled = false;
        effect.suspended = false;
        oldEffect.disabled = true;
        oldEffect.suspended = true;
        oldEffect.suspendedBy = effect.name;
        uniqueEffectsApplied.set(statusId, effect);
      }
    }
    else {
      effect.suspended = false;
      effect.statuses.forEach(statusId => {
        const status = CONFIG.statusEffects.find(e => e.id === statusId);
        if (status && !status.stackable) {
          uniqueEffectsApplied.set(statusId, effect);
        }
      });
    }
  });
}

function preInitializeFlags(actor) {
	if (actor.flags.dc20rpg) return;

	const flags = {
		editMode: false,
		hideNonessentialEffects: false,
		showInactiveEffects: true,
		showUnknownSkills: true,
		showUnknownTradeSkills: false,
		showUnknownLanguages: false,
		showEmptyReductions: false,
		showEmptyConditions: false,
		onelinerModeDMR: true,
		onelinerModeCI: true,
		showBasicActions: false,
		advancementCounter: 0,
		effectsToRemoveAfterRoll: [],
		actionHeld: {
			isHeld: false,
			itemId: null,
			itemImg: null,
			apForAdv: null,
			enhancements: null,
			mcp: null,
			rollsHeldAction: false
		}
	};

	_initializeRollMenu(flags);
	if (actor.type === 'character') _initializeFlagsForCharacter(flags);
	else _initializeFlagsForNpc(flags);

	actor.update({[`flags.dc20rpg`]: flags});
}

function _initializeRollMenu(flags) {
	flags.rollMenu = {
		autoCrit: false,
		autoFail: false,
		dis: 0,
		adv: 0,
		apCost: 0,
		gritCost: 0,
		d8: 0,
		d6: 0,
		d4: 0,
	};
}

function _initializeFlagsForCharacter(flags) {
		flags.headerFilters = {
			inventory: "",
			features: "",
			techniques: "",
			spells: "",
			favorites: "",
			basic: "",
		};
		flags.headersOrdering = {
			inventory: {
				weapon: {
					name: "Weapons",
					order: 0,
					custom: false
				},
				equipment: {
					name: "Equipments",
					order: 1,
					custom: false
				},
				consumable: {
					name: "Consumables",
					order: 2,
					custom: false
				},
				loot: {
					name: "Loot",
					order: 3,
					custom: false
				}
			},
			features: {
				class: {
					name: "Class Features",
					order: 0,
					custom: false
				},
				subclass: {
					name: "Subclass Features",
					order: 1,
					custom: false
				},
				ancestry: {
					name: "Ancestry Traits",
					order: 2,
					custom: false
				},
				feature: {
					name: "Features",
					order: 3,
					custom: false
				},
				passive: {
					name: "Passives",
					order: 4,
					custom: false
				}
			},
			techniques: {
				maneuver: {
					name: "Maneuvers",
					order: 0,
					custom: false
				},
				technique: {
					name: "Techniques",
					order: 1,
					custom: false
				},
			},
			spells: {
				cantrip: {
					name: "Cantrips",
					order: 0,
					custom: false
				},
				spell: {
					name: "Spells",
					order: 1,
					custom: false
				},
			},
			basic: {
				offensive: {
					name: "Offensive",
					order: 0,
					custom: false
				},
				defensive: {
					name: "Defensive",
					order: 1,
					custom: false
				},
				utility: {
					name: "Utility",
					order: 2,
					custom: false
				},
				reaction: {
					name: "Reaction",
					order: 3,
					custom: false
				},
				skillBased: {
					name: "Skill Based",
					order: 4,
					custom: false
				},
			},
			favorites: {
				basic: {
					name: "Basic Actions",
					order: 0,
					custom: false
				},
				feature: {
					name: "Features",
					order: 1,
					custom: false
				},
				inventory: {
					name: "Inventory",
					order: 2,
					custom: false
				},
				technique: {
					name: "Techniques",
					order: 3,
					custom: false
				},
				spell: {
					name: "Spells",
					order: 4,
					custom: false
				},
			}
		};
}

function _initializeFlagsForNpc(flags) {
	flags.headerFilters = {
		main: "",
		basic: "",
	};
	flags.headersOrdering = {
		main: {
			action: {
				name: "Actions",
				order: 0,
				custom: false
			},
			feature: {
				name: "Features",
				order: 1,
				custom: false
			},
			technique: {
				name: "Techniques",
				order: 2,
				custom: false
			},
			spell: {
				name: "Spells",
				order: 3,
				custom: false
			},
			inventory: {
				name: "Inventory",
				order: 4,
				custom: false
			}
		},
		basic: {
			offensive: {
				name: "Offensive",
				order: 0,
				custom: false
			},
			defensive: {
				name: "Defensive",
				order: 1,
				custom: false
			},
			utility: {
				name: "Utility",
				order: 2,
				custom: false
			},
			reaction: {
				name: "Reaction",
				order: 3,
				custom: false
			},
			skillBased: {
				name: "Skill Based",
				order: 4,
				custom: false
			},
		}
	};
}

function prepareRollData$1(actor, data) {
  _attributes$1(data);
  _details(data);
  _mods(data, actor);
	_allSkills$1(data, actor);
	_defences(data, actor);
	return data;
}

/**
 * Formulas from Active Effects have limited access to calculated data
 * because those calculations happend after active effect are added to character sheet.
 * We want to prepare some common data to be used by active effects here. 
 * Be aware it might be different then fully prepared item roll data.
 */
function prepareRollDataForEffectCall(actor, data) {
	_calculateAttributes(data, actor);
	_calculateDetails(data, actor);
	return data;
}

function _calculateAttributes(data, actor) {
	const attributes = data.attributes;
	let primeAttrKey = "mig";
	if (data.attributes) {
		for (let [key, attribute] of Object.entries(data.attributes)) {
			if (key === "prime") continue;
			data[key] = foundry.utils.deepClone(attribute.current);
			if (attribute.current >= attributes[primeAttrKey].current) primeAttrKey = key;
		}
	}
	const useMaxPrime = game.settings.get("dc20rpg", "useMaxPrime");
	if (useMaxPrime && actor.type === "character") {
		const level = actor.system.details.level;
		const limit = 3 + Math.floor(level/5);
		data.prime = {
			saveMastery: true,
			current: limit,
			value: limit,
			save: limit,
			check: limit,
			label: "Prime",
			bonuses: {
				check: 0,
				value: 0,
				save: 0
			}
		};
	}
	else {
		data.prime = foundry.utils.deepClone(attributes[primeAttrKey].current);
	}
}

function _calculateDetails(data, actor) {
	if (data.details.level) {
		const level = actor.system.details.level || 0;
		data.level = level;
		data.combatMastery = Math.ceil(level/2);
	}
}

function _attributes$1(data) {
	// Copy the attributes to the top level, so that rolls can use
	// formulas like `@mig + 4` or `@prime + 4`
	if (data.attributes) {
		for (let [key, attribute] of Object.entries(data.attributes)) {
			data[key] = foundry.utils.deepClone(attribute.value);
		}
	}
}

function _details(data) {
	// Add level for easier access, or fall back to 0.
	if (data.details.level) {
		data.level = data.details.level ?? 0;
	}
	if (data.details.combatMastery) {
		data.combatMastery = data.details.combatMastery ?? 0;
	}
}

function _mods(data, actor) {
	const attackMod = actor.system.attackMod.value;
	if (attackMod.martial) {
		data.attack = attackMod.martial;
		
		if (data.combatMastery) data.attackNoCM = data.attack - data.combatMastery;
		else data.attackNoCM = data.attack;
	}
	if (attackMod.spell) {
		data.spell = attackMod.spell;
	}
}

function _allSkills$1(data, actor) {
	const allSkills = {};
	for (const [key, skill] of Object.entries(actor.system.skills)) {
		allSkills[key] = skill.modifier;
	}
	if (actor.type === "character") {
		for (let [key, skill] of Object.entries(actor.system.tradeSkills)) {
			allSkills[key] = skill.modifier;
		}
	}
	data.allSkills = allSkills;
}

function _defences(data, actor) {
	const defences = actor.system.defences;
	data.pd = {
		armor: defences.precision.bonuses.armor,
		bonus: defences.precision.bonuses.final,
		value: defences.precision.value,
		heavy: defences.precision.heavy,
		brutal: defences.precision.brutal,
	};
	data.ad = {
		bonus: defences.area.bonuses.final,
		value: defences.area.value,
		heavy: defences.area.heavy,
		brutal: defences.area.brutal,
	};
}

/**
 * Extend the base Actor document by defining a custom roll data structure which is ideal for the Simple system.
 * @extends {Actor}
 */
class DC20RpgActor extends Actor {

  get exhaustion() {
    return getStatusWithId(this, "exhaustion")?.stack || 0
  }

  get allEffects() {
    const effects = [];
    for ( const effect of this.allApplicableEffects()) {
      effects.push(effect);
    }
    const sorted = effects.sort((a, b) => b.changes.length - a.changes.length);
    return sorted;
  }

  /**
   * Collect all events - even from disabled effects
   */
  get allEvents() {
    const events = [];
    for (const effect of this.allApplicableEffects()) {
      for (const change of effect.changes) {
        if (change.key === "system.events") {
          const changeValue = `"effectId": "${effect.id}", ` + change.value; // We need to inject effect id
          const paresed = parseEvent(changeValue);
          events.push(paresed);
        }
      }
    }
    return events;
  }

  /**
   * Collect all events from enabled effects + events with alwaysActive flag set to true
   */
  get activeEvents() {
    const events = [];
    for (const effect of this.allApplicableEffects()) {
      for (const change of effect.changes) {
        if (change.key === "system.events") {
          const changeValue = `"effectId": "${effect.id}", ` + change.value; // We need to inject effect id
          const paresed = parseEvent(changeValue);
          if (!effect.disabled || paresed.alwaysActive) {
            events.push(paresed);
          }
        }
      }
    }
    return events;
  }

  get hasOtherMoveOptions() {
    const movements = this.system.movement;
    if (movements.burrow.current > 0) return true;
    if (movements.climbing.current > 0) return true;
    if (movements.flying.current > 0) return true;
    if (movements.glide.current > 0) return true;
    if (movements.swimming.current > 0) return true;
    return false
  }

  get statusIds() {
    return this.statuses.map(status => status.id);
  }

  /** @override */
  prepareData() {
    this.statuses ??= new Set();
    this.coreStatuses ??= new Set();
    const specialStatuses = new Map();
    for ( const statusId of Object.values(CONFIG.specialStatusEffects) ) {
      specialStatuses.set(statusId, this.hasStatus(statusId));
    }
    super.prepareData();

    const tokens = this.getDependentTokens({scenes: canvas.scene}).filter(t => t.rendered).map(t => t.object) || [];
    for ( const [statusId, wasActive] of specialStatuses ) {
      const isActive = this.hasStatus(statusId);
      if ( isActive === wasActive ) continue;
      for ( const token of tokens ) {
        token._onApplyStatusEffect(statusId, isActive);
      }
    }
    for ( const token of tokens ) token.document.prepareData();
  }

  prepareBaseData() {
    if (this.type === "companion") this._prepareCompanionOwner();
    super.prepareBaseData();
  }

  _prepareCompanionOwner() {
    const companionOwnerId = this.system.companionOwnerId;
    if (companionOwnerId) {
      const companionOwner = game.actors.get(companionOwnerId);
      if (!companionOwner) {
        if (!ui.notifications) console.warn(`Cannot find actor with id "${companionOwnerId}" in Actors directory, try adding it again to ${this.name} companion sheet.`);
        else ui.notifications.warn(`Cannot find actor with id "${companionOwnerId}" in Actors directory, try adding it again to ${this.name} companion sheet.`);
        this.update({["system.companionOwnerId"]: ""}); // We want to clear that information from companion as it is outdated
        return;
      }

      this.companionOwner = companionOwner;
      // Register update actor hook, only once per companion
      if (this.companionOwner.id && !this.updateHookRegistered) {
        Hooks.on("updateActor", (actor, updateData) => {
          if (actor.id === this.companionOwner?.id) {
            this.companionOwner = actor;
            this.prepareData();
            this.sheet.render(false, { focus: false });
            this.getActiveTokens().forEach(token => token.refresh());
          }
        });
      }
      this.updateHookRegistered = true;
    }
    else {
      this.companionOwner = null;
    }
  }

  prepareEmbeddedDocuments() {
    fullyStunnedCheck(this);
    exhaustionCheck(this);
    dazedCheck(this);
    
    prepareUniqueItemData(this);
    prepareEquippedItemsFlags(this);
    enhanceEffects(this);
    this.prepareActiveEffectsDocuments();
    prepareRollDataForItems(this);
    this.prepareOtherEmbeddedDocuments();
    prepareDataFromItems(this);
  }

  /**
   * We need to prepare Active Effects before we deal with other documents.
   * We want them to use modifications applied by active effects.
   */
  prepareActiveEffectsDocuments() {
    for ( const collectionName of Object.keys(this.constructor.hierarchy || {}) ) {
      if (collectionName === "effects") {
        for ( let e of this.getEmbeddedCollection(collectionName) ) {
          e._safePrepareData();
        }
      }
    }
    suspendDuplicatedConditions(this);
    this.applyActiveEffects();

    let token = undefined;
    let controlled = false;
    const selectedTokens = getSelectedTokens();
    if (selectedTokens?.length > 0) {
      token = selectedTokens[0];
      controlled = true;
    }
    if (token) Hooks.call('controlToken', token, controlled); // Refresh token effects tracker
  }

  /**
   * We need to prepare Active Effects before we deal with other items.
   */
  prepareOtherEmbeddedDocuments() {
    for ( const collectionName of Object.keys(this.constructor.hierarchy || {}) ) {
      if (collectionName !== "effects") {
        for ( let e of this.getEmbeddedCollection(collectionName) ) {
          e._safePrepareData();
        }
      }
    }
  }

  /**
   * @override
   * This method collects calculated data (non editable on charcter sheet) that isn't defined in template.json
   */
  prepareDerivedData() {
    makeCalculations$1(this);
    this._prepareCustomResources();
    translateLabels(this);
    this.prepared = true; // Mark actor as prepared
  }

  applyActiveEffects() {
    modifyActiveEffects(this.allApplicableEffects(), this);

    const overrides = {};
    this.statuses.clear();
    this.coreStatuses.clear();
    const numberOfDuplicates = new Map();

    // Organize non-disabled effects by their application priority
    const changes = [];
    for ( const effect of this.allApplicableEffects() ) {
      if ( !effect.active ) continue;
      changes.push(...effect.changes.map(change => {
        const c = foundry.utils.deepClone(change);
        c.effect = effect;
        c.priority = c.priority ?? (c.mode * 10);
        return c;
      }));
      for ( const statusId of effect.statuses ) {
        let oldStatus = getStatusWithId(this, statusId);
        let newStatus = oldStatus || {id: statusId, stack: 1};

        // If condition exist already add +1 stack, if effect is stackable or remove multiplying changes if not
        if (hasStatusWithId(this, statusId)) {
          const status = CONFIG.statusEffects.find(e => e.id === statusId);
          if (status.stackable) newStatus.stack ++;
          else {
            // If it is not stackable it might cause some duplicates in changes we need to get rid of
            for (const change of changes) {
              if (effect.isChangeFromStatus(change, status)) {
                const dupCha = numberOfDuplicates.get(change);
                if (dupCha) numberOfDuplicates[change.key] = {change: change, number: dupCha.number + 1};
                else numberOfDuplicates[change.key] = {change: change, number: 1};
              }
            }
          }
        }

        // remove old status (if exist) and add new record
        this.statuses.delete(oldStatus);
        this.statuses.add(newStatus);
      }

      // Core status
      if (effect.system.statusId) this.coreStatuses.add(effect.system.statusId);
    }

    // Remove duplicated changes from 
    for (const duplicate of Object.values(numberOfDuplicates)) {
      for (let i = 0; i < duplicate.number; i++) {
        let indexToRemove = changes.indexOf(duplicate.change);
        if (indexToRemove !== -1) {
          changes.splice(indexToRemove, 1);
        }
      }
    }

    changes.sort((a, b) => a.priority - b.priority);
    // Apply all changes
    for ( let change of changes ) {
      if ( !change.key ) continue;
      const changes = change.effect.apply(this, change);
      Object.assign(overrides, changes);
    }

    // Expand the set of final overrides
    this.overrides = foundry.utils.expandObject(overrides);
  }

  /** @override */
  getRollData(activeEffectCalls) { 
    // We want to operate on copy of original data because we are making some changes to it
    const data = {...super.getRollData()};
    if (activeEffectCalls) return prepareRollDataForEffectCall(this, data);
    return prepareRollData$1(this, data);
  }

  getCheckOptions(attack, attributes, skills, trades) {
    let checkOptions = attack ? {"att": "Attack Check", "spe": "Spell Check"} : {};
    if (attributes) {
      checkOptions = {...checkOptions, ...CONFIG.DC20RPG.ROLL_KEYS.attributeChecks};
    }
    if (skills) {
      // Martial Check requires acrobatic and athletics skills
      if (this.system.skills.acr && this.system.skills.ath) checkOptions.mar = "Martial Check";
      Object.entries(this.system.skills).forEach(([key, skill]) => checkOptions[key] = `${skill.label} Check`);
    }
    if (trades && this.system.tradeSkills) {
      Object.entries(this.system.tradeSkills).forEach(([key, skill]) => checkOptions[key] = `${skill.label} Check`);
    }
    return checkOptions;
  }

  /**
   * Returns object containing items owned by actor that have charges or are consumable.
   */
  getOwnedItemsIds(excludedId) {
    const excludedTypes = ["class", "subclass", "ancestry", "background", "loot"];

    const itemsWithCharges = {};
    const consumableItems = {};
    const weapons = {};
    const items = this.items;
    items.forEach(item => {
      if (item.id !== excludedId && !excludedTypes.includes(item.type)) {
        const maxChargesFormula = item.system.costs.charges.maxChargesFormula;
        if (maxChargesFormula) itemsWithCharges[item.id] = item.name; 
        if (item.type === "consumable") consumableItems[item.id] = item.name;
        if (item.type === "weapon") weapons[item.id] = item.name;
      }
    });
    return {
      withCharges: itemsWithCharges,
      consumable: consumableItems,
      weapons: weapons
    }
  }

  getWeapons() {
    const weapons = {};
    this.items.forEach(item => {
      const identified = item.system.statuses ? item.system.statuses.identified : true;
      if (item.type === "weapon" && identified) 
        weapons[item.id] = item.name;
    });
    return weapons;
  }

  hasStatus(statusId) {
    return hasStatusWithId(this, statusId)
  }

  hasAnyStatus(statuses) {
    for (const statusId of statuses) {
      if (hasStatusWithId(this, statusId)) return true;
    }
    return false;
  }

  getEffectWithName(effectName) {
    for (const effect of this.allApplicableEffects()) {
      if (effect.name === effectName) return effect;
    }
  }

  async refreshSkills() {
    const skillStore = game.settings.get("dc20rpg", "skillStore");
    const skills = this.system.skills;
    const tradeSkills = this.system.tradeSkills;
    const languages = this.system.languages;

    // Prepare keys to add and remove
    const toRemove = {
      skills: Object.keys(skills).filter((key) => !skillStore.skills[key] && !skills[key].custom),
      languages: Object.keys(languages).filter((key) => !skillStore.languages[key] && !languages[key].custom)
    };
    if (tradeSkills) toRemove.trades = Object.keys(tradeSkills).filter((key) => !skillStore.trades[key] && !tradeSkills[key].custom);
    const toAdd = {
      skills: Object.keys(skillStore.skills).filter((key) => !skills[key]),
      languages: Object.keys(skillStore.languages).filter((key) => !languages[key])
    };
    if (tradeSkills) toAdd.trades = Object.keys(skillStore.trades).filter((key) => !tradeSkills[key]);
  
    // Prepare update data
    const updateData = {system: {skills: {}, languages: {}}};
    if (tradeSkills) updateData.system.tradeSkills = {};
    toRemove.skills.forEach(key => updateData.system.skills[`-=${key}`] = null);
    toRemove.languages.forEach(key => updateData.system.languages[`-=${key}`] = null);
    if (tradeSkills) toRemove.trades.forEach(key => updateData.system.tradeSkills[`-=${key}`] = null);
    toAdd.skills.forEach(key => updateData.system.skills[key] = skillStore.skills[key]);
    toAdd.languages.forEach(key => updateData.system.languages[key] = skillStore.languages[key]);
    if (tradeSkills) toAdd.trades.forEach(key => updateData.system.tradeSkills[key] = skillStore.trades[key]);

    // Update actor
    await this.update(updateData);
  }

  _prepareCustomResources() {
    const customResources = this.system.resources.custom;

    // remove empty custom resources and calculate its max charges
    for (const [key, resource] of Object.entries(customResources)) {
      if (!resource.name) delete customResources[key];
      const fromFormula = resource.maxFormula ? evaluateDicelessFormula(resource.maxFormula, this.getRollData()).total : 0;
      resource.max = fromFormula + (resource.bonus || 0);
    }
  }

  async toggleStatusEffect(statusId, {active, overlay=false, extras}={}) {
    const status = CONFIG.statusEffects.find(e => e.id === statusId);
    if ( !status ) throw new Error(`Invalid status ID "${statusId}" provided to Actor#toggleStatusEffect`);
    const existing = [];

    // Find the effect with the static _id of the status effect
    if ( status._id ) {
      const effect = this.effects.get(status._id);
      if ( effect ) existing.push(effect.id);
    }

    // If no static _id, find all single-status effects that have this status
    else {
      for (const effect of this.allEffects) {
        const statuses = effect.statuses;
        // We only want to turn off standard status effects that way, not the ones from items.
        if (effect.sourceName === "None") {
          if (statuses.size === 1 &&  statuses.has(statusId)) existing.push(effect.id);
        }
      }
    }

    // Remove the existing effects unless the status effect is forced active
    if (!active && existing.length) {
      await this.deleteEmbeddedDocuments("ActiveEffect", [existing.pop()]); // We want to remove 1 stack of effect at the time
      this.reset();
      return false;
    }
    
    // Create a new effect unless the status effect is forced inactive
    if ( !active && (active !== undefined) ) return;
    // Create new effect only if status is stackable
    if (existing.length > 0 && !status.stackable) return;
    // Do not create new effect if actor is immune to it.
    if (this.system.statusResistances[statusId]?.immunity) {
      ui.notifications.warn(`${this.name} is immune to '${statusId}'.`);
      return;
    }

    let effect = await ActiveEffect.implementation.fromStatusEffect(statusId);
    if ( overlay ) effect.updateSource({"flags.core.overlay": true});
    effect = enhanceStatusEffectWithExtras(effect, extras);
    const effectData = {...effect};
    effectData._id = effect._id;
    const created = await ActiveEffect.implementation.create(effectData, {parent: this, keepId: true});

    // Unconscious also causes prone
    if (created.statuses.has("unconscious")) await this.toggleStatusEffect("prone", {active: true});
    // Deaths Doors also adds one exhaustion stack
    if (created.statuses.has("deathsDoor")) await this.toggleStatusEffect("exhaustion", {active: true});
    
    this.reset();
    return created;
  }

  //NEW UPDATE CHECK: We need to make sure it works fine with future foundry updates
  /** @override */
  async rollInitiative({createCombatants=false, rerollInitiative=false, initiativeOptions={}}={}) {

    // Obtain (or create) a combat encounter
    let combat = game.combat;
    if ( !combat ) {
      if ( game.user.isGM && canvas.scene ) {
        const cls = getDocumentClass("Combat");
        combat = await cls.create({scene: canvas.scene.id, active: true});
      }
      else {
        ui.notifications.warn("COMBAT.NoneActive", {localize: true});
        return null;
      }
    }

    // Create new combatants
    if ( createCombatants ) {
      let tokens = this.getActiveTokens();

      //====== INJECTED ====== 
      // If tokens are linked we want to roll initiative only for one token
      if (tokens[0] && tokens[0].document.actorLink === true) {
        let tokenFound = tokens[0];

        // We expect that player will controll token from which he wants to roll initiative, if not we will pick first one
        const controlledIds = canvas.tokens.controlled.map(token => token.id);
        for(const token of tokens) {
          if (controlledIds.includes(token.id)) {
            tokenFound = token;
            break;
          }
        }
        tokens = [tokenFound];
      }
      //====== INJECTED ====== 

      const toCreate = [];
      if ( tokens.length ) {
        for ( let t of tokens ) {
          if ( t.inCombat ) continue;
          toCreate.push({tokenId: t.id, sceneId: t.scene.id, actorId: this.id, hidden: t.document.hidden});
        }
      } else toCreate.push({actorId: this.id, hidden: false});
      await combat.createEmbeddedDocuments("Combatant", toCreate);
    }

    // Roll initiative for combatants
    const combatants = combat.combatants.reduce((arr, c) => {
      if ( this.isToken && (c.token !== this.token) ) return arr;
      if ( !this.isToken && (c.actor !== this) ) return arr;
      if ( !rerollInitiative && (c.initiative !== null) ) return arr;
      arr.push(c.id);
      return arr;
    }, []);

    await combat.rollInitiative(combatants, initiativeOptions);
    return combat;
  }

  async modifyTokenAttribute(attribute, value, isDelta=false, isBar=true) {
    // We want to suppress default bar behaviour for health as we have our special method to deal with health changes
    if (attribute === "resources.health") {
      isBar = false; 
      attribute += ".value";
    }
    return await super.modifyTokenAttribute(attribute, value, isDelta, isBar);
  }

  /** @override */
  _onCreate(data, options, userId) {
    super._onCreate(data, options, userId);
    if (userId === game.user.id) {
      this.prepareBasicActions();
      preConfigurePrototype(this);
      preInitializeFlags(this);
    }
  }

  _onUpdate(changed, options, userId) {
    super._onUpdate(changed, options, userId);
    // HP change check
    if (userId === game.user.id) {
      if (changed.system?.resources?.health) {
        const newHP = changed.system.resources.health;
        const previousHP = this.hpBeforeUpdate;
        const tempHpChange = newHP.temp > 0 && !newHP.current;
        
        const newValue = newHP.value;
        const oldValue = previousHP.value;
        healthThresholdsCheck(newHP.current, this);
        
        const hpDif = oldValue - newValue;
        const tokens = getAllTokensForActor(this);
        if (hpDif < 0) {
          const text = `+${Math.abs(hpDif)}`;
          tokens.forEach(token => displayScrollingTextOnToken(token, text, "#009c0d"));
          if(!this.fromEvent && !tempHpChange) runEventsFor("healingTaken", this, minimalAmountFilter(Math.abs(hpDif)), {amount: Math.abs(hpDif), messageId: this.messageId}); // Temporary HP does not trigger that event (it is not healing)
        }
        else if (hpDif > 0) {
          const text = `-${Math.abs(hpDif)}`;
          tokens.forEach(token => displayScrollingTextOnToken(token, text, "#9c0000"));
          if(!this.fromEvent) runEventsFor("damageTaken", this, minimalAmountFilter(Math.abs(hpDif)), {amount: Math.abs(hpDif), messageId: this.messageId});
        }
      }
    }
  }

  /** @inheritDoc */
  async _preUpdate(changes, options, user) {
    await updateActorHp(this, changes);
    if (changes.system?.resources?.health) {
      this.fromEvent = changes.fromEvent;
      this.messageId = changes.messageId;
      this.hpBeforeUpdate = this.system.resources.health;
    }
    return await super._preUpdate(changes, options, user);
  }

  prepareBasicActions() {
    if (!this.flags.basicActionsAdded) {
      addBasicActions(this);
    }
  }
}

function makeCalculations(item) {
  if (item.system.attackFormula) _calculateRollModifier(item);
  if (item.system.rollRequests) _calculateSaveDC(item);
  if (item.system.costs?.charges) _calculateMaxCharges(item);
  if (item.system.enhancements) _calculateSaveDCForEnhancements(item);
  if (item.system.conditional) _calculateSaveDCForConditionals(item);
  if (item.type === "weapon") _runWeaponStyleCheck(item);
  if (item.type === "feature") _checkFeatureSourceItem(item);

  if (item.system.hasOwnProperty("usesWeapon")) _usesWeapon(item);
}

function _calculateRollModifier(item) {
  const system = item.system;
  const attackFormula = system.attackFormula;
  
  // Prepare formula
  let calculationFormula = "";

  // determine if it is a spell or attack check
  if (attackFormula.checkType === "attack") {
    if (system.attackFormula.combatMastery) calculationFormula += " + @attack";
    else calculationFormula += " + @attackNoCM";
  }
  else if (attackFormula.checkType === "spell") calculationFormula += " + @spell";

  if (system.attackFormula.rollBonus) calculationFormula +=  " + @rollBonus";
  attackFormula.formulaMod = calculationFormula;

  // Calculate roll modifier for formula
  const rollData = item.getRollData();
  attackFormula.rollModifier = attackFormula.formulaMod ? evaluateDicelessFormula(attackFormula.formulaMod, rollData).total : 0;
}

function _calculateSaveDC(item) {
  const rollRequests = item.system.rollRequests;
  if (!item.actor) return;

  for (const [key, request] of Object.entries(rollRequests)) {
    if (request.category !== "save") continue;
    if (request.dcCalculation === "flat") continue;
    request.dc = _getSaveDCFromActor(request, item.actor);
    rollRequests[key] = request;
  }
}

function _calculateSaveDCForEnhancements(item) {
  if (!item.actor) return;

  const enhancements = item.system.enhancements;
  for (const enh of Object.values(enhancements)) {
    if (enh.modifications.addsNewRollRequest) {
      const save = enh.modifications.rollRequest;
      if (save.category !== "save") continue;
      if (save.dcCalculation === "flat") continue;
      enh.modifications.rollRequest.dc = _getSaveDCFromActor(save, item.actor);
    }
  }
}

function _calculateSaveDCForConditionals(item) {
  if (!item.actor) return;

  const conditionals = item.system.conditionals;
  if (!conditionals) return;

  for (const cond of Object.values(conditionals)) {
    if (cond.addsNewRollRequest) {
      const save = cond.rollRequest;
      if (save.category === "save" && save.dcCalculation !== "flat") {
        cond.rollRequest.dc = _getSaveDCFromActor(save, item.actor);
      }
    }
  }
}

function _getSaveDCFromActor(request, actor) {
  const saveDC = actor.system.saveDC;
  switch (request.dcCalculation) {
    case "martial":
      return saveDC.value.martial;
    case "spell":
      return saveDC.value.spell; 
    default:
      let dc = 10;
      const key = request.dcCalculation;
      if (!key) return 0;
      dc += actor.system.attributes[key].value;
      if (request.addMastery) dc += actor.system.details.combatMastery;
      return dc;
  }
}

function _calculateMaxCharges(item) {
  const charges = item.system.costs.charges;
  const rollData = item.getRollData();
  charges.max = charges.maxChargesFormula ? evaluateDicelessFormula(charges.maxChargesFormula, rollData).total : null;
  if (charges.current === null) charges.current = charges.max;
}

function _usesWeapon(item) {
  const usesWeapon = item.system.usesWeapon;
  if (!usesWeapon?.weaponAttack) return;

  const owner = item.actor;
  if (!owner) return;

  const weapon = owner.items.get(usesWeapon.weaponId);
  if (!weapon) return;
  
  // We want to copy weapon attack range, weaponStyle and weaponType so we can make 
  // conditionals work for techniques and features that are using weapons
  item.system.weaponStyle = weapon.system.weaponStyle;
  item.system.weaponType = weapon.system.weaponType;
  item.system.weaponStyleActive = weapon.system.weaponStyleActive;
  item.system.attackFormula.rangeType = weapon.system.attackFormula.rangeType;
  item.system.attackFormula.checkType = weapon.system.attackFormula.checkType;

  // We also want to copy weapon properties and range
  item.system.properties = weapon.system.properties;
  item.system.range = weapon.system.range;
}

function _runWeaponStyleCheck(item) {
  const owner = item.actor;
  if (!owner) return;

  const weaponStyleActive = item.system.weaponStyleActive;
  // If it is not true then we want to check if actor has "weapons" Combat Training.
  // If it is true, then we assume that some feature made it that way and we dont care about the actor
  if (!weaponStyleActive) item.system.weaponStyleActive = owner.system.combatTraining.weapons;
}

function _checkFeatureSourceItem(item) {
  const system = item.system;
  if (!CONFIG.DC20RPG.UNIQUE_ITEM_IDS) return;

  if (["class", "subclass", "ancestry", "background"].includes(system.featureType)) {
    const newOrigin = CONFIG.DC20RPG.UNIQUE_ITEM_IDS[system.featureType]?.[system.featureSourceItem];
    if (newOrigin && newOrigin !== item.system.featureOrigin) item.update({["system.featureOrigin"]: newOrigin});
  }
}

function initFlags(item) {
  if (!item.flags.dc20rpg) item.flags.dc20rpg = {};

  const flags = item.flags.dc20rpg;
  if (flags.favorites === undefined) flags.favorites = false;
  _rollMenu(flags);
}

function _rollMenu(flags) {
  if (flags.rollMenu === undefined) flags.rollMenu = {};
	if (flags.rollMenu.dis === undefined) flags.rollMenu.dis = 0;
	if (flags.rollMenu.adv === undefined) flags.rollMenu.adv = 0;
  if (flags.rollMenu.apCost === undefined) flags.rollMenu.apCost = 0;
  if (flags.rollMenu.gritCost === undefined) flags.rollMenu.gritCost = 0;
	if (flags.rollMenu.d8 === undefined) flags.rollMenu.d8 = 0;
	if (flags.rollMenu.d6 === undefined) flags.rollMenu.d6 = 0;
	if (flags.rollMenu.d4 === undefined) flags.rollMenu.d4 = 0;
	if (flags.rollMenu.free === undefined) flags.rollMenu.free = false;
  if (flags.rollMenu.rangeType === undefined) flags.rollMenu.rangeType = false;
  if (flags.rollMenu.versatile === undefined) flags.rollMenu.versatile = false;
  if (flags.rollMenu.ignoreMCP === undefined) flags.rollMenu.ignoreMCP = false;
  if (flags.rollMenu.showMenu === undefined) flags.rollMenu.showMenu = false;
  if (flags.rollMenu.flanks === undefined) flags.rollMenu.flanks = false;
  if (flags.rollMenu.halfCover === undefined) flags.rollMenu.halfCover = false;
  if (flags.rollMenu.tqCover === undefined) flags.rollMenu.tqCover = false;
  if (flags.rollMenu.autoCrit === undefined) flags.rollMenu.autoCrit = false;
  if (flags.rollMenu.autoFail === undefined) flags.rollMenu.autoFail = false;
}

function prepareRollData(item, data) {
  let rollData = {
    ...data,
    rollBonus: data.attackFormula?.rollBonus
  };

  // If present, add the actor's roll data.
  const actor = item.actor;
  if (actor) {
    const actorRollData = actor.getRollData();
    return {...rollData, ...actorRollData};
  }
  else return rollData;
}

/**
 * Extend the basic Item with some very simple modifications.
 * @extends {Item}
 */
class DC20RpgItem extends Item {
  static enhLoopCounter = 0;

  get checkKey() {
    const actionType = this.system.actionType;
    if (actionType === "attack") return this.system.attackFormula.checkType.substr(0, 3);
    if (actionType === "check") return this.system.check.checkKey;
    return null;
  }

  get allEffects() {
    const effects = [];
    for (const effect of this.effects) {
      effects.push(effect);
    }
    return effects;
  }

  get allEnhancements() {
    let enhancements = new Map();
    if (!this.system.enhancements) return enhancements;

    // Collect enhancements from that specific item
    for (const [key, enh] of Object.entries(this.system.enhancements)) {
      enh.sourceItemId = this.id;
      enh.sourceItemName = this.name;
      enh.sourceItemImg = this.img;
      enhancements.set(key, enh);
    }

    const parent = this.actor;
    if (!parent) return enhancements;

    // We need to deal with case where items call each other in a infinite loop
    // We expect 10 to be deep enough to collect all the coppied enhancements
    let firstCall = false;
    if (DC20RpgItem.enhLoopCounter === 0) firstCall = true;
    if (DC20RpgItem.enhLoopCounter > 10) return enhancements;
    DC20RpgItem.enhLoopCounter++;

    // Collect copied enhancements
    for (const itemWithCopyEnh of parent.itemsWithEnhancementsToCopy) {
      if (itemWithCopyEnh.itemId === this.id) continue;
      if (itemMeetsUseConditions(itemWithCopyEnh.copyFor, this)) {
        const item = parent.items.get(itemWithCopyEnh.itemId);
        if (this.id === item.system.usesWeapon?.weaponId) continue; //Infinite loop when it happends
        if (item && item.system.copyEnhancements?.copy && toggleCheck(item, item.system.copyEnhancements?.linkWithToggle)) {
          enhancements = new Map([...enhancements, ...item.allEnhancements]);
        }
      }
    }

    // Collet from used weapon
    const usesWeapon = this.system.usesWeapon;
    if (usesWeapon?.weaponAttack) {
      const weapon = parent.items.get(usesWeapon.weaponId);
      if (weapon) {
        enhancements = new Map([...enhancements, ...weapon.allEnhancements]);
      }
    }

    if (firstCall) DC20RpgItem.enhLoopCounter = 0;
    return enhancements;
  }

  get activeEnhancements() {
    const active = new Map();
    for (const [key, enh] of this.allEnhancements.entries()) {
      if (enh.number > 0) active.set(key, enh);
    }
    return active
  }

  /**
   * Augment the basic Item data model with additional dynamic data.
   */
  prepareData() {
    // As with the actor class, items are documents that can have their data
    // preparation methods overridden (such as prepareBaseData()).
    super.prepareData();
  }

  prepareBaseData() {
    super.prepareBaseData();
    initFlags(this);
  }
 
  prepareDerivedData() {
    makeCalculations(this);
    translateLabels(this);
    this.prepared = true; // Mark item as prepared
  }

  /**
   * Prepare a data object which is passed to any Roll formulas which are created related to this Item
   * @private
   */
  getRollData() {
    const data = {...super.getRollData()};
    return prepareRollData(this, data);
  }

  swapMultiFaceted() {
    const multiFaceted = this.system.properties?.multiFaceted;
    if (!multiFaceted || !multiFaceted.active) return;

    const damageFormula = this.system.formulas.weaponDamage;
    if (!damageFormula) {
      ui.notifications.error("Original damage formula cannot be found. You have to recreate this item to fix that problem");
      return;
    }

    if (multiFaceted.selected === "first") multiFaceted.selected = "second";
    else multiFaceted.selected = "first";
    const selected = multiFaceted.selected;

    multiFaceted.labelKey = multiFaceted.weaponStyle[selected];
    const weaponStyle = multiFaceted.weaponStyle[selected];
    damageFormula.type = multiFaceted.damageType[selected];

    const updateData = {
      system: {
        weaponStyle: weaponStyle,
        properties: {
          multiFaceted: multiFaceted
        },
        formulas: {
          weaponDamage: damageFormula
        }
      }
    };
    this.update(updateData);
  }

  async update(data={}, operation={}) {
    try {
      await super.update(data, operation);
    } catch (error) {
      if (error.message.includes("does not exist!")) {
        ui.notifications.clear();
      }
      else throw error;
    }
  }

  getEffectWithName(effectName) {
    return this.effects.getName(effectName);
  }

  async _onCreate(data, options, userId) {
    const onCreateReturn = super._onCreate(data, options, userId);
    if (userId === game.user.id && this.actor) {
      await runTemporaryItemMacro(this, "onCreate", this.actor);
      addItemToActorInterceptor(this, this.actor);
    }
    return onCreateReturn;
  }

  _onUpdate(changed, options, userId) {
    super._onUpdate(changed, options, userId);
    if (userId === game.user.id && this.actor) {
      modifiyItemOnActorInterceptor(this, changed, this.actor);
    }
  }

  async _preDelete(options, user) {
    if (this.actor) {
      await runTemporaryItemMacro(this, "preDelete", this.actor);
      removeItemFromActorInterceptor(this, this.actor);
    }
    return await super._preDelete(options, user);
  }

  //=========================
  //        FORMULAS        =
  //=========================
  /**
   * Creates new non-core Formula object on this item.
   * Both formula and formulaKey parameters are optional and if not provided will be generated automatically.
   */
  createFormula(formula={}, formulaKey) {
    const newFormula = foundry.utils.mergeObject(this.getFormulaObjectExample(), formula);
    const key = formulaKey ? formulaKey : generateKey();
    this.update({[`system.formulas.${key}`]: newFormula});
  }

  removeFormula(key) {
    this.update({ [`system.formulas.-=${key}`]: null });
  }

  /**
   * Returns example Formula object that can be modified and used for creating new macros.
   */
  getFormulaObjectExample() {
    return {
      formula: "",
      type: "",
      category: "damage",
      fail: false,
      failFormula: "",
      each5: false,
      each5Formula: "",
      dontMerge: false,
      overrideDefence: "",
    }
  }

  //==========================
  //       ROLL REQUEST      =
  //==========================
  /**
   * Creates new Roll Request object on this item.
   * Both rollRequest and rollRequestKey parameters are optional and if not provided will be generated automatically.
   */
  createRollRequest(rollRequest={}, rollRequestKey) {
    const request = foundry.utils.mergeObject(this.getRollRequestObjectExample(), rollRequest);
    const key = rollRequestKey ? rollRequestKey : generateKey();
    this.update({[`system.rollRequests.${key}`]: request});
  }

  removeRollRequest(key) {
    this.update({ [`system.rollRequests.-=${key}`]: null });
  }

  /**
   * Returns example Roll Request object that can be modified and used for creating new macros.
   */
  getRollRequestObjectExample() {
    return {
      category: "save",
      saveKey: "",
      contestedKey: "",
      dcCalculation: "spell",
      dc: 0,
      addMasteryToDC: true,
      respectSizeRules: false,
    }
  }

  //============================
  //       AGAINST STATUS      =
  //============================
  /**
   * Creates new Against Status object on this item.
   * Both againstStatus and againstStatusKey parameters are optional and if not provided will be generated automatically.
   */
  createAgainstStatus(againstStatus={}, againstStatusKey) {
    const against = foundry.utils.mergeObject(this.getAgainstStatusObjectExample(), againstStatus);
    const key = againstStatusKey ? againstStatusKey : generateKey();
    this.update({[`system.againstStatuses.${key}`]: against});
  }

  removeAgainstStatus(key) {
    this.update({ [`system.againstStatuses.-=${key}`]: null });
  }

  /**
   * Returns example Against Status object that can be modified and used for creating new macros.
   */
  getAgainstStatusObjectExample() {
    return {
      id: "",
      supressFromChatMessage: false,
      untilYourNextTurnStart: false,
      untilYourNextTurnEnd: false,
      untilTargetNextTurnStart: false,
      untilTargetNextTurnEnd: false,
      untilFirstTimeTriggered: false,
      forOneMinute: false,
      repeatedSave: false,
      repeatedSaveKey: "phy"
    };
  }

  //==========================
  //       ENHANCEMENTS      =
  //==========================
  /**
   * Creates new Enhancement object on this item.
   * Both enhancement and enhancementKey parameters are optional and if not provided will be generated automatically.
   */
  createNewEnhancement(enhancement={}, enhancementKey) {
    const enh = foundry.utils.mergeObject(this.getEnhancementObjectExample(), enhancement);
    const key = enhancementKey ? enhancementKey : generateKey();
    this.update({[`system.enhancements.${key}`]: enh});
  }

  removeEnhancement(key) {
    this.update({[`system.enhancements.-=${key}`]: null });
  }

  /**
   * Returns example enhancement object that can be modified and used for creating new enhancements.
   */
  getEnhancementObjectExample() {
    const customCosts = Object.fromEntries(
      Object.entries(this.system.costs.resources.custom)
        .map(([key, custom]) => { 
          custom.value = null; 
          return [key, custom];
        })
      );

    const resources = {
      actionPoint: null,
      health: null,
      mana: null,
      stamina: null, 
      grit: null,
      custom: customCosts
    };
    const charges = {
      consume: false,
      fromOriginal: false
    };
    const modifications = {
      modifiesCoreFormula: false,
      coreFormulaModification: "",
      overrideTargetDefence: false,
      targetDefenceType: "area",
      hasAdditionalFormula: false,
      additionalFormula: "",
      overrideDamageType: false,
      damageType: "",
      addsNewFormula: false,
      formula: {
        formula: "",
        type: "",
        category: "damage",
        dontMerge: false,
        overrideDefence: ""
      },
      addsNewRollRequest: false,
      rollRequest: {
        category: "",
        saveKey: "",
        contestedKey: "",
        dcCalculation: "",
        dc: 0,
        addMasteryToDC: true,
        respectSizeRules: false,
      },
      addsAgainstStatus: false,
      againstStatus: {
        id: "",
        supressFromChatMessage: false,
        untilYourNextTurnStart: false,
        untilYourNextTurnEnd: false,
        untilTargetNextTurnStart: false,
        untilTargetNextTurnEnd: false,
        untilFirstTimeTriggered: false,
        forOneMinute: false,
        repeatedSave: false,
        repeatedSaveKey: "phy"
      },
      addsEffect: null,
      macro: "",
      macros: {
        preItemRoll: "",
        postItemRoll: ""
      },
      rollLevelChange: false,
      rollLevel: {
        type: "adv",
        value: 1
      },
      addsRange: false,
      bonusRange: {
        melee: null,
        normal: null,
        max: null
      }
    };

    return {
      name: "New Enhancement",
      number: 0,
      resources: resources,
      charges: charges,
      modifications: modifications,
      description: "",
      hide: false,
    }
  }

  //============================
  //        CONDITIONALS       =
  //============================
  /**
   * Creates new conditional object on this item.
   * Both conditional and conditionalKey parameters are optional and if not provided will be generated automatically.
   */
  createNewConditional(conditional={}, conditionalKey) {
    const cond = foundry.utils.mergeObject(this.getConditionalObjectExample(), conditional);
    const key = conditionalKey ? conditionalKey : generateKey();
    this.update({[`system.conditionals.${key}`]: cond});
  }

  removeConditional(key) {
    this.update({[`system.conditionals.-=${key}`]: null});
  }

  /**
   * Returns example conditional object that can be modified and used for creating new conditionals.
   */
  getConditionalObjectExample() {
    return {
      name: "New Conditional",
      condition: "", 
      useFor: "",
      linkWithToggle: false,
      bonus: "",
      flags: {
        ignorePdr: false,
        ignoreEdr: false,
        ignoreMdr: false,
        ignoreResistance: {},
        ignoreImmune: {}
      },
      effect: null,
      addsNewRollRequest: false,
      rollRequest: {
        category: "",
        saveKey: "phy",
        contestedKey: "",
        dcCalculation: "",
        dc: 0,
        addMasteryToDC: true,
        respectSizeRules: false,
      },
      addsNewFormula: false,
      formula: {
        formula: "",
        type: "",
        category: "damage",
        dontMerge: false,
        overrideDefence: "",
      },
    };
  }

  //==========================
  //        ITEM MACRO       =
  //==========================
  /**
   * Creates new Item Macro object on this item.
   * Both macroObject and macroKey parameters are optional and if not provided will be generated automatically.
   */
  createNewItemMacro(macroObject={}, macroKey) {
    const macro = foundry.utils.mergeObject(this.getMacroObjectExample(), macroObject);
    const key = macroKey ? macroKey : generateKey();
    this.update({[`system.macros.${key}`]: macro});
  }

  editItemMacro(key) {
    const command = this.system.macros[key]?.command;
    if (!command === undefined) return;
    const macro = createTemporaryMacro(command, this, {item: this, key: key});
    macro.canUserExecute = (user) => {
      ui.notifications.warn("This is an Item Macro and it cannot be executed here.");
      return false;
    };
    macro.sheet.render(true);
  }

  removeItemMacro(key) {
    this.update({[`system.macros.-=${key}`]: null});
  }

  /**
   * Returns example macro object that can be modified and used for creating new macros.
   */
  getMacroObjectExample() {
    return {
      command: "",
      trigger: "",
      disabled: false,
      name: "New Macro",
      title: "",
    };
  }
}

class DC20RpgCombatant extends Combatant {

  constructor(data, combat) {
    super(data, combat);
    const isCharacterType = this.actor.type === "character";
    const companionDoesNotShareInitiative = this.actor.type === "companion" && !companionShare(this.actor, "initiative");
    this.canRollInitiative = isCharacterType || companionDoesNotShareInitiative;
  }

  get isDefeated() {
    return this.defeated || !!this.actor?.hasStatus(CONFIG.specialStatusEffects.DEFEATED);
  }

  prepareData() {
    super.prepareData();
    if (!this.actor.prepared) this.actor.prepareData();
  }
}

class InitiativeSlotSelector extends Dialog {

  constructor(combat, winningTeam, dialogData = {}, options = {}) {
    super(dialogData, options);
    this.pcWin = winningTeam === "pc";
    this.combat = combat;
    this.enemySelected = "";
  }

  static get defaultOptions() {
    return foundry.utils.mergeObject(super.defaultOptions, {
      classes: ["dc20rpg", "dialog"],
      width: 450,
    });
  }

  /** @override */
  get template() {
    return "systems/dc20rpg/templates/dialogs/initiative-slot-selector.hbs";
  }

  getData() {
    const pcCombatants = this.combat.combatants
              .filter(combatant => combatant.actor.type === "character")
              .sort((a, b) => {
                const first = a.slot || 99;
                const second = b.slot || 99;
                return first - second;
              });
    const slots = this.pcWin ?
      {1:1, 3:3, 5:5, 7:7, 9:9, 11:11, 13:13, 15:15, 17:17, 19:19} :
      {2:2, 4:4, 6:6, 8:8, 10:10, 12:12, 14:14, 16:16, 16:16, 20:20};

    return {
      header: this.pcWin ? "PC Team Wins" : "Enemy Team Wins",
      combatants: pcCombatants,
      enemySelection: this.pcWin ? null : this._getEnemySelection(),
      enemySelected: this.enemySelected,
      slots: slots
    }
  }

  _getEnemySelection() {
    const enemySelection = {};
    this.combat.combatants
          .filter(combatant => combatant.actor.type === "npc")
          .forEach(combatant => enemySelection[combatant._id] = combatant.name);
    return enemySelection;
  }

  activateListeners(html) {
    super.activateListeners(html);
    html.find(".select-slot").change(ev => this._onSelection(datasetOf(ev).combatantId, valueOf(ev)));
    html.find(".select-enemy").change(ev => this._onEnemySelection(valueOf(ev)));
    html.find(".confirm-selection").click(ev => this._onConfirm(ev));
  }

  _onSelection(combatantId, value) {
    const combatant = this.combat.combatants.get(combatantId);
    if (!combatant) return;
    combatant.slot = value;
    this.render();
  }

  _onEnemySelection(combatantId) {
    // Deselect already selected
    if (this.enemySelected) {
      const alreadySelected = this.combat.combatants.get(this.enemySelected);
      if (alreadySelected) alreadySelected.slot = undefined;
    }

    // Select new enemy
    const combatant = this.combat.combatants.get(combatantId);
    if (!combatant) return;
    combatant.slot = 1;
    this.enemySelected = combatantId;
    this.render();
  }

  async _onConfirm(event) {
    event.preventDefault();

    const combatants = this.combat.combatants;
    for (const combatant of combatants) {
      if (combatant.slot) {
        let numericValue = parseInt(combatant.slot);
        if (isNaN(numericValue)) continue;
        combatant.update({initiative: 20 - numericValue});
      }
    }
    this.promiseResolve(null);
    this.close();
  }

  static async create(combat, winningTeam, dialogData = {}, options = {}) {
    const prompt = new InitiativeSlotSelector(combat, winningTeam, dialogData, options);
    return new Promise((resolve) => {
      prompt.promiseResolve = resolve;
      prompt.render(true);
    });
  }

  /** @override */
  close(options) {
    if (this.promiseResolve) this.promiseResolve(false);
    super.close(options);
  }
}

async function initiativeSlotSelector(combat, winningTeam) {
  return await InitiativeSlotSelector.create(combat, winningTeam, {title: "Initative Slot Selector"});
}

class DC20RpgCombat extends Combat {

  prepareData() {
    super.prepareData();
    this._prepareCompanionSharingInitiative();
  }

  isActorCurrentCombatant(actorId) {
    if (this.combatant.actor.id === actorId) return true;
    if (this.combatant.companions && this.combatant.companions.length !== 0) {
      const comapnionIds = this.combatant.companions;
      for (const combatant of this.combatants) {
        if (!comapnionIds.includes(combatant.id)) continue;
        if (companionShare(combatant.actor, "initiative")) {
          if (combatant.actor.id === actorId) return true;
        }
      }
    }
    return false;
  }

  _prepareCompanionSharingInitiative() {
    this.combatants.forEach(combatant => {
      if (companionShare(combatant.actor, "initiative")) {
        const companionOwnerId = combatant.actor.companionOwner.id;
        const owner = this.combatants.find(combatant => combatant.actorId === companionOwnerId);
        if (!owner) {
          combatant.skip = false;
          return;
        }

        combatant.skip = true;
        const companions = owner.companions || [];
        // We want to add companion only once
        if (!companions.find(companionId => companionId === combatant._id)) companions.push(combatant._id); 
        owner.companions = companions;
      }
      else {
        combatant.skip = false;
      }
    });
  }

  /** @override **/
  async rollInitiative(ids, {formula=null, updateTurn=true, messageOptions={}}={}) {
    if (game.combat.started) {
      ui.notifications.warn(game.i18n.localize("dc20rpg.combatTracker.combatStarted"));
      return;
    }
    // Structure input data
    ids = typeof ids === "string" ? [ids] : ids;
    const currentId = this.combatant?.id;

    // Iterate over Combatants, performing an initiative roll for each
    const updates = [];
    const messages = [];
    for ( let [i, id] of ids.entries() ) {

      // Get Combatant data (non-strictly)
      const combatant = this.combatants.get(id);
      if ( !combatant?.isOwner ) continue;

      let initiative = null;
      if (combatant.actor.type === "character") initiative = await this._initiativeRollForPC(combatant);
      if (combatant.actor.type === "companion") initiative = await this._initiativeForCompanion(combatant);
      if (initiative === null) return;
      updates.push({_id: id, initiative: initiative, system: combatant.system});
    }
    if ( !updates.length ) return this;

    // Update multiple combatants
    await this.updateEmbeddedDocuments("Combatant", updates);

    // Ensure the turn order remains with the same combatant
    if ( updateTurn && currentId ) {
      await this.update({turn: this.turns.findIndex(t => t.id === currentId)});
    }

    // Create multiple chat messages
    await DC20ChatMessage.implementation.create(messages);
    return this;
  }

  /** @override **/
  async startCombat() {
    if (!this.flags.dc20rpg?.initiativeDC) {
      ui.notifications.warn(game.i18n.localize("dc20rpg.combatTracker.provideDC"));
      return;
    }

    let numberOfPCs = 0;
    let successPCs = 0;
    this.combatants.forEach(combatant => {
      const actor = combatant.actor;
      if (actor.type === "character") {
        numberOfPCs++;
        successPCs += this._checkInvidualOutcomes(combatant);
      }
      refreshOnCombatStart(actor);
      runEventsFor("combatStart", actor);
      reenableEventsOn("combatStart", actor);
    });

    await this.resetAll();
    if (successPCs >= Math.ceil(numberOfPCs/2)) {
      await initiativeSlotSelector(this, "pc");
      await this.update({["flags.dc20rpg.winningTeam"]: "pc"});
    }
    else {
      await initiativeSlotSelector(this, "enemy");
      await this.update({["flags.dc20rpg.winningTeam"]: "enemy"});
    }
    return await super.startCombat();
  }

  /** @override **/
  async endCombat() {
    await super.endCombat();
    const combatantId = this.current.combatantId;
    const combatant = this.combatants.get(combatantId);
    if (combatant) clearMultipleCheckPenalty(combatant.actor);
  }

  /** @override **/
  //NEW UPDATE CHECK: We need to make sure it works fine with future foundry updates
  async nextTurn() {
    let turn = this.turn ?? -1;
    let skip = this.settings.skipDefeated;

    // Check if should skip next combatant (e.g. When companion shares initiative with the owner) 
    //======== INJECTED =========
    let combatant = {skip: false};
    let next = null;
    do {
      // Determine the next turn number
      if ( skip ) {
        for ( let [i, t] of this.turns.entries() ) {
          if ( i <= turn ) continue;
          if ( t.isDefeated ) continue;
          next = i;
          break;
        }
      }
      else {
        next = turn + 1;
      }
      combatant = this.turns[next];
      turn++;
    } while (combatant !== undefined && combatant.skip);
    //============================

    // Maybe advance to the next round
    let round = this.round;
    if ( (this.round === 0) || (next === null) || (next >= this.turns.length) ) {
      return this.nextRound();
    }

    // Update the document, passing data through a hook first
    const updateData = {round, turn: next};
    const updateOptions = {direction: 1, worldTime: {delta: CONFIG.time.turnTime}};
    Hooks.callAll("combatTurn", this, updateData, updateOptions);
    return this.update(updateData, updateOptions);
  }

  /** @override **/
  //NEW UPDATE CHECK: We need to make sure it works fine with future foundry updates
  async previousTurn() {
    if ( (this.turn === 0) && (this.round === 0) ) return this;
    let previousTurn = (this.turn ?? this.turns.length) - 1;

    // Check if should skip previous combatant (e.g. When companion shares initiative with the owner) 
    //======== INJECTED =========
    let combatant = this.turns[previousTurn];
    while (combatant !== undefined && combatant.skip) {
      previousTurn--;
      combatant = this.turns[previousTurn];
    }
    //===========================

    if ( (previousTurn < 0) && (this.turn !== null) ) return this.previousRound();

    // Update the document, passing data through a hook first
    const updateData = {round: this.round, turn: previousTurn};
    const updateOptions = {direction: -1, worldTime: {delta: -1 * CONFIG.time.turnTime}};
    Hooks.callAll("combatTurn", this, updateData, updateOptions);
    return this.update(updateData, updateOptions);
  }

  /** @override **/
  //NEW UPDATE CHECK: We need to make sure it works fine with future foundry updates
  async previousRound() {
    let turn = ( this.round === 0 ) ? 0 : Math.max(this.turns.length - 1, 0);
    if ( this.turn === null ) turn = null;

    // Check if should skip previous combatant (e.g. When companion shares initiative with the owner) 
    //======== INJECTED =========
    let combatant = this.turns[turn];
    while (combatant !== undefined && combatant.skip) {
      turn--;
      combatant = this.turns[turn];
    }
    //===========================
    let round = Math.max(this.round - 1, 0);
    if ( round === 0 ) turn = null;
    let advanceTime = -1 * (this.turn || 0) * CONFIG.time.turnTime;
    if ( round > 0 ) advanceTime -= CONFIG.time.roundTime;

    // Update the document, passing data through a hook first
    const updateData = {round, turn};
    const updateOptions = {direction: -1, worldTime: {delta: advanceTime}};
    Hooks.callAll("combatRound", this, updateData, updateOptions);
    return this.update(updateData, updateOptions);
  }

  async _onStartTurn(combatant) {
    const actor = combatant.actor;
    await this._respectRoundCounterForEffects();
    this._deathsDoorCheck(actor);
    this._sustainCheck(actor);
    runEventsFor("turnStart", actor);
    reenableEventsOn("turnStart", actor);
    this._runEventsForAllCombatants("actorWithIdStartsTurn", actorIdFilter(actor.id));
    clearHelpDice(actor);
    clearHeldAction(actor);
    await super._onStartTurn(combatant);

    // Run onStartTurn for all linked companions
    if (combatant.companions) combatant.companions.forEach(companionId => {
      const companion = this.combatants.get(companionId);
      if(companion) this._onStartTurn(companion);
    });
  }

  async _onEndTurn(combatant) {
    const actor = combatant.actor;
    const currentRound = this.turn === 0 ? this.round - 1 : this.round; 
    refreshOnRoundEnd(actor);
    runEventsFor("turnEnd", actor);
    runEventsFor("nextTurnEnd", actor, currentRoundFilter(actor, currentRound));
    reenableEventsOn("turnEnd", actor);
    this._runEventsForAllCombatants("actorWithIdEndsTurn", actorIdFilter(actor.id));
    this._runEventsForAllCombatants("actorWithIdEndsNextTurn", actorIdFilter(actor.id), currentRound);
    clearMultipleCheckPenalty(actor);
    clearMovePoints(actor);
    await super._onEndTurn(combatant);

    // Run onEndTurn for all linked companions
    if (combatant.companions) combatant.companions.forEach(companionId => {
      const companion = this.combatants.get(companionId);
      if(companion) this._onEndTurn(companion);
    });
  }

  async _respectRoundCounterForEffects() {
    for (const combatant of this.combatants) {
      const actor = combatant.actor;
      if (!actor) continue;
      for (const effect of actor.temporaryEffects) {
        await effect.respectRoundCounter();
      }
    }
  }

  async _runEventsForAllCombatants(trigger, filters, currentRound) {
    this.combatants.forEach(combatant => {
      const actor = combatant.actor;
      if (currentRound) filters = [...filters, ...currentRoundFilter(actor, currentRound)];
      runEventsFor(trigger, actor, filters);
      reenableEventsOn(trigger, actor, filters);
    });
  }

  async _initiativeForCompanion(combatant) {
    if (companionShare(combatant.actor, "initiative")) {
      const companionOwnerId = combatant.actor.companionOwner.id;
      const owner = this.combatants.find(combatant => combatant.actorId === companionOwnerId);
      if (!owner) ui.notifications.warn("This companion shares initiative with its owner. You need to roll for Initiative for the owner!");
      return null;
    }
    else {
      return this._initiativeRollForPC(combatant);
    }
  }

  _checkInvidualOutcomes(combatant) {
    const initiativeDC = this.flags.dc20rpg.initiativeDC;
    const actor = combatant.actor;

    // Crit Success
    if (combatant.system.crit) {
      sendDescriptionToChat(actor, {
        rollTitle: "Initiative Critical Success",
        image: actor.img,
        description: "You gain ADV on 1 Check or Save of your choice during the first Round of Combat.",
      });
      createEffectOn(this._getInitiativeCritEffectData(actor), actor);
    }
    // Crit Fail
    if (combatant.system.fail) {
      sendDescriptionToChat(actor, {
        rollTitle: "Initiative Critical Fail",
        image: actor.img,
        description: "The first Attack made against you during the first Round of Combat has ADV.",
      });
      createEffectOn(this._getInitiativeCritFailEffectData(actor), actor);
    }
    // Success
    if (combatant.initiative >= initiativeDC) {
      sendDescriptionToChat(actor, {
        rollTitle: "Initiative Success",
        image: actor.img,
        description: "You gain a d6 Inspiration Die, which you can add to 1 Check or Save of your choice that you make during this Combat. The Inspiration Die expires when the Combat ends.",
      });
      prepareHelpAction(actor, {diceValue: 6, doNotExpire: true});
      return true;
    }
    return false;
  }

  async _initiativeRollForPC(combatant) {
    const actor = combatant.actor;

    // TODO: Is initiative choice still an option?
    // const options = {"flat": "Flat d20 Roll", "initiative": "Initiative (Agi + CM)", ...actor.getCheckOptions(true, true, true, true)};
    // const preselected = game.settings.get("dc20rpg", "defaultInitiativeKey");
    // const checkKey = await getSimplePopup("select", {header: game.i18n.localize("dc20rpg.initiative.selectInitiative"), selectOptions: options, preselect: (preselected || "initiative")});
    // if (!checkKey) return null;
    // const details = prepareCheckDetailsFor(checkKey, null, null, "Initiative Roll", options[checkKey]);
    // details.type = "initiative" // For Roll Level Check

    const details = prepareCheckDetailsFor("initiative", null, null, "Initiative Roll");
    const roll = await promptRoll(actor, details);
    if (!roll) return null;

    combatant.system.crit = roll.crit;
    combatant.system.fail = roll.fail;
    return roll.total;
  }

  _getInitiativeCritEffectData(actor) {
    const checkKeys = [
      "martial.melee", "martial.ranged", "spell.melee", "spell.ranged",
      "checks.mig", "checks.agi", "checks.cha", "checks.int", "checks.att", "checks.spe",
      "saves.mig", "saves.agi", "saves.cha", "saves.int"
    ];
    const change = (checkPath) => {
      return {
        key: `system.rollLevel.onYou.${checkPath}`,
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "adv", "label": "Initiative Critical Success", "confirmation": true, "afterRoll": "delete"'
      }
    };

    const changes = [];
    for (const key of checkKeys) {
      changes.push(change(key));
    }

    return {
      label: "Initiative Critical Success",
      img: "icons/svg/angel.svg",
      origin: actor.uuid,
      duration: {
        rounds: 1,
        startRound: 1,
        startTurn: 0,
      },
      "flags.dc20rpg.duration.useCounter": true,
      "flags.dc20rpg.duration.onTimeEnd": "delete",
      description: "You gain ADV on 1 Check or Save of your choice during the first Round of Combat.",
      disabled: false,
      changes: changes
    }
  }

  _getInitiativeCritFailEffectData(actor) {
    const checkKeys = ["martial.melee", "martial.ranged", "spell.melee", "spell.ranged"];
    const change = (checkPath) => {
      return {
        key: `system.rollLevel.againstYou.${checkPath}`,
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "adv", "label": "Initiative Critical Fail", "afterRoll": "delete"'
      }
    };

    const changes = [];
    for (const key of checkKeys) {
      changes.push(change(key));
    }

    return {
      label: "Initiative Critical Fail",
      img: "icons/svg/coins.svg",
      origin: actor.uuid,
      duration: {
        rounds: 1,
        startRound: 1,
        startTurn: 0,
      },
      "flags.dc20rpg.duration.useCounter": true,
      "flags.dc20rpg.duration.onTimeEnd": "delete",
      description: "The first Attack made against you during the first Round of Combat has ADV.",
      disabled: false,
      changes: changes
    }
  }

  _initiativeForNPC() {
    const pcTurns = [];
    const npcTurns = [];
    this.turns.forEach((turn) => {
      if (turn.initiative != null) {
        if (turn.actor.type === "character") pcTurns.push(turn);
        if ((turn.actor.type === "npc")) npcTurns.push(turn);
      }
    });
    
    if (pcTurns.length === 0) {
      ui.notifications.error("At least one PC should be in initiative order at this point!"); 
      return;
    }

    // For nat 1 we want player to always start last.We give them initiative equal to 0 so 0.5 is a minimum value that enemy can get
    const checkOutcome = this._checkWhoGoesFirst();
    // Special case when 2 PC start in initiative order
    if (checkOutcome === "2PC") {
      // Only one PC
      if (pcTurns.length === 1 && !npcTurns[0]) return Math.max(pcTurns[0].initiative - 0.5, 0.5);
      // More than one PC
      for (let i = 1; i < pcTurns.length; i ++) {
        if (!npcTurns[i-1]) return Math.max(pcTurns[i].initiative - 0.5, 0.5);
      }
      // More NPCs than PCs - add those at the end
      if (npcTurns.length >= pcTurns.length - 1) return Math.max(npcTurns[npcTurns.length - 1].initiative - 0.55, 0.5);
    }
    else {
      for (let i = 0; i < pcTurns.length; i ++) {
        if (!npcTurns[i]) {
          // Depending on outcome of encounter check we want enemy to be before or after pcs
          const changeValue = checkOutcome === "PC" ? - 0.5 : 0.5; 
          return Math.max(pcTurns[i].initiative + changeValue, 0.5);
        }
      }
      // More NPCs than PCs - add those at the end
      if (npcTurns.length >= pcTurns.length) return Math.max(npcTurns[npcTurns.length - 1].initiative - 0.55, 0.5); 
    }
  }

  _checkWhoGoesFirst() {
    // Determine who goes first. Players or NPCs
    const turns = this.turns;
    if (turns) {
      let highestPCInitiative;
      for (let i = 0; i < turns.length; i++) {
        if (turns[i].actor.type === "character") {
          highestPCInitiative = turns[i].initiative;
          break;
        }
      }
      if (highestPCInitiative >= this.flags.dc20rpg.initiativeDC + 5) return "2PC";
      else if (highestPCInitiative >= this.flags.dc20rpg.initiativeDC) return "PC";
      else return "ENEMY";
    }
  }

  async _deathsDoorCheck(actor) {
    // Check if actor is on death's door
    const notDead = !actor.hasStatus("dead");
    const deathsDoor = actor.system.death;
    const exhaustion = actor.exhaustion;
    const saveFormula = `d20 - ${exhaustion}`;
    if (deathsDoor.active && notDead) {
      const roll = await promptRollToOtherPlayer(actor, {
        label: game.i18n.localize('dc20rpg.death.save'),
        type: "deathSave",
        against: 10,
        roll: saveFormula
      });

      // Critical Success: You are restored to 1 HP
      if (roll.crit) {
        const health = actor.system.resources.health;
        actor.update({["system.resources.health.current"]: 1});
        sendHealthChangeMessage(actor, Math.abs(health.current) + 1, game.i18n.localize('dc20rpg.death.crit'), "healing");
      }
      // Success (5): You regain 1 HP.
      else if (roll._total >= 15) {
        const health = actor.system.resources.health;
        actor.update({["system.resources.health.current"]: (health.current + 1)});
        sendHealthChangeMessage(actor, 1, game.i18n.localize('dc20rpg.death.success'), "healing");
      }
      // Success: No change.

      // Failure: You gain Bleeding 1.
      if (roll._total < 10) {
        actor.toggleStatusEffect("bleeding", {active: true});
      }

      // Failure (5): You gain Bleeding 2 instead.
      if (roll._total < 5) {
        actor.toggleStatusEffect("bleeding", {active: true});
      }

      // Critical Failure: You also fall Unconscious until you’re restored to 1 HP or higher.
      if (roll.fail) {
        actor.toggleStatusEffect("unconscious", {active: true});
      }
    }
  }

  async _sustainCheck(actor) {
    const currentSustain = actor.system.sustain;
    let sustained = [];
    for (const sustain of currentSustain) {
      const confirmed = await getSimplePopup("confirm", {header: `Do you want to spend 1 AP to sustain '${sustain.name}'?`});
      if (confirmed) {
        const subtracted = await subtractAP(actor, 1);
        if (subtracted) sustained.push(sustain);
        else {
          sendDescriptionToChat(actor, {
            rollTitle: `${sustain.name} - Sustain dropped`,
            image: sustain.img,
            description: `You are no longer sustaining '${sustain.name}' - Not enough AP to sustain`,
          });
        }
      }
      else {
        sendDescriptionToChat(actor, {
          rollTitle: `${sustain.name} - Sustain dropped`,
          image: sustain.img,
          description: `You are no longer sustaining '${sustain.name}'`,
        });
      }
    }

    if (sustained.length !== currentSustain.length) {
      await actor.update({[`system.sustain`]: sustained});
    }
  }
}

class CharacterConfigDialog extends Dialog {

  constructor(actor, dialogData = {}, options = {}) {
    super(dialogData, options);
    this.actor = actor;
    this.updateData = this._perpareUpdateData();
  }

  static get defaultOptions() {
    return foundry.utils.mergeObject(super.defaultOptions, {
      classes: ["dc20rpg", "dialog"],
      height: 600,
      width: 450
    });
  }

  /** @override */
  get template() {
    return `systems/dc20rpg/templates/dialogs/character-config-dialog.hbs`;
  }

  getData() {
    const selectedPrecisionFormula = CONFIG.DC20RPG.SYSTEM_CONSTANTS.precisionDefenceFormulas[this.updateData.defences.precision.formulaKey];
    const selectedAreaFormula = CONFIG.DC20RPG.SYSTEM_CONSTANTS.areaDefenceFormulas[this.updateData.defences.area.formulaKey];

    return {
      ...this.updateData,
      config:  CONFIG.DC20RPG,
      selectedPrecisionFormula: selectedPrecisionFormula,
      selectedAreaFormula: selectedAreaFormula
    }
  }

  activateListeners(html) {
    super.activateListeners(html);
    html.find(".save").click((ev) => this._onSave(ev));
    html.find(".selectable").change(ev => this._onValueChange(datasetOf(ev).path, valueOf(ev)));
    html.find(".input").change(ev => this._onValueChange(datasetOf(ev).path, valueOf(ev)));
    html.find(".numeric-input").change(ev => this._onNumericValueChange(datasetOf(ev).path, valueOf(ev)));
    html.find('.activable').click(ev => this._onActivable(datasetOf(ev).path));
  }

  _perpareUpdateData() {
    const system = this.actor.system;

    // Defences
    const defences = {
      area: {
        formulaKey: system.defences.area.formulaKey,
        normal: system.defences.area.normal,
        customFormula: system.defences.area.customFormula
      },
      precision: {
        formulaKey: system.defences.precision.formulaKey,
        normal: system.defences.precision.normal,
        customFormula: system.defences.precision.customFormula
      }
    };

    // Rest Points 
    const resources = {
      stamina: {
        maxFormula: system.resources.stamina.maxFormula
      },
      mana: {
        maxFormula: system.resources.mana.maxFormula
      },
      grit: {
        maxFormula: system.resources.grit.maxFormula
      },
    };

    // Movements
    const movement = {
      ground: {
        useCustom: system.movement.ground.useCustom,
        value: system.movement.ground.value
      },
      burrow: {
        useCustom: system.movement.burrow.useCustom,
        value: system.movement.burrow.value
      },
      climbing: {
        useCustom: system.movement.climbing.useCustom,
        value: system.movement.climbing.value
      },
      flying: {
        useCustom: system.movement.flying.useCustom,
        value: system.movement.flying.value
      },
      glide: {
        useCustom: system.movement.glide.useCustom,
        value: system.movement.glide.value
      },
      swimming: {
        useCustom: system.movement.swimming.useCustom,
        value: system.movement.swimming.value
      }
    };

    // Jump
    const jump = {
      key: system.jump.key,
      value: system.jump.value
    };

    const size = {
      fromAncestry: system.size.fromAncestry,
      size: system.size.size
    };

    // Senses
    const senses = {
      darkvision: {
        overridenRange: system.senses.darkvision.overridenRange,
        override: system.senses.darkvision.override
      },
      tremorsense: {
        overridenRange: system.senses.tremorsense.overridenRange,
        override: system.senses.tremorsense.override
      },
      blindsight: {
        overridenRange: system.senses.blindsight.overridenRange,
        override: system.senses.blindsight.override
      },
      truesight: {
        overridenRange: system.senses.truesight.overridenRange,
        override: system.senses.truesight.override
      },
    };

    const attributePoints = {
      overridenMax: system.attributePoints.overridenMax,
      override: system.attributePoints.override
    };

    const skillPoints = {
      skill: {
        overridenMax: system.skillPoints.skill.overridenMax,
        override: system.skillPoints.skill.override
      },
      trade: {
        overridenMax: system.skillPoints.trade.overridenMax,
        override: system.skillPoints.trade.override
      },
      language: {
        overridenMax: system.skillPoints.language.overridenMax,
        override: system.skillPoints.language.override
      },
    };

    return {
      defences: defences,
      movement: movement,
      jump: jump,
      size: size,
      senses: senses,
      attributePoints: attributePoints,
      skillPoints: skillPoints,
      resources: resources,
    }
  }

  _onValueChange(path, value) {
    setValueForPath(this.updateData, path, value);
    this.render(true);
  }
  _onNumericValueChange(path, value) {
    const numericValue = parseInt(value);
    setValueForPath(this.updateData, path, numericValue);
    this.render(true);
  }
  _onActivable(pathToValue) {
    const value = getValueFromPath(this.updateData, pathToValue);
    setValueForPath(this.updateData, pathToValue, !value);
    this.render(true);
  }
  _onSave(ev) {
    ev.preventDefault();
    this.actor.update({['system']: this.updateData});
    this.close();
  }
}

function characterConfigDialog(actor) {
  new CharacterConfigDialog(actor, {title: `Configure Character: ${actor.name}`}).render(true);
}

/**
 * Dialog window for different actor configurations.
 */
class ResourceConfigDialog extends Dialog {

  constructor(actor, resourceKey, dialogData = {}, options = {}) {
    super(dialogData, options);
    this.actor = actor;
    this.key = resourceKey;
  }

  static get defaultOptions() {
    return foundry.utils.mergeObject(super.defaultOptions, {
      classes: ["dc20rpg", "dialog"]
    });
  }

  /** @override */
  get template() {
    return "systems/dc20rpg/templates/dialogs/resource-config-dialog.hbs";
  }

  getData() {
    const resourceKey = this.key;
    const resource = this.actor.system.resources.custom[resourceKey];
    const resetTypes = CONFIG.DC20RPG.DROPDOWN_DATA.resetTypes;

    return {
      ...resource,
      resetTypes,
      resourceKey
    }
  }

   /** @override */
  activateListeners(html) {
    super.activateListeners(html);
    html.find(".save").click((ev) => this._onSave(ev));
    html.find(".selectable").change(ev => this._onValueChange(datasetOf(ev).path, valueOf(ev)));
    html.find(".input").change(ev => this._onValueChange(datasetOf(ev).path, valueOf(ev)));
    html.find(".numeric-input").change(ev => this._onNumericValueChange(datasetOf(ev).path, valueOf(ev)));
  }

  _onSave(event) {
    event.preventDefault();
    const updatePath = `system.resources.custom.${this.key}`;
    const updateData = getValueFromPath(this.actor, updatePath);
    if (updateData.bonus) delete updateData.bonus; // We do not want to commit any bonuses
    this.actor.update({ [updatePath] : updateData });
    this.close();
  }

  _onValueChange(path, value) {
    setValueForPath(this.actor, path, value);
    this.render(true);
  }

  _onNumericValueChange(path, value) {
    const numericValue = parseInt(value);
    setValueForPath(this.actor, path, numericValue);
    this.render(true);
  }
}

function resourceConfigDialog(actor, resourceKey) {
  new ResourceConfigDialog(actor, resourceKey, {title: "Configure Resource"}).render(true);
}

function closeContextMenu(html) {
  const contextMenu = html.find('#context-menu');
  if (contextMenu[0]) contextMenu[0].style.visibility = "hidden";
}

function itemContextMenu(item, event, html) {
  if (item.type === "basicAction") return; // We dont want to open context menu for basic actions

  // Prepare content
  let content = '';

  // Edit/Remove Item
  content += `<a class="elem item-edit"><i class="fas fa-edit"></i><span>${game.i18n.localize('dc20rpg.sheet.items.editItem')}</span></a>`;
  if (!["ancestry", "class", "subclass", "background"].includes(item.type)) {
    content += `<a class="elem item-copy"><i class="fas fa-copy"></i><span>${game.i18n.localize('dc20rpg.sheet.items.copyItem')}</span></a>`;
  }
  content += `<a class="elem item-delete"><i class="fas fa-trash"></i><span>${game.i18n.localize('dc20rpg.sheet.items.deleteItem')}</span></a>`;
  
  // Prepare Equip/Attune
  const statuses = item.system.statuses;
  if (statuses) {
    const equippedTitle = statuses.equipped ? game.i18n.localize(`dc20rpg.sheet.itemTable.unequipItem`) : game.i18n.localize(`dc20rpg.sheet.itemTable.equipItem`);
    content += `<a class="elem item-activable" data-path="system.statuses.equipped"><i class="fa-solid fa-suitcase-rolling"></i><span>${equippedTitle}</span></a>`;

    if (item.system.properties.attunement.active) {
      const attunedTitle = statuses.attuned ? game.i18n.localize(`dc20rpg.sheet.itemTable.unattuneItem`) : game.i18n.localize(`dc20rpg.sheet.itemTable.attuneItem`);
      content += `<a class="elem item-activable" data-path="system.statuses.attuned"><i class="fa-solid fa-hat-wizard"></i><span>${attunedTitle}</span></a>`;
    }
  }

  // Mark Favorite
  if (!["ancestry", "class", "subclass", "background"].includes(item.type)) {
    const favoriteTitle = item.flags.dc20rpg.favorite ? game.i18n.localize(`dc20rpg.sheet.itemTable.removeFavorite`) : game.i18n.localize(`dc20rpg.sheet.itemTable.addFavorite`);
    content += `<a class="elem item-activable" data-path="flags.dc20rpg.favorite"><i class="fa-solid fa-star"></i><span>${favoriteTitle}</span></a>`;
  }
  _showContextMenu(content, event, html, item);
}

function _showContextMenu(content, event, html, item) {
  event.preventDefault();
  const contextMenu = html.find('#context-menu');

  // Fill Content
  contextMenu.html(content);

  // Set position
  const x = event.pageX;
  const y = event.pageY;
  contextMenu[0].style.left = (x + 10) + "px";
  contextMenu[0].style.top = (y + 6) + "px";

  _addEventListener(contextMenu, item);

  // Show Menu
  contextMenu[0].style.visibility = "visible";
}

function _addEventListener(contextMenu, item) {
  contextMenu.find('.item-delete').click(() => item.delete());
  contextMenu.find('.item-edit').click(() => item.sheet.render(true));
  contextMenu.find('.item-copy').click(() => Item.create(item, { parent: item.parent }));
  contextMenu.find('.item-activable').click((ev) => {
    const path = datasetOf(ev).path;
    let value = getValueFromPath(item, path);
    item.update({[path]: !value});
  });
}

class MixAncestryDialog extends Dialog {

  constructor(dialogData = {}, options = {}) {
    super(dialogData, options);
    this.ancestryCollection = {};
  }

  static get defaultOptions() {
    return foundry.utils.mergeObject(super.defaultOptions, {
      classes: ["dc20rpg", "dialog"]
    });
  }

  /** @override */
  get template() {
    return "systems/dc20rpg/templates/dialogs/mix-ancestry-dialog.hbs";
  }

  getData() {
    return {
      ancestryCollection: this.ancestryCollection,
      canMix: Object.keys(this.ancestryCollection).length === 2
    }
  }

   /** @override */
  activateListeners(html) {
    super.activateListeners(html);
    html.find(".mix").click(ev => this._onMix(ev));
    html.find(".remove-item").click(ev => this._onItemRemoval(datasetOf(ev).id));
    html.find('.open-compendium').click(ev => createItemBrowser("ancestry", true, this));

    // Drag and drop events
    html[0].addEventListener('dragover', ev => ev.preventDefault());
    html[0].addEventListener('drop', async ev => await this._onDrop(ev));
  }

  async _onMix(event) {
    event.preventDefault();
    const [first, second] = Object.values(this.ancestryCollection);
    const ancestryData = mixAncestry(first, second);
    
    if (ancestryData) {
      this.promiseResolve(ancestryData);
      this.close();
    }
  }

  async _onDrop(event) {
    event.preventDefault();
    const droppedData  = event.dataTransfer.getData('text/plain');
    if (!droppedData) return;
    
    const droppedObject = JSON.parse(droppedData);
    if (droppedObject.type !== "Item") return;

    const item = await Item.fromDropData(droppedObject);
    if (item.type !== "ancestry") return;

    if (Object.keys(this.ancestryCollection).length >= 2) return; // For now we only want to merge two ancestries, no more
    this.ancestryCollection[item.id] = item;
    this.render();
  }

  _onItemRemoval(id) {
    delete this.ancestryCollection[id];
    this.render();
  }

  static async create(dialogData = {}, options = {}) {
    const dialog = new MixAncestryDialog(dialogData, options);
    return new Promise((resolve) => {
      dialog.promiseResolve = resolve;
      dialog.render(true);
    });
  }

  /** @override */
  close(options) {
    if (this.promiseResolve) this.promiseResolve(null);
    super.close(options);
  }
}

async function createMixAncestryDialog() {
  return await MixAncestryDialog.create({title: "Mix Ancestry"});
}

async function addUpdateItemToKeyword(actor, keyword, itemId, value) {
  let kw = actor.system.keywords[keyword];
  if (!kw) {
    kw = {
      value: value,
      updateItems: [itemId]
    };
  }
  else {
    kw.updateItems.push(itemId);
  }
  actor.update({[`system.keywords.${keyword}`]: kw});
}

async function removeUpdateItemFromKeyword(actor, keyword, itemId) {
  let kw = actor.system.keywords[keyword];
  if (!kw) return;

  const updateItems = new Set(kw.updateItems);
  updateItems.delete(itemId);
  if (updateItems.size !== 0) actor.update({[`system.keywords.${keyword}.updateItems`]: Array.from(updateItems)});
  else actor.update({[`system.keywords.-=${keyword}`]: null});
}

async function updateKeywordValue(actor, keyword, newValue) {
  let kw = actor.system.keywords[keyword];
  if (!kw) return;

  const updateItems = kw.updateItems;
  for (const itemId of updateItems) {
    const item = actor.items.get(itemId);
    if (item) await runTemporaryItemMacro(item, "onKeywordUpdate", actor, {keyword: keyword, newValue: newValue});
  }
  await actor.update({[`system.keywords.${keyword}.value`]: newValue});
}

function removeKeyword(actor, keyword) {
  actor.update({[`system.keywords.-=${keyword}`]: null});
}

function addNewKeyword(actor, keyword, value) {
  let kw = actor.system.keywords[keyword];
  if (kw) return;

  actor.update({[`system.keywords.${keyword}`]: {
    value: value,
    updateItems: []
  }});
}

class KeywordEditor extends Dialog {

  constructor(actor, dialogData = {}, options = {}) {
    super(dialogData, options);
    this.actor = actor;
    this.updateData = this._perpareUpdateData();
  }

  static get defaultOptions() {
    return foundry.utils.mergeObject(super.defaultOptions, {
      classes: ["dc20rpg", "dialog"],
      height: 500,
      width: 400
    });
  }

  /** @override */
  get template() {
    return `systems/dc20rpg/templates/dialogs/keyword-editor-dialog.hbs`;
  }

  getData() {
    return {
      keywords: this.updateData,
    }
  }

  activateListeners(html) {
    super.activateListeners(html);
    activateDefaultListeners(this, html);
    html.find(".add-keyword").change(ev => this._onKeyword(valueOf(ev), true));
    html.find(".remove-keyword").click(ev => this._onKeyword(datasetOf(ev).keyword, false));
    html.find(".add-item-id").change(ev => this._onItemId(valueOf(ev), datasetOf(ev).keyword, true));
    html.find(".remove-item-id").click(ev => this._onItemId(datasetOf(ev).itemId, this._extractKeyword(ev), false));
    html.find(".save").click((ev) => this._onSave(ev));
  }

  _perpareUpdateData() {
    const keywords = foundry.utils.deepClone(this.actor.system.keywords);
    return keywords;
  }

  _onKeyword(keyword, add) {
    if (add) {
      this.updateData[keyword] = {
        value: "",
        updateItems: []
      };
    }
    else {
      delete this.updateData[keyword];
    }
    this.render();
  }

  _onItemId(itemId, keyword, add) {
    this.updateData[keyword].updateItems.push(itemId);
    const updateItems = this.updateData[keyword]?.updateItems;
    if (!updateItems) return;

    const asSet = new Set(updateItems);
    if (add) asSet.add(itemId);
    else asSet.delete(itemId);

    this.updateData[keyword].updateItems = Array.from(asSet);
    this.render();
  }

  async _onSave(ev) {
    ev.preventDefault();
    this.close();

    const currentKeywords = this.actor.system.keywords;
    const newKeywords = this.updateData;
    const removedKeywords = Object.keys(currentKeywords).filter(keyword => newKeywords[keyword] === undefined);

    for (const [key, newKeyword] of Object.entries(newKeywords)) {
      // Update Keyword
      await this.actor.update({[`system.keywords.${key}`]: newKeyword});
      
      // Call update on items linked to keyword
      for (const itemId of newKeyword.updateItems) {
        const item = this.actor.items.get(itemId);
        if (item) runTemporaryItemMacro(item, "onKeywordUpdate", this.actor, {keyword: key, newValue: newKeyword.value});
      }
    }

    // Remove deleted keywords
    for (const removed of removedKeywords) {
      await removeKeyword(this.actor, removed);
    }
  }

  _extractKeyword(ev) {
    return ev.currentTarget.parentElement?.parentElement?.dataset?.keyword || "";
  }
}

function keywordEditor(actor) {
  new KeywordEditor(actor, {title: `Keywords Editor: ${actor.name}`}).render(true);
}

function activateCommonLinsters$1(html, actor) {
  // Core funcionalities
  html.find(".activable").click(ev => changeActivableProperty(datasetOf(ev).path, actor));
  html.find(".item-activable").click(ev => changeActivableProperty(datasetOf(ev).path, getItemFromActor(datasetOf(ev).itemId, actor)));
  html.find('.rollable').click(ev => promptRoll(actor, datasetOf(ev), ev.shiftKey));
  html.find('.roll-item').click(ev => promptItemRoll(actor, getItemFromActor(datasetOf(ev).itemId, actor), ev.shiftKey));
  html.find('.toggle-item-numeric').mousedown(ev => toggleUpOrDown(datasetOf(ev).path, ev.which, getItemFromActor(datasetOf(ev).itemId, actor), (datasetOf(ev).max || 9), 0));
  html.find('.toggle-actor-numeric').mousedown(ev => toggleUpOrDown(datasetOf(ev).path, ev.which, actor, (datasetOf(ev).max || 9), 0));
  html.find('.ap-for-adv-item').mousedown(ev => advForApChange(getItemFromActor(datasetOf(ev).itemId, actor), ev.which));
  html.find('.ap-for-adv').mousedown(ev => advForApChange(actor, ev.which));
  html.find('.change-actor-value').change(ev => changeValue(valueOf(ev), datasetOf(ev).path, actor));
  html.find('.change-item-numeric-value').change(ev => changeNumericValue(valueOf(ev), datasetOf(ev).path, getItemFromActor(datasetOf(ev).itemId, actor)));
  html.find('.change-actor-numeric-value').change(ev => changeNumericValue(valueOf(ev), datasetOf(ev).path, actor));
  html.find('.update-charges').change(ev => changeCurrentCharges(valueOf(ev), getItemFromActor(datasetOf(ev).itemId, actor)));
  html.find('.recharge-item').click(ev => rechargeItem(getItemFromActor(datasetOf(ev).itemId, actor), false));
  html.find('.initiative-roll').click(() => actor.rollInitiative({createCombatants: true, rerollInitiative: true}));

  // Items 
  html.find('.item-create').click(ev => _onItemCreate(datasetOf(ev).tab, actor));
  html.find('.item-delete').click(ev => deleteItemFromActor(datasetOf(ev).itemId, actor));
  html.find('.item-edit').click(ev => editItemOnActor(datasetOf(ev).itemId, actor));
  html.find('.item-copy').click(ev => duplicateItem(datasetOf(ev).itemId, actor));
  html.find('.editable').mousedown(ev => {
    if (ev.which === 2) editItemOnActor(datasetOf(ev).itemId, actor);
    if (ev.which === 3) itemContextMenu(getItemFromActor(datasetOf(ev).itemId, actor), ev, html);
  });
  html.find('.run-on-demand-macro').click(ev => runTemporaryItemMacro(getItemFromActor(datasetOf(ev).itemId, actor), "onDemand", actor));
  html.click(ev => closeContextMenu(html)); // Close context menu
  html.find(".reorder").click(ev => reorderTableHeaders(datasetOf(ev).tab, datasetOf(ev).current, datasetOf(ev).swapped, actor));
  html.find('.table-create').click(ev => createNewTable(datasetOf(ev).tab, actor));
  html.find('.table-remove').click(ev => removeCustomTable(datasetOf(ev).tab, datasetOf(ev).table, actor));
  html.find('.select-other-item').change(ev => changeValue(html.find(`.${datasetOf(ev).selector} option:selected`).val(), datasetOf(ev).path, getItemFromActor(datasetOf(ev).itemId, actor)));
  html.find('.select-other-item').click(ev => {ev.preventDefault(); ev.stopPropagation();});
  html.find('.select-other-check').change(ev => changeValue(html.find(`.${datasetOf(ev).selector} option:selected`).val(), datasetOf(ev).path, getItemFromActor(datasetOf(ev).itemId, actor)));
  html.find('.select-other-check').click(ev => {ev.preventDefault(); ev.stopPropagation();});
  html.find('.item-multi-faceted').click(ev => {ev.stopPropagation(); getItemFromActor(datasetOf(ev).itemId, actor).swapMultiFaceted();});
  html.find('.open-compendium').click(ev => createItemBrowser(datasetOf(ev).itemType, datasetOf(ev).unlock !== "true", actor.sheet));
  html.find('.reload-weapon').click(ev => reloadWeapon(getItemFromActor(datasetOf(ev).itemId, actor), actor));
  
  // Resources
  html.find(".use-ap").click(() => subtractAP(actor, 1));
  html.find(".regain-ap").click(() => regainBasicResource("ap", actor, 1, "true"));
  html.find(".regain-all-ap").click(() => refreshAllActionPoints(actor));
  html.find(".edit-max-ap").change(ev => {
    changeNumericValue(valueOf(ev), "system.resources.ap.value", actor);
    changeNumericValue(valueOf(ev), "system.resources.ap.max", actor);
  });
  html.find(".regain-resource").click(ev => regainBasicResource(datasetOf(ev).key, actor, datasetOf(ev).amount, datasetOf(ev).boundary));
  html.find(".spend-resource").click(ev => subtractBasicResource(datasetOf(ev).key, actor, datasetOf(ev).amount, datasetOf(ev).boundary));
  html.find(".spend-regain-resource").mousedown(ev => {
    if (ev.which === 3) subtractBasicResource(datasetOf(ev).key, actor, datasetOf(ev).amount, datasetOf(ev).boundary);
    if (ev.which === 1) regainBasicResource(datasetOf(ev).key, actor, datasetOf(ev).amount, datasetOf(ev).boundary);
  });
  html.find(".rest-point-to-hp").click(async ev => {datasetOf(ev); await spendRpOnHp(actor, 1);});

  // Custom Resources
  html.find(".add-custom-resource").click(() => createNewCustomResource("New Resource", actor));
  html.find('.edit-resource').click(ev => resourceConfigDialog(actor, datasetOf(ev).key));
  html.find(".remove-resource").click(ev => removeResource(datasetOf(ev).key, actor));
  html.find(".edit-resource-img").click(ev => changeResourceIcon(datasetOf(ev).key, actor));
  html.find(".spend-regain-custom-resource").mousedown(ev => {
    if (ev.which === 3) subtractCustomResource(datasetOf(ev).key, actor, 1, "true");
    if (ev.which === 1) regainCustomResource(datasetOf(ev).key, actor, 1, "true");
  });

  // Active Effects
  html.find(".effect-create").click(ev => createNewEffectOn(datasetOf(ev).type, actor));
  html.find(".effect-toggle").click(ev => toggleEffectOn(datasetOf(ev).effectId, actor, datasetOf(ev).turnOn === "true"));
  html.find(".effect-edit").click(ev => editEffectOn(datasetOf(ev).effectId, actor));
  html.find('.editable-effect').mousedown(ev => ev.which === 2 ? editEffectOn(datasetOf(ev).effectId, actor) : ()=>{});
  html.find(".effect-delete").click(ev => deleteEffectFrom(datasetOf(ev).effectId, actor));
  html.find(".status-toggle").mousedown(ev => toggleStatusOn(datasetOf(ev).statusId, actor, ev.which));
  
  // Skills
  html.find(".expertise-toggle").click(ev => manualSkillExpertiseToggle(datasetOf(ev).key, actor, datasetOf(ev).type));
  html.find(".skill-mastery-toggle").mousedown(ev => toggleSkillMastery(datasetOf(ev).type, datasetOf(ev).key, ev.which, actor));
  html.find(".language-mastery-toggle").mousedown(ev => toggleLanguageMastery(datasetOf(ev).path, ev.which, actor));
  html.find(".skill-point-converter").click(ev => convertSkillPoints(actor, datasetOf(ev).from, datasetOf(ev).to, datasetOf(ev).operation, datasetOf(ev).rate));
  html.find('.add-skill').click(() => addCustomSkill(actor, false));
  html.find('.remove-skill').click(ev => removeCustomSkill(datasetOf(ev).key, actor, false));
  html.find('.add-trade').click(() => addCustomSkill(actor, true));
  html.find('.remove-trade').click(ev => removeCustomSkill(datasetOf(ev).key, actor, true));
  html.find('.add-language').click(() => addCustomLanguage(actor));
  html.find('.remove-language').click(ev => removeCustomLanguage(datasetOf(ev).key, actor));

  // Sidetab
  html.find(".sidetab-button").click(ev => _onSidetab(ev));
  html.find(".show-img").click(() => new ImagePopout(actor.img, { title: actor.name, uuid: actor.uuid }).render(true));
  html.find('.mix-ancestry').click(async () => {
    const ancestryData = await createMixAncestryDialog();
    if (ancestryData) await createItemOnActor(actor, ancestryData);
  });

  // Tooltips
  html.find('.item-tooltip').hover(ev => itemTooltip(getItemFromActor(datasetOf(ev).itemId, actor), ev, html, {inside: datasetOf(ev).inside === "true"}), ev => hideTooltip(ev, html));
  html.find('.enh-tooltip').hover(ev => enhTooltip(getItemFromActor(datasetOf(ev).itemId, actor), datasetOf(ev).enhKey, ev, html), ev => hideTooltip(ev, html));
  html.find('.effect-tooltip').hover(ev => effectTooltip(getEffectFrom(datasetOf(ev).effectId, actor), ev, html), ev => hideTooltip(ev, html));
  html.find('.text-tooltip').hover(ev => textTooltip(datasetOf(ev).text, datasetOf(ev).title, datasetOf(ev).img, ev, html), ev => hideTooltip(ev, html));
  html.find('.journal-tooltip').hover(ev => journalTooltip(datasetOf(ev).uuid, datasetOf(ev).header, datasetOf(ev).img, ev, html, {inside: datasetOf(ev).inside === "true"}), ev => hideTooltip(ev, html));
}

function activateCharacterLinsters(html, actor) {
  // Header - Top Buttons
  html.find(".rest").click(() => createRestDialog(actor));
  html.find(".level").click(async ev => {
    if (datasetOf(ev).up !== "true") {
      const confirmed = await getSimplePopup("confirm", {header: "Do you want to level down?"});
      if (!confirmed) return;
    }
    changeLevel(datasetOf(ev).up, datasetOf(ev).itemId, actor);
  });
  html.find(".rerun-advancement").click(ev => rerunAdvancement(actor, datasetOf(ev).classId));
  html.find(".configuration").click(() => characterConfigDialog(actor));
  html.find(".keyword-editor").click(() => keywordEditor(actor));

  // Attributes
  html.find('.subtract-attribute-point').click(ev => manipulateAttribute(datasetOf(ev).key, actor, true));
  html.find('.add-attribute-point').click(ev => manipulateAttribute(datasetOf(ev).key, actor, false));
}

function activateNpcLinsters(html, actor) {
  // Custom Resources
  html.find(".add-legendary-resources").click(() => createLegenedaryResources(actor));
}

function activateCompanionListeners(html, actor) {
  const getTrait = (actor, traitKey) => actor.system?.traits[traitKey];

  html.find(".trait-tooltip").hover(ev => traitTooltip(getTrait(actor, datasetOf(ev).traitKey), ev, html, {inside: datasetOf(ev).inside === "true"}), ev => hideTooltip(ev, html));
  html.find(".activable-trait").mousedown(ev => {
    if (ev.which === 1) activateTrait(datasetOf(ev).traitKey, actor);
    if (ev.which === 3) deactivateTrait(datasetOf(ev).traitKey, actor);
  });
  html.find(".trait-delete").click(ev =>  deleteTrait(datasetOf(ev).traitKey, actor));
  html.find(".trait-repeatable").click(ev => {
    const trait = getTrait(actor, datasetOf(ev).traitKey);
    actor.update({[`system.traits.${datasetOf(ev).traitKey}.repeatable`]: !trait.repeatable});
  });
  html.find(".remove-companion-owner").click(() => actor.update({["system.companionOwnerId"]: ""}));
}

function _onSidetab(ev) {
  const icon = ev.currentTarget;
  const sidebar = ev.currentTarget.parentNode;
  sidebar.classList.toggle("expand");
  icon.classList.toggle("fa-square-caret-left");
  icon.classList.toggle("fa-square-caret-right");
  const isExpanded = sidebar.classList.contains("expand");
  game.user.setFlag("dc20rpg", "sheet.character.sidebarCollapsed", !isExpanded);
}

async function _onItemCreate(tab, actor) {
  let selectOptions = CONFIG.DC20RPG.DROPDOWN_DATA.creatableTypes;
  switch(tab) {
    case "inventory":   selectOptions = CONFIG.DC20RPG.DROPDOWN_DATA.inventoryTypes; break;
    case "features":    selectOptions = CONFIG.DC20RPG.DROPDOWN_DATA.featuresTypes; break;
    case "techniques":  selectOptions = CONFIG.DC20RPG.DROPDOWN_DATA.techniquesTypes; break;
    case "spells":      selectOptions = CONFIG.DC20RPG.DROPDOWN_DATA.spellsTypes; break; 
  }

  const itemType = await getSimplePopup("select", {header: game.i18n.localize("dc20rpg.dialog.create.itemType"), selectOptions});
  if (!itemType) return;

  const itemData = {
    type: itemType,
    name: `New ${getLabelFromKey(itemType, CONFIG.DC20RPG.DROPDOWN_DATA.creatableTypes)}`
  };
  createItemOnActor(actor, itemData);
}

function duplicateData(context, actor) {
  context.config = CONFIG.DC20RPG;
  context.type = actor.type;
  context.system = actor.system;
  context.flags = actor.flags;
  context.editMode = context.flags.dc20rpg?.editMode;
  context.expandedSidebar = !game.user.getFlag("dc20rpg", "sheet.character.sidebarCollapsed");
  context.weaponsOnActor = actor.getWeapons();
}

function prepareCommonData(context) {
  _damageReduction(context);
  _statusResistances(context);
  _resourceBarsPercentages(context);
  _oneliners(context);
  _attributes(context);
  _size(context);
}

function prepareCharacterData(context) {
  _skills(context);
  _tradeSkills(context);
  _languages(context);
}

function prepareNpcData(context) {
  _allSkills(context);
  _languages(context);
}

function prepareCompanionData(context) {
  context.shareWithCompanionOwner = _shareOptionsSimplyfied(context.system.shareWithCompanionOwner, "");
}

function _shareOptionsSimplyfied(options, prefix) {
  const simplified = [];
  Object.entries(options).forEach(([key, option]) => {
    if (typeof option === "object") {
      simplified.push(..._shareOptionsSimplyfied(option, key));
    }
    else {
      const finalKey = prefix ? `${prefix}.${key}` : key;
      simplified.push({
        key: finalKey,
        active: option,
        label: game.i18n.localize(`dc20rpg.sheet.companionConfig.${prefix}${key}`),
      });
    }
  });
  return simplified;
}

function _damageReduction(context) {
  const dmgTypes = context.system.damageReduction.damageTypes;
  for (const [key, dmgType] of Object.entries(dmgTypes)) {
    dmgType.notEmpty = false;
    if (dmgType.immune) dmgType.notEmpty = true;
    if (dmgType.resistance) dmgType.notEmpty = true;
    if (dmgType.vulnerability) dmgType.notEmpty = true;
    if (dmgType.vulnerable) dmgType.notEmpty = true;
    if (dmgType.resist) dmgType.notEmpty = true;
  }
}

function _statusResistances(context) {
  const statusResistances = context.system.statusResistances;
  for (const [key, status] of Object.entries(statusResistances)) {
    status.notEmpty = false;
    if (status.immunity) status.notEmpty = true;
    if (status.resistance) status.notEmpty = true;
    if (status.vulnerability) status.notEmpty = true;
  }
}

function _resourceBarsPercentages(context) {
  const resources = context.system.resources;

  const hpCurrent = resources.health.current;
  const hpMax = resources.health.max;
  const hpPercent = Math.ceil(100 * hpCurrent/hpMax);
  if (isNaN(hpPercent)) resources.health.percent = 0;
  else resources.health.percent = hpPercent <= 100 ? hpPercent : 100;

  const hpValue = resources.health.value;
  const hpPercentTemp = Math.ceil(100 * hpValue/hpMax);
  if (isNaN(hpPercent)) resources.health.percentTemp = 0;
  else resources.health.percentTemp = hpPercentTemp <= 100 ? hpPercentTemp : 100;

  if (!resources.mana) return;
  const manaCurrent = resources.mana.value;
  const manaMax = resources.mana.max;
  const manaPercent = Math.ceil(100 * manaCurrent/manaMax);
  if (isNaN(manaPercent)) resources.mana.percent = 0;
  else resources.mana.percent = manaPercent <= 100 ? manaPercent : 100;

  if (!resources.stamina) return;
  const staminaCurrent = resources.stamina.value;
  const staminaMax = resources.stamina.max;
  const staminaPercent = Math.ceil(100 * staminaCurrent/staminaMax);
  if (isNaN(staminaPercent)) resources.stamina.percent = 0;
  else resources.stamina.percent = staminaPercent <= 100 ? staminaPercent : 100;

  if (!resources.grit) return;
  const gritCurrent = resources.grit.value;
  const gritMax = resources.grit.max;
  const gritPercent = Math.ceil(100 * gritCurrent/gritMax);
  if (isNaN(gritPercent)) resources.grit.percent = 0;
  else resources.grit.percent = gritPercent <= 100 ? gritPercent : 100;
}

function _oneliners(context) {
  const oneliners = {
    damageReduction: {},
    statusResistances: {}
  };

  const dmgRed = Object.entries(context.system.damageReduction.damageTypes)
                    .map(([key, reduction]) => [key, _prepReductionOneliner(reduction)])
                    .filter(([key, oneliner]) => oneliner);

  const statusResistances = Object.entries(context.system.statusResistances)
                      .map(([key, condition]) => [key, _prepConditionsOneliners(condition)])
                      .filter(([key, oneliner]) => oneliner);

  oneliners.damageReduction = Object.fromEntries(dmgRed);
  oneliners.statusResistances = Object.fromEntries(statusResistances);
  context.oneliners = oneliners;
}

function _attributes(context) {
  const attributes = context.system.attributes;

  context.attributes = {
    mig: attributes.mig,
    cha: attributes.cha,
    agi: attributes.agi,
    int: attributes.int
  };
}

function _size(context) {
  const size = context.system.size.size;
  const label = size === "mediumLarge" 
                  ? getLabelFromKey("large", CONFIG.DC20RPG.DROPDOWN_DATA.sizes)
                  : getLabelFromKey(context.system.size.size, CONFIG.DC20RPG.DROPDOWN_DATA.sizes);
  context.system.size.label = label;
}

function _allSkills(context) {
  const skills = Object.entries(context.system.skills)
                  .map(([key, skill]) => [key, _prepSkillMastery(skill)]);
  context.skills = {
    allSkills: Object.fromEntries(skills)
  };
}

function _skills(context) {
  const skills = Object.entries(context.system.skills)
                  .map(([key, skill]) => [key, _prepSkillMastery(skill)]);
  context.skills = {
    skills: Object.fromEntries(skills)
  };
}

function _tradeSkills(context) {
  const trade = Object.entries(context.system.tradeSkills)
                  .map(([key, skill]) => [key, _prepSkillMastery(skill)]);
  context.skills.trade = Object.fromEntries(trade);
}

function _languages(context) {
  const languages = Object.entries(context.system.languages)
                  .map(([key, skill]) => [key, _prepLangMastery(skill)]);
  context.skills.languages = Object.fromEntries(languages);
}

function _prepSkillMastery(skill) {
  let mastery = foundry.utils.deepClone(skill.mastery);
  
  skill.short = CONFIG.DC20RPG.SYSTEM_CONSTANTS.skillMasteryShort[mastery];
  skill.masteryLabel = CONFIG.DC20RPG.SYSTEM_CONSTANTS.skillMasteryLabel[mastery];
  return skill;
}

function _prepLangMastery(lang) {
  const mastery = lang.mastery;
  lang.short = CONFIG.DC20RPG.SYSTEM_CONSTANTS.languageMasteryShort[mastery];
  lang.masteryLabel = CONFIG.DC20RPG.SYSTEM_CONSTANTS.languageMasteryLabel[mastery];
  return lang;
}

function _prepReductionOneliner(reduction) {
  if (reduction.immune) return `${reduction.label} ${game.i18n.localize("dc20rpg.sheet.dmgTypes.immune")}`;

  let oneliner = "";

  // Resist / Vulnerable
  const reductionX = reduction.resist - reduction.vulnerable;
  if (reductionX > 0) {
    let typeLabel = game.i18n.localize("dc20rpg.sheet.dmgTypes.resistanceX");
    typeLabel = typeLabel.replace("X", reductionX);
    oneliner += `${reduction.label} ${typeLabel}`;
  }
  if (reductionX < 0) {
    let typeLabel = game.i18n.localize("dc20rpg.sheet.dmgTypes.vulnerabilityX");
    typeLabel = typeLabel.replace("X", Math.abs(reductionX));
    oneliner += `${reduction.label} ${typeLabel}`;
  }

  // Reduction / Vulnerability 
  if (reduction.vulnerability && !reduction.resistance) {
    if (oneliner) oneliner += ` & ${game.i18n.localize("dc20rpg.sheet.dmgTypes.vulnerabilityHalf")}`;
    else oneliner += `${reduction.label} ${game.i18n.localize("dc20rpg.sheet.dmgTypes.vulnerabilityHalf")}`;
  }
  if (reduction.resistance && !reduction.vulnerability) {
    if (oneliner) oneliner += ` & ${game.i18n.localize("dc20rpg.sheet.dmgTypes.resistanceHalf")}`;
    else oneliner += `${reduction.label} ${game.i18n.localize("dc20rpg.sheet.dmgTypes.resistanceHalf")}`;
  }
  return oneliner;
}

function _prepConditionsOneliners(condition) {
  if (condition.immunity) return `${condition.label} ${game.i18n.localize("dc20rpg.sheet.condImm.immunity")}`;
  const resistance = condition.resistance || 0;
  const vulnerability = condition.vulnerability || 0;
  const finalLevel = resistance - vulnerability;

  if (finalLevel > 0) {
    let typeLabel = game.i18n.localize("dc20rpg.sheet.condImm.resistanceX");
    typeLabel = typeLabel.replace("X", Math.abs(finalLevel));
    return `${condition.label} ${typeLabel}`;
  }
  if (finalLevel < 0) {
    let typeLabel = game.i18n.localize("dc20rpg.sheet.condImm.vulnerabilityX");
    typeLabel = typeLabel.replace("X", Math.abs(finalLevel));
    return `${condition.label} ${typeLabel}`;
  }
  return ""
}

async function fillPdfFrom(actor) {
  if (!actor) return;

  const pdf = PDFLib.PDFDocument;
  const sheetPath = "systems/dc20rpg/pdf/fillable-character-sheet.pdf";
  const sheetPdfBytes = await fetch(sheetPath).then(res => res.arrayBuffer());

  // Load the PDF document
  const pdfDoc = await pdf.load(sheetPdfBytes);
  const form = pdfDoc.getForm();
  const itemCards = [];
  _setValues(form, actor, itemCards);

  // Item cards to pdf
  const LINES_PER_PAGE = 62;
  let linesOnPage = 62;
  let pageName = "Item-Card-0";
  let pageText = "";
  let pageCounter = 0;
  for (const card of itemCards) {
    let currentLines = 3;
    currentLines += _calculateLineCount(card.description, 84);

    if (linesOnPage + currentLines > LINES_PER_PAGE) {
      // Add current text to the page
      if (pageText) _text(form.getTextField(pageName), pageText);
      pageText = "";
      
      // Prepare next page
      linesOnPage = currentLines;
      pageCounter++;
      pageName = `Item-Card-${pageCounter}`;
      _addNewPage(pdfDoc, pageName);
    }
    else {
      linesOnPage += currentLines;
    }

    pageText += "=================================================================================\n";
    pageText += `${card.name}  ${card.details}\n`;
    pageText += "=================================================================================";
    pageText += card.description;
    pageText += "\n\n";
  }
  _text(form.getTextField(pageName), pageText); // Fill last page

  const pdfBytes = await pdfDoc.save();
  const blob = new Blob([pdfBytes], { type: "application/pdf" });
  const link = document.createElement("a");
  link.href = URL.createObjectURL(blob);
  link.download = `${actor.name}-character-sheet.pdf`;
  link.click();
}

function _addNewPage(pdfDoc, textFieldName) {
  const width = 595;  // A4 width
  const height = 842; // A4 height

  const page = pdfDoc.addPage([width, height]);
  let textField = pdfDoc.getForm().createTextField(textFieldName);
  textField.addToPage(page, {
    x: 10,
    y: 10,  
    width: width - 20,
    height: height - 20,
    borderColor: PDFLib.rgb(1, 1, 1),
  });
  textField.enableMultiline();
  textField.setAlignment('Left');
  textField.setFontSize(12);
}

function _calculateLineCount(text, lineLength) {
  let lines = 0;
  let paragraphs = text.split("\n");
  paragraphs.forEach(paragraph => {
      if (paragraph.length === 0) {
          lines += 1; // Empty line still takes one line
      } else {
          lines += Math.ceil(paragraph.length / lineLength);
      }
  });
  return lines;
}

function _setValues(form, actor, itemCards) {
  const prime = actor.system.attributes.prime.value || 0;
  const combatMastery = actor.system.details.combatMastery || 0;

  // =================== HEADER ===================
  _text(form.getTextField("Character Name"), actor.name);

  const className = _getItemName(actor.system.details.class.id, actor);
  const subclassName = _getItemName(actor.system.details.subclass.id, actor);
  const classAndSubclass = subclassName !== "" ? `${subclassName} ${className}` : className;
  _text(form.getTextField("Class & Subclass"), classAndSubclass);

  const ancestryName = _getItemName(actor.system.details.ancestry.id, actor);
  const backgroundName = _getItemName(actor.system.details.background.id, actor);
  _text(form.getTextField("Ancestry"), `${ancestryName} (${backgroundName})`);

  _text(form.getTextField("Level"), actor.system.details.level);
  _text(form.getTextField("Combat Mastery"), combatMastery);
  // =================== HEADER ===================


  // ================== COLUMN 1 ==================
  _text(form.getTextField("Prime"), actor.system.attributes.prime.value);
  _text(form.getTextField("Might"), actor.system.attributes.mig.value);
  _text(form.getTextField("Agility"), actor.system.attributes.agi.value);
  _text(form.getTextField("Charisma"), actor.system.attributes.cha.value);
  _text(form.getTextField("Intelligence"), actor.system.attributes.int.value);

  _text(form.getTextField("Might Save"), actor.system.attributes.mig.save);
  _text(form.getTextField("Agility Save"), actor.system.attributes.agi.save);
  _text(form.getTextField("Charisma Save"), actor.system.attributes.cha.save);
  _text(form.getTextField("Intelligence Save"), actor.system.attributes.int.save);

  _setSkills(form, actor);
  _setLangs(form, actor);
  _setResistances(form, actor);
  // ================== COLUMN 1 ==================


  // ================== COLUMN 2 ==================
  _text(form.getTextField("Hit Points Current"), actor.system.resources.health.max);
  _text(form.getTextField("Hit Points Max"), actor.system.resources.health.max);
  _text(form.getTextField("Physical Defense"), actor.system.defences.precision.normal); // TODO FIX
  _text(form.getTextField("Mental Defense"), actor.system.defences.area.normal);
  _text(form.getTextField("PD Heavy Threshold"), actor.system.defences.precision.heavy);
  _text(form.getTextField("PD Brutal Threshold"), actor.system.defences.precision.brutal);
  _text(form.getTextField("MD Heavy Threshold"), actor.system.defences.area.heavy);
  _text(form.getTextField("MD Brutal Threshold"), actor.system.defences.area.brutal);
  _checkbox(form.getCheckBox("Physical-Damage-Reduction"), actor.system.damageReduction.pdr.active, "circle");
  _checkbox(form.getCheckBox("Elemantal-Damage-Reduction"), actor.system.damageReduction.edr.active, "circle");
  _checkbox(form.getCheckBox("Mental-Damage-Reduction"), actor.system.damageReduction.mdr.active, "circle");

  _text(form.getTextField("Attack Check"), prime + combatMastery);
  _text(form.getTextField("Save DC"), 10 + prime + combatMastery);
  _text(form.getTextField("Initiative"), actor.system.special.initiative);
  // ================== COLUMN 2 ==================


  // ================== COLUMN 3 ==================
  _text(form.getTextField("Stamina Points Current"), actor.system.resources.stamina.max);
  _text(form.getTextField("Stamina Points Max"), actor.system.resources.stamina.max);
  _text(form.getTextField("Mana Points Current"), actor.system.resources.mana.max);
  _text(form.getTextField("Mana Points Max"), actor.system.resources.mana.max);
  _text(form.getTextField("Grit Point Current"), actor.system.resources.grit.max);
  _text(form.getTextField("Grit Point Cap"), actor.system.resources.grit.max);
  _text(form.getTextField("Rest Point Current"), actor.system.resources.restPoints.max);
  _text(form.getTextField("Rest Point Cap"), actor.system.resources.restPoints.max);
  _setCustomResources(form, actor);

  const movement = actor.system.movement;
  _text(form.getTextField("Move Speed"), movement.ground.current);
  _checkbox(form.getCheckBox("Fly-Half"), movement.flying.halfSpeed || movement.flying.fullSpeed, "circle");
  _checkbox(form.getCheckBox("Fly-Full"), movement.flying.fullSpeed, "circle");
  _checkbox(form.getCheckBox("Swim-Half"), movement.swimming.halfSpeed || movement.swimming.fullSpeed, "circle");
  _checkbox(form.getCheckBox("Swim-Full"), movement.swimming.fullSpeed, "circle");
  _checkbox(form.getCheckBox("Glide-Half"), movement.glide.halfSpeed || movement.glide.fullSpeed, "circle");
  _checkbox(form.getCheckBox("Glide-Full"), movement.glide.fullSpeed, "circle");
  _checkbox(form.getCheckBox("Climb-Half"), movement.climbing.halfSpeed || movement.climbing.fullSpeed, "circle");
  _checkbox(form.getCheckBox("Climb-Full"), movement.climbing.fullSpeed, "circle");
  _checkbox(form.getCheckBox("Burrow-Half"), movement.burrow.halfSpeed || movement.burrow.fullSpeed, "circle");
  _checkbox(form.getCheckBox("Burrow-Full"), movement.burrow.fullSpeed, "circle");

  _text(form.getTextField("Jump Distance"), actor.system.jump.current);
  _checkbox(form.getCheckBox("Exhaustion -1"), false, "rect");
  _checkbox(form.getCheckBox("Exhaustion -2"), false, "rect");
  _checkbox(form.getCheckBox("Exhaustion -3"), false, "rect");
  _checkbox(form.getCheckBox("Exhaustion -4"), false, "rect");
  _checkbox(form.getCheckBox("Exhaustion -5"), false, "rect");
  _text(form.getTextField("Death Threshold"), actor.system.death.treshold);
  // ================== COLUMN 3 ==================


  // =================== ITEMS ====================
  _prepareFeatures(form, actor, itemCards);
  _prepareSpellsAndTechniques(form, actor, itemCards);
  _prepareInventory(form, actor, itemCards);
  // =================== ITEMS ====================
}

function _checkbox(field, checked, type) {
  const assert = (condition, msg = '') => {
    if (!condition) throw new Error(msg || 'Assertion failed');
  };

  // check/uncheck
  if (checked) field.check();
  else field.uncheck();

  // set style
  if (type === "circle") {
    field.updateAppearances((checkBox, widget) => { 
      assert(checkBox === field);
      assert(widget instanceof PDFLib.PDFWidgetAnnotation);
      return { on: [..._drawCircle(true)], off: [..._drawCircle(false)] };
    });
  } 
  if (type === "rect") {
    field.updateAppearances((checkBox, widget) => { 
      assert(checkBox === field);
      assert(widget instanceof PDFLib.PDFWidgetAnnotation);
      return { on: [..._drawRect(true)], off: [..._drawRect(false)] };
    });
  }
  if (type === "diamond") {
    field.updateAppearances((checkBox, widget) => { 
      assert(checkBox === field);
      assert(widget instanceof PDFLib.PDFWidgetAnnotation);
      return { on: [..._drawDiamond(true)], off: [..._drawDiamond(false)] };
    });
  }
}

function _text(field, value) {
  field.setText(value.toString());
}

function _drawCircle(on) {
  return PDFLib.drawEllipse({ 
    x: 6, 
    y: 6.5, 
    xScale: 3, 
    yScale: 3, 
    borderWidth: 0, 
    color: on ? PDFLib.rgb(0,0,0) : PDFLib.rgb(1, 1, 1, 0), 
    borderColor: undefined, 
    rotate: PDFLib.degrees(0), 
  })
}

function _drawRect(on) {
  return PDFLib.drawRectangle({ 
    x: 1.5, 
    y: 1.5, 
    width: 6, 
    height: 6, 
    borderWidth: 0, 
    color: on ? PDFLib.rgb(0,0,0) : PDFLib.rgb(1, 1, 1, 0), 
    borderColor: undefined, 
    rotate: PDFLib.degrees(0), 
    xSkew: PDFLib.degrees(0),
    ySkew: PDFLib.degrees(0),
  })
}

function _drawDiamond(on) {
  return PDFLib.drawRectangle({ 
    x: 6.20, 
    y: 4, 
    width: 3.5, 
    height: 3.5, 
    borderWidth: 0, 
    color: on ? PDFLib.rgb(0,0,0) : PDFLib.rgb(1, 1, 1, 0), 
    borderColor: undefined, 
    rotate: PDFLib.degrees(45), 
    xSkew: PDFLib.degrees(0),
    ySkew: PDFLib.degrees(0),
  })
}

function _setSkills(form, actor) {
  // First we need to update styles for custom knowledge and trade skills
  for (let i = 2; i <= 10; i+=2) {
    _checkbox(form.getCheckBox(`Mastery-Trade-A-${i}`), false, "circle");
    _checkbox(form.getCheckBox(`Mastery-Trade-B-${i}`), false, "circle");
    _checkbox(form.getCheckBox(`Mastery-Trade-C-${i}`), false, "circle");
    _checkbox(form.getCheckBox(`Mastery-Trade-D-${i}`), false, "circle");
  }

  const expertise = new Set([...actor.system.expertise.automated, ...actor.system.expertise.manual]);
  const skillEntries = Object.entries(actor.system.skills);
  const tradeEntries = Object.entries(actor.system.tradeSkills);

  // Prepare Trades Masteries
  let tradesCounter = 0;
  const dif = ["A", "B", "C", "D"];
  for (const [key, trade] of tradeEntries) {
    // Knowledge trades are on the character sheet
    if (["arc", "nat", "his", "occ", "rel"].includes(key)) {
      skillEntries.push([key, trade]);
      continue;
    }

    if (trade.mastery === 0) continue;
    const tradeKey = dif[tradesCounter];
    if (!tradeKey) continue;

    tradesCounter++;
    _text(form.getTextField(`Custom Trade ${tradeKey}`), trade.label);
    _text(form.getTextField(`Trade ${tradeKey}`), trade.modifier);

    // Mark Expertise
    if (expertise.has(key)) _checkbox(form.getCheckBox(`Trade-${tradeKey}-Proficiency`), true, "diamond");
    else _checkbox(form.getCheckBox(`Trade-${tradeKey}-Proficiency`), false, "diamond");

    for (let i = 2; i <= 10; i+=2) {
      _checkbox(form.getCheckBox(`Mastery-Trade-${tradeKey}-${i}`), (2*trade.mastery) >= i, "circle");
    }
  }


  // Prepere Skill Masteries
  for (const [key, skill] of skillEntries) {
    let label = skill.label;
    if (expertise.has(key)) _checkbox(form.getCheckBox(`${label}-Proficiency`), true, "diamond");
    else _checkbox(form.getCheckBox(`${label}-Proficiency`), false, "diamond");

    // Mark Expertise
    try {_text(form.getTextField(`${label}`), skill.modifier);}
    catch (error) {continue;}

    for (let i = 2; i <= 10; i+=2) {
      _checkbox(form.getCheckBox(`Mastery-${label}-${i}`), (2*skill.mastery) >= i, "circle");
    }
  }
}

function _setLangs(form, actor) {
  // First we need to update styles for languages
  _checkbox(form.getCheckBox("Mastery-Language-A-Limited"), false, "circle");
  _checkbox(form.getCheckBox("Mastery-Language-A-Fluent"), false, "circle");
  _checkbox(form.getCheckBox("Mastery-Language-B-Limited"), false, "circle");
  _checkbox(form.getCheckBox("Mastery-Language-B-Fluent"), false, "circle");
  _checkbox(form.getCheckBox("Mastery-Language-C-Limited"), false, "circle");
  _checkbox(form.getCheckBox("Mastery-Language-C-Fluent"), false, "circle");
  _checkbox(form.getCheckBox("Mastery-Language-D-Limited"), false, "circle");
  _checkbox(form.getCheckBox("Mastery-Language-D-Fluent"), false, "circle");

  const languages = actor.system.languages;
  let langCounter = 0;
  const dif = ["A", "B", "C", "D"];
  for (const lang of Object.values(languages)) {
    if (lang.mastery === 0) continue;
    const langKey = dif[langCounter];
    if (!langKey) continue;

    langCounter++;
    _text(form.getTextField(`Language ${langKey}`), lang.label);
    if (lang.mastery >= 1) _checkbox(form.getCheckBox(`Mastery-Language-${langKey}-Limited`), true, "circle");
    if (lang.mastery >= 2) _checkbox(form.getCheckBox(`Mastery-Language-${langKey}-Fluent`), true, "circle");
  }
}

function _setResistances(form, actor) {
  const dr = actor.system.damageReduction.damageTypes;
  const sr = actor.system.statusResistances;

  let misc = "";

  // Damage Resistance
  for (const reduction of Object.values(dr)) {
    if (reduction.immune)                                       misc += `${reduction.label} Immunity, `;
    else if (reduction.resistance && !reduction.vulnerability)  misc += `${reduction.label} Resistance(Half), `;
    else if (!reduction.resistance && reduction.vulnerability)  misc += `${reduction.label} Vulnerability(Half), `;

    if (reduction.immune) continue;
    const value = reduction.resist - reduction.vulnerable;
    if (value < 0)  misc += `${reduction.label} Vulnerability(${Math.abs(value)}), `;
    if (value > 0)  misc += `${reduction.label} Resistance(${value}), `;
  }

  // Status Resistance
  misc += "\n";
  for (const status of Object.values(sr)) {
    if (status.immunity)            misc += `${status.label} Immunity, `;
    else if (status.resistance > 0)  misc += `${status.label} Resistance(${status.resistance}), `;
    else if (status.vulnerability > 0)  misc += `${status.label} Vulnerability(${status.vulnerability}), `;
  }

  _text(form.getTextField("Misc"), misc);
}

function _setCustomResources(form, actor) {
  const customResources = actor.system.resources.custom;
  if (!customResources) return;

  let counter = 0;
  const dif = ["A", "B", "C", "D", "E", "F", "G", "H", "I"];
  for (const custom of Object.values(customResources)) {
    const key = dif[counter];
    if (!key) continue;

    counter++;
    _text(form.getTextField(`Resource ${key}`), custom.name);
    _text(form.getTextField(`Resource ${key} Current`), custom.max);
    _text(form.getTextField(`Resource ${key} Cap`), custom.max);
  }
}

function _prepareFeatures(form, actor, itemCards) {
  const features = actor.items.filter(item => item.type === "feature");

  let text = "";
  for (const feature of features) {
    const name = feature.name;
    const cost = _itemUseCost(feature, actor);
    const details = _itemDetails(feature);

    text += `== ${name}${cost}: ${details}\n`;
    itemCards.push({
      name: `${name}${cost}`,
      details: details,
      description: _itemDescription(feature)
    });
  }
  _text(form.getTextField("Features"), text);
}

function _prepareSpellsAndTechniques(form, actor, itemCards) {
  const spells = actor.items.filter(item => item.type === "spell");
  const techniques = actor.items.filter(item => item.type === "technique");

  let text = "";
  for (const technique of techniques) {
    const name = technique.name;
    const cost = _itemUseCost(technique, actor);
    const details = _itemDetails(technique);

    text += `== ${name}${cost}: ${details}\n`;
    itemCards.push({
      name: `${name}${cost}`,
      details: details,
      description: _itemDescription(technique)
    });
  }

  for (const spell of spells) {
    const name = spell.name;
    const cost = _itemUseCost(spell, actor);
    const details = _itemDetails(spell);

    text += `== ${name}${cost}: ${details}\n`;
    itemCards.push({
      name: `${name}${cost}`,
      details: details,
      description: _itemDescription(spell)
    });
  }
  _text(form.getTextField("Spells and Techniques"), text);
}

function _prepareInventory(form, actor, itemCards) {
  const inventory = actor.items.filter(item => ["weapon", "equipment", "consumable"].includes(item.type));

  let text = "";
  for (const item of inventory) {
    const name = item.name;
    const cost = _itemUseCost(item, actor);
    const details = _itemDetails(item);

    text += `== ${name}${cost}: ${details}\n`;
    itemCards.push({
      name: `${name}${cost}`,
      details: details,
      description: _itemDescription(item)
    });
  }
  _text(form.getTextField("Carried"), text);
}

function _getItemName(itemId, actor) {
  const item = getItemFromActor(itemId, actor);
  if (item) return item.name;
  return "";
}

function _itemDescription(item) {
  let content = item.system.description;
  content = content.replaceAll("<li><p>", "\n- ");
  content = content.replace(/<li[^>]*>\s*(.*?)\s*<\/li>/gs, '\n\- $1 ');
  content = content.replace(/<p[^>]*>\s*(.*?)\s*<\/p>/gs, '\n$1 ');
  content = content.replaceAll("<br>", "\n");
  content = content.replaceAll("</p>", "");
  content = content.replaceAll("</li>", "");
  let div = document.createElement("div");
  div.innerHTML = content;
  return _removeLinks(div.innerText)
}

function _itemDetails(item) {
  let content = itemDetailsToHtml(item);
  content = content.replace(/<div[^>]*>\s*(.*?)\s*<\/div>/gs, '[$1] ');
  content = content.replaceAll("\n", "");
  if (content.startsWith("[ ")) content = content.replace("[ ", "[");
  return content;
}

function _itemUseCost(item, actor) {
  let text = "";
  const cost = collectExpectedUsageCost(actor, item)[0];

  if (cost.actionPoint > 0) text += `${cost.actionPoint} AP, `;
  if (cost.stamina > 0) text += `${cost.stamina} SP, `;
  if (cost.mana > 0) text += `${cost.mana} MP, `;
  if (cost.health > 0) text += `${cost.health} HP, `;
  if (cost.grit > 0) text += `${cost.grit} GP, `;
  if (cost.restPoints > 0) text += `${cost.restPoints} RP, `;

  // Prepare Custom resource cost
  if (cost.custom) {
    for (const custom of Object.values(cost.custom)) {
      if (custom.value > 0) text += `${custom.value} ${custom.name}, `;
    }
  }

  if (text) {
    text = text.slice(0,-2);
    text = ` (${text})`;
  }
  return text;
}

function _removeLinks(description) {
  const uuidRegex = /@UUID\[[^\]]*]\{[^}]*}/g;
  const itemLinks = [...description.matchAll(uuidRegex)];
  itemLinks.forEach(link => {
    link = link[0];
    let [uuid, name] = link.split("]{");    
    name = name.slice(0, name.length- 1);
    description = description.replace(link, name);
  });
  return description;
}

/**
 * Extend the basic ActorSheet with some very simple modifications
 * @extends {ActorSheet}
 */
class DC20RpgActorSheet extends ActorSheet {

  /** @override */
  static get defaultOptions() {
    return foundry.utils.mergeObject(super.defaultOptions, {
      classes: ["dc20rpg", "sheet", "actor"], //css classes
      width: 790,
      height: 863,
      tabs: [{ navSelector: ".char-sheet-navigation", contentSelector: ".char-sheet-body", initial: "core" }],
      dragDrop: [
        {dragSelector: ".custom-resource[data-key]", dropSelector: null},
        {dragSelector: ".effects-row[data-effect-id]", dropSelector: null},
        {dragSelector: ".item-list .item", dropSelector: null}
      ],
    });
  }

  /** @override */
  get template() {
    return `systems/dc20rpg/templates/actor_v2/${this.actor.type}.hbs`;
  }

  /** @override */
  _getHeaderButtons() {
    const buttons = super._getHeaderButtons();
    if (this.actor.type === "character") {
      buttons.unshift({
        label: "PDF",
        class: "export-to-pdf",
        icon: "fas fa-file-pdf",
        tooltip: "Export to PDF",
        onclick: () => fillPdfFrom(this.actor)
      });
    }
    return buttons;
  }

  /** @override */
  async getData() {
    const context = await super.getData();
    duplicateData(context, this.actor);
    sortMapOfItems(context, this.actor.items);
    prepareCommonData(context);

    const actorType = this.actor.type;
    switch (actorType) {
      case "character": 
        prepareCharacterData(context);
        prepareItemsForCharacter(context, this.actor);
        break;
      case "npc": case "companion": 
        this.options.classes.push(actorType);
        this.position.width = 672;
        this.position.height = 700;
        prepareNpcData(context);
        prepareItemsForNpc(context, this.actor);
        if (actorType === "npc") {
          context.isNPC = true;
        }
        if (actorType === "companion") {
          prepareCompanionData(context);
          prepareCompanionTraits(context, this.actor);
          context.companionOwner = this.actor.companionOwner;
        }
        break;
    } 
    prepareActiveEffectsAndStatuses(this.actor, context);

    // Enrich text editors
    context.enriched = {};
    context.enriched.journal = await TextEditor.enrichHTML(context.system.journal, {secrets:true});
    return context;
  }

  /** @override */
  activateListeners(html) {
    super.activateListeners(html);
    activateCommonLinsters$1(html, this.actor);
    switch (this.actor.type) {
      case "character": 
        activateCharacterLinsters(html, this.actor);
        break;
      case "npc": 
        activateNpcLinsters(html, this.actor);
        break;
      case "companion": 
        activateNpcLinsters(html, this.actor);
        activateCompanionListeners(html, this.actor);
        break;
    }
  }

  /** @override */
  _onSortItem(event, itemData) {
    onSortItem(event, itemData, this.actor);
  }

  _getChildWithClass(parent, clazz) {
    if (!parent) return;
    return Object.values(parent.children).filter(child => child.classList.contains(clazz))[0];
  }

  setPosition(position) {
   super.setPosition(position);

   if (position?.height) {
    const windowContent = this._getChildWithClass(this._element[0], 'window-content');
    const actor_v2 = this._getChildWithClass(windowContent, "actor_v2");
    const charSheetDetails = this._getChildWithClass(actor_v2, 'char-sheet-details');
    const sidetabContent = this._getChildWithClass(charSheetDetails, 'sidetab-content');
    
    if (sidetabContent) {
      const newY = position.height - 30;
      sidetabContent.style = `height: ${newY}px`;
    }
   }
  }

  _onDragStart(event) {
    const dataset = event.currentTarget.dataset;
    if (dataset.type === "resource") {
      const resource = this.actor.system.resources.custom[dataset.key];
      resource.type = "resource";
      resource.key = dataset.key;
      if (!resource) return;
      event.dataTransfer.setData("text/plain", JSON.stringify(resource));
    }
    if (dataset.effectId) {
      const effect = getEffectFrom(dataset.effectId, this.actor);
      if (effect) {
        event.dataTransfer.setData("text/plain", JSON.stringify(effect.toDragData()));
      }
      return;
    }
    super._onDragStart(event);
  }

  /** @override */
  async _onDropItem(event, data) {
    if (this.actor.type === "companion") {
      const selected = await getSimplePopup("confirm", {header: "Add as Companion Trait?", information: ["Do you want to add this item as Companion Trait?", "Yes: Add as Companion Trait", "No: Add as Standard Item"]});
      if (selected) {
        const item = await Item.implementation.fromDropData(data);
        const itemData = item.toObject();
        createTrait(itemData, this.actor);
      }
      else return await super._onDropItem(event, data);
    }
    else return await super._onDropItem(event, data);
  }

  /** @override */
  async _onDropActor(event, data) {
    if (this.actor.type === "companion")this._onDropCompanionOwner(data);
    else return await super._onDropActor(event, data);
  }

  async _onDropCompanionOwner(data) {
    if (this.actor.system.companionOwnerId) {
      ui.notifications.warn("Owner of this companion already exist");
      return;
    }
    else {
      if (!data.uuid.startsWith("Actor")) {
        ui.notifications.warn("Owning actor must be stored insde of 'Actors' directory");
        return;
      }
      const companionOwner = await fromUuid(data.uuid);
      if (companionOwner?.type !== "character") {
        ui.notifications.warn("Only Player Character can be selected as an owner");
        return;
      }
      this.actor.update({["system.companionOwnerId"]: companionOwner.id});
    }
  }
}

/**
 * Dialog window for creating weapons.
 */
class WeaponCreatorDialog extends Dialog {

  constructor(weapon , dialogData = {}, options = {}) {
    super(dialogData, options);
    this.weapon = weapon;
    this._prepareWeaponBlueprint();
  }

  static get defaultOptions() {
    return foundry.utils.mergeObject(super.defaultOptions, {
      template: "systems/dc20rpg/templates/dialogs/weapon-creator-dialog.hbs",
      classes: ["dc20rpg", "dialog", "flex-dialog"]
    });
  }

  _prepareWeaponBlueprint() {
    const blueprint = {
      weaponType: "melee",
      weaponStyle: "axe",
      secondWeaponStyle: "axe",
      stats: {
        damage: 1,
        dmgType: "slashing",
        secondDmgType: "slashing",
        range: {
          normal: 0,
          max: 0,
          unit: ""
        },
        pointsCost: 0
      },
      properties: {
        melee: {
          concealable: {active: false},
          guard: {active: false},
          heavy: {active: false},
          impact: {active: false},
          multiFaceted: {active: false},
          reach: {active: false},
          silent: {active: false},
          toss: {active: false},
          thrown: {active: false},
          twoHanded: {active: false},
          unwieldy: {active: false},
          versatile: {active: false},
        },
        ranged: {
          ammo: {active: true},
          concealable: {active: false},
          heavy: {active: false},
          impact: {active: false},
          longRanged: {active: false},
          reload: {active: false},
          silent: {active: false},
          twoHanded: {active: true},
          unwieldy: {active: true},
        },
        special: {
          capture: {active: false},
          returning: {active: false},
        }
      }
    };
    this.blueprint = blueprint;
  }

  getData() {
    return {
      config: {
        weaponTypes: CONFIG.DC20RPG.DROPDOWN_DATA.weaponTypes,
        propertiesJournal: CONFIG.DC20RPG.SYSTEM_CONSTANTS.JOURNAL_UUID.propertiesJournal,
        meleeWeaponStyles: CONFIG.DC20RPG.DROPDOWN_DATA.meleeWeaponStyles,
        rangedWeaponStyles: CONFIG.DC20RPG.DROPDOWN_DATA.rangedWeaponStyles,
        dmgTypes: CONFIG.DC20RPG.DROPDOWN_DATA.physicalDamageTypes,
        properties: CONFIG.DC20RPG.DROPDOWN_DATA.properties,
      },
      blueprint: this.blueprint
    };
  }

   /** @override */
  activateListeners(html) {
    super.activateListeners(html);
    html.find('.activable').click(ev => this._onActivable(datasetOf(ev).path));
    html.find(".input").change(ev => this._onValueChange(datasetOf(ev).path, valueOf(ev)));
    html.find(".selectable").change(ev => this._onValueChange(datasetOf(ev).path, valueOf(ev)));
    html.find(".create-weapon").click(ev => this._onWeaponCreate(ev.preventDefault()));
    html.find('.journal-tooltip').hover(ev => journalTooltip(datasetOf(ev).uuid, datasetOf(ev).header, datasetOf(ev).img, ev, html), ev => hideTooltip(ev, html));
  }

  _onValueChange(pathToValue, value) {
    setValueForPath(this, pathToValue, value);
    this._runWeaponStatsCheck();
    this.render(true);
  }

  _onActivable(pathToValue) {
    const value = getValueFromPath(this, pathToValue);
    setValueForPath(this, pathToValue, !value);
    this._runWeaponStatsCheck();
    this.render(true);
  }

  _onWeaponCreate() {
    this._runWeaponStatsCheck();
    const blueprint = this.blueprint;
    const stats = blueprint.stats;
    const rangeType = blueprint.weaponType === "ranged" ? "ranged" : "melee";
    const updateData = {
      system: {
        actionType: "attack",
        weaponType: blueprint.weaponType,
        weaponStyle: blueprint.weaponStyle,
        secondWeaponStyle: blueprint.secondWeaponStyle,
        ["costs.resources.actionPoint"]: 1,
        ["attackFormula.rangeType"]: rangeType,
        properties: this._getPropertiesToUpdate(),
        range: blueprint.stats.range,
        formulas: {
          weaponDamage: {
            formula: stats.damage.toString(),
            type: stats.dmgType,
            category: "damage",
            fail: false,
            failFormula: "",
            each5: false,
            each5Formula: "",
          }
        }
      }
    };
    this.close();
    this.weapon.update(updateData);
  }

  _runWeaponStatsCheck() {
    const weaponType = this.blueprint.weaponType;
    const range = weaponType === "ranged" ? {normal: 15, max: 45} : {normal: 0, max: 0};
    const pointsCost = weaponType === "ranged" ? 2 : 0;
    let stats = {
      range: range,
      damage: 1,
      dmgType: this.blueprint.stats.dmgType,
      secondDmgType: this.blueprint.stats.secondDmgType,
      bonusPD: false,
      requiresMastery: false,
      pointsCost: pointsCost
    };

    let properties = this.blueprint.properties[weaponType];
    if (weaponType === "special") properties = {...properties, ...this.blueprint.properties["melee"]};
    this.blueprint.stats = this._runPropertiesCheck(stats, properties);
  }

  _runPropertiesCheck(stats, properties) {
    if (properties.heavy?.active) stats.damage += 1;
    if (properties.reload?.active) stats.damage += 1;
    if (properties.toss?.active) stats.range = {normal: 5, max: 10};
    if (properties.thrown?.active) stats.range = {normal: 10, max: 20};
    if (properties.longRanged?.active) stats.range = {normal: 30, max: 90};

    const propCost = CONFIG.DC20RPG.SYSTEM_CONSTANTS.propertiesCost;
    Object.entries(properties).forEach(([key, prop]) => {
      if (prop.active) stats.pointsCost += propCost[key];
    });
    return stats;
  }

  _getPropertiesToUpdate() {
    let propsToUpdate = this.weapon.system.properties;
    Object.entries(propsToUpdate).forEach(([key, prop]) => prop.active = false);
    const properties = this.blueprint.properties;

    const weaponType = this.blueprint.weaponType;
    if (weaponType === "melee")   propsToUpdate = {...propsToUpdate, ...properties["melee"]};
    if (weaponType === "ranged")  propsToUpdate = {...propsToUpdate, ...properties["ranged"]};
    if (weaponType === "special") propsToUpdate = {...propsToUpdate, ...properties["melee"], ...properties["special"]};

    // Prepare Multi-Faceted property
    if (propsToUpdate.multiFaceted.active === true) {
      const blueprint = this.blueprint;
      propsToUpdate.multiFaceted = {
        active: true,
        selected: "first",
        labelKey: blueprint.weaponStyle,
        weaponStyle: {
          first: blueprint.weaponStyle,
          second: blueprint.secondWeaponStyle
        },
        damageType: {
          first: blueprint.stats.dmgType,
          second: blueprint.stats.secondDmgType
        }
      };
    }
    return propsToUpdate;
  }
}

function createWeaponCreator(weapon) {
  new WeaponCreatorDialog(weapon, {title: "Create Your Weapon"}).render(true);
}

/**
 * Editor dialog
 */
class EditorDialog extends FormApplication {

  constructor(owner, updatePath, dialogData = {}, options = {}) {
    super(dialogData, options);
    this.owner = owner;
    this.updatePath = updatePath;

    // We need to prepare that for editor to set its inital value
    this.object.text = getValueFromPath(this.owner, this.updatePath); 
  }

  static get defaultOptions() {
    return foundry.utils.mergeObject(super.defaultOptions, {
      template: "systems/dc20rpg/templates/dialogs/editor-dialog.hbs",
      classes: ["dc20rpg", "dialog"],
    });
  }
  
  async getData() {
    return {
      description: this.object.text,
      path: this.updatePath
    };
  }

  _updateObject(event, formData) {
    this.owner.update({[this.updatePath]: formData.text});
  }
}

/**
 * Creates VariableAttributePickerDialog for given actor and with dataset extracted from event. 
 */
function createEditorDialog(owner, updatePath) {
  let dialog = new EditorDialog(owner, updatePath, {title: "Editor"});
  dialog.render(true);
}

/**
 * Configuration of advancements on item
 */
class AdvancementConfiguration extends Dialog {

  constructor(item, key, newAdv, dialogData = {}, options = {}) {
    super(dialogData, options);
    this.item = item;
    this.key = key;

    if(newAdv) this.advancement = createNewAdvancement();
    else this.advancement = foundry.utils.deepClone(item.system.advancements[this.key]);
  }

  static get defaultOptions() {
    return foundry.utils.mergeObject(super.defaultOptions, {
      template: "systems/dc20rpg/templates/dialogs/character-progress/advancement-config-dialog.hbs",
      classes: ["dc20rpg", "dialog"],
      width: 750,
      height: 550,
      resizable: true,
      draggable: true,
    });
  }

  async getData() {
    const advancement = this.advancement;

    // Collect items that are part of advancement
    Object.values(advancement.items).forEach(async record => {
      const item = await fromUuid(record.uuid);
      record.img = item.img;
      record.name = item.name;

      if (record.mandatory) record.selected = true;
    });

    return {
      ...advancement,
      source: this.item.type,
      compendiumTypes: CONFIG.DC20RPG.DROPDOWN_DATA.advancementItemTypes
    };
  }

   /** @override */
  activateListeners(html) {
    super.activateListeners(html);
    html.find(".save").click((ev) => this._onSave(ev));
    html.find('.activable').click(ev => this._onActivable(datasetOf(ev).path));
    html.find(".selectable").change(ev => this._onValueChange(datasetOf(ev).path, valueOf(ev)));
    html.find(".input").change(ev => this._onValueChange(datasetOf(ev).path, valueOf(ev)));
    html.find(".numeric-input").change(ev => this._onNumericValueChange(datasetOf(ev).path, valueOf(ev)));
    html.find(".numeric-input-nullable").change(ev => this._onNumericValueChange(datasetOf(ev).path, valueOf(ev), true));
    html.find(".repeat-at").change(ev => this._onRepeatedAt(datasetOf(ev).index, valueOf(ev)));

    // Item manipulation
    html.find('.item-delete').click(ev => this._onItemDelete(datasetOf(ev).key)); 

    // Drag and drop events
    html[0].addEventListener('dragover', ev => ev.preventDefault());
    html[0].addEventListener('drop', async ev => await this._onDrop(ev));
  }

  _onSave(event) {
    event.preventDefault();
    this.item.update({[`system.advancements.${this.key}`] : this.advancement});
    this.close();
  }
  _onActivable(pathToValue) {
    let value = getValueFromPath(this.advancement, pathToValue);
    setValueForPath(this.advancement, pathToValue, !value);
    this.render(true);
  }
  _onValueChange(path, value) {
    setValueForPath(this.advancement, path, value);
    this.render(true);
  }
  _onNumericValueChange(path, value, nullable) {
    let numericValue = parseInt(value);
    if (nullable && isNaN(numericValue)) numericValue = null;
    setValueForPath(this.advancement, path, numericValue);
    this.render(true);
  }

  _onRepeatedAt(index, value) {
    if (value === "") value = 0;
    const numericValue = parseInt(value);
    this.advancement.repeatAt[index] = numericValue;
    this.render(true);
  }

  async _onDrop(event) {
    event.preventDefault();
    const droppedData  = event.dataTransfer.getData('text/plain');
    if (!droppedData) return;
    
    const droppedObject = JSON.parse(droppedData);
    if (droppedObject.type !== "Item") return;

    const item = await Item.fromDropData(droppedObject);
    if (!["feature", "technique", "spell", "weapon", "equipment"].includes(item.type)) return;

    // Can be counted towards known spell/techniques
    const canBeCounted = ["technique", "spell"].includes(item.type);

    // Get item
    this.advancement.items[item.id] = {
      uuid: droppedObject.uuid,
      createdItemId: "",
      selected: false,
      pointValue: item.system.choicePointCost !== undefined ? item.system.choicePointCost : 1,
      mandatory: false,
      canBeCounted: canBeCounted,
      ignoreKnown: false,
    };
    this.render(true);
  }

  _onItemDelete(itemKey) {
    delete this.advancement.items[itemKey];
    this.item.update({[`system.advancements.${this.key}.items.-=${itemKey}`] : null});
    this.render(true);
  }

  setPosition(position) {
    super.setPosition(position);

    this.element.css({
      "min-height": "200px",
      "min-width": "450px",
    });
  }
}

/**
 * Creates AdvancementConfiguration dialog for given item. 
 */
function configureAdvancementDialog(item, advancementKey) {
  let newAdv = false;
  if (!advancementKey) {
    newAdv = true;
    const advancements = item.system.advancements;
    do {
      advancementKey = generateKey();
    } while (advancements[advancementKey]);
  }
  const dialog = new AdvancementConfiguration(item, advancementKey, newAdv, {title: `Configure Advancement`});
  dialog.render(true);
}

function activateCommonLinsters(html, item) {
  html.find('.activable').click(ev => changeActivableProperty(datasetOf(ev).path, item));
  html.find('.numeric-input').change(ev => changeNumericValue(valueOf(ev), datasetOf(ev).path, item));
  html.find('.input').change(ev => changeValue(valueOf(ev), datasetOf(ev).path, item));
  html.find(".selectable").change(ev => changeValue(valueOf(ev), datasetOf(ev).path, item));

  // Weapon Creator
  html.find('.weapon-creator').click(() => createWeaponCreator(item));
  html.find('.scroll-creator').click(() => createScrollFromSpell(item));

  // Roll Templates
  html.find('.roll-template').click(ev => _onRollTemplateSelect(valueOf(ev), item));

  // Tooltip
  html.find('.journal-tooltip').hover(ev => journalTooltip(datasetOf(ev).uuid, datasetOf(ev).header, datasetOf(ev).img, ev, html, {inside: datasetOf(ev).inside === "true"}), ev => hideTooltip(ev, html));
  html.find('.content-link').hover(async ev => _onHover(ev, html), ev => hideTooltip(ev, html));

  // Formulas
  html.find('.add-formula').click(ev => item.createFormula({category: datasetOf(ev).category}));
  html.find('.remove-formula').click(ev => item.removeFormula(datasetOf(ev).key));

  // Roll Requests
  html.find('.add-roll-request').click(() => item.createRollRequest());
  html.find('.remove-roll-request').click(ev => item.removeRollRequest(datasetOf(ev).key));

  // Against Status
  html.find('.add-against-status').click(() => item.createAgainstStatus());
  html.find('.remove-against-status').click(ev => item.removeAgainstStatus(datasetOf(ev).key));

  // Advancements
  html.find('.create-advancement').click(() => configureAdvancementDialog(item));
  html.find('.advancement-edit').click(ev => configureAdvancementDialog(item, datasetOf(ev).key));
  html.find('.editable-advancement').mousedown(ev => ev.which === 2 ? configureAdvancementDialog(item, datasetOf(ev).key) : ()=>{});
  html.find('.advancement-delete').click(ev => deleteAdvancement(item, datasetOf(ev).key));

  // Item Macros
  html.find('.add-macro').click(() => item.createNewItemMacro());
  html.find('.remove-macro').click(ev => item.removeItemMacro(datasetOf(ev).key));
  html.find('.macro-edit').click(ev => item.editItemMacro(datasetOf(ev).key));

  // Conditional
  html.find('.add-conditional').click(() => item.createNewConditional());
  html.find('.remove-conditional').click(ev => item.removeConditional(datasetOf(ev).key));

  // Resources Managment
  html.find('.update-scaling').change(ev => updateScalingValues(item, datasetOf(ev), valueOf(ev)));
  html.find('.update-item-resource').change(ev => updateResourceValues(item, datasetOf(ev).index, valueOf(ev)));

  html.find('.select-other-item').change(ev => _onSelection(datasetOf(ev).path, datasetOf(ev).selector, item, html));
  html.find('.multi-select').change(ev => addToMultiSelect(item, datasetOf(ev).path, valueOf(ev), getLabelFromKey(valueOf(ev), CONFIG.DC20RPG.ROLL_KEYS.allChecks)));
  html.find('.multi-select-remove').click(ev => removeMultiSelect(item, datasetOf(ev).path, datasetOf(ev).key));

  // Enhancement
  html.find('.add-enhancement').click(() => item.createNewEnhancement({name: html.find('.new-enhancement-name')?.val()}));
  html.find('.edit-description').click(ev => createEditorDialog(item, datasetOf(ev).path));
  html.find('.remove-enhancement').click(ev => item.removeEnhancement(datasetOf(ev).key));
  html.find('.enh-macro-edit').click(ev => _onEnhancementMacroEdit(datasetOf(ev).key, datasetOf(ev).macroKey, item));

  // Macros and effects
  html.find('.add-effect-to').click(ev => _onCreateEffectOn(datasetOf(ev).type, item, datasetOf(ev).key));
  html.find('.remove-effect-from').click(ev => _onDeleteEffectOn(datasetOf(ev).type, item, datasetOf(ev).key));
  html.find('.edit-effect-on').click(ev => _onEditEffectOn(datasetOf(ev).type, item, datasetOf(ev).key));

  // Active Effect Managment
  html.find(".effect-create").click(ev => createNewEffectOn(datasetOf(ev).type, item));
  html.find(".effect-edit").click(ev => editEffectOn(datasetOf(ev).effectId, item));
  html.find('.editable-effect').mousedown(ev => ev.which === 2 ? editEffectOn(datasetOf(ev).effectId, item) : ()=>{});
  html.find(".effect-delete").click(ev => deleteEffectFrom(datasetOf(ev).effectId, item));
  html.find('.effect-tooltip').hover(ev => effectTooltip(getEffectFrom(datasetOf(ev).effectId, item), ev, html), ev => hideTooltip(ev, html));

  // Target Management
  html.find('.remove-area').click(ev => removeAreaFromItem(item, datasetOf(ev).key));
  html.find('.create-new-area').click(() => addNewAreaToItem(item));

  // Special Class Id selection
  html.find('.select-class-id').change(ev => _onClassIdSelection(ev, item));
  html.find('.input-class-id').change(ev => _onClassIdSelection(ev, item));

  // Drag and drop events
  html[0].addEventListener('dragover', ev => ev.preventDefault());
  html[0].addEventListener('drop', async ev => await _onDrop(ev, item));
  html.find('.remove-resource').click(ev => _removeResourceFromItem(item, datasetOf(ev).key));
}

async function _onDrop(event, parentItem) {
  event.preventDefault();
  const droppedData  = event.dataTransfer.getData('text/plain');
  if (!droppedData) return;
  const droppedObject = JSON.parse(droppedData);

  if (droppedObject.type === "Item") {
    const item = await Item.fromDropData(droppedObject);

    // Core Usage
    const itemResource = item.system.resource;
    if (!itemResource) return;
    
    const key = itemResource.resourceKey;
    const customResource = {
      name: itemResource.name,
      img: item.img,
    };
    if (item.system.isResource) _addCustomResource(customResource, key, parentItem);
  }

  if (droppedObject.type === "resource") {
    _addCustomResource(droppedObject, droppedObject.key, parentItem);
  }

  if (droppedObject.type === "ActiveEffect") {
    const effect = await ActiveEffect.fromDropData(droppedObject);
    createEffectOn(effect, parentItem);
  }
}

function _onSelection(path, selector, item, html) {
  const itemId = html.find(`.${selector} option:selected`).val();
  item.update({[path]: itemId});
}

function _addCustomResource(customResource, key, item) {
  if (!item.system.costs.resources.custom) return;
  customResource.value = null;

  // Enhancements 
  const enhancements = item.system.enhancements;
  if (enhancements) {
    Object.keys(enhancements)
            .forEach(enhKey=> enhancements[enhKey].resources.custom[key] = customResource); 
  }

  const updateData = {
    system: {
      [`costs.resources.custom.${key}`]: customResource,
      enhancements: enhancements
    }
  };
  item.update(updateData);
}

function _removeResourceFromItem(item, key) {
  const enhUpdateData = {};
  if (item.system.enhancements) {
    Object.keys(item.system.enhancements)
            .forEach(enhKey=> enhUpdateData[`enhancements.${enhKey}.resources.custom.-=${key}`] = null); 
  }

  const updateData = {
    system: {
      [`costs.resources.custom.-=${key}`]: null,
      ...enhUpdateData
    }
  };
  item.update(updateData);
}

async function _onClassIdSelection(event, item) {
  event.preventDefault();
  const classSpecialId = valueOf(event);
  const className = CONFIG.DC20RPG.UNIQUE_ITEM_IDS.class[classSpecialId];

  item.update({
    ["system.forClass"]: {
      classSpecialId: classSpecialId,
      name: className
    }
  });
}

function _onRollTemplateSelect(selected, item) {
  const system = {};
  const saveRequest = {
    category: "save",
    saveKey: "phy",
    contestedKey: "",
    dcCalculation: "spell",
    dc: 0,
    addMasteryToDC: true,
    respectSizeRules: false,
  };
  const contestRequest = {
    category: "contest",
    saveKey: "phy",
    contestedKey: "",
    dcCalculation: "spell",
    dc: 0,
    addMasteryToDC: true,
    respectSizeRules: false,
  };

  // Set action type
  if (["dynamic", "attack"].includes(selected)) system.actionType = "attack";
  if (["check", "contest"].includes(selected)) system.actionType = "check";
  if (["save"].includes(selected)) system.actionType = "other";
  
  // Set save request
  if (["dynamic", "save"].includes(selected)) system.rollRequests = {rollRequestFromTemplate: saveRequest};
  if (["contest"].includes(selected)) system.rollRequests = {rollRequestFromTemplate: contestRequest};
  if (["check", "attack"].includes(selected)) system.rollRequests = {['-=rollRequestFromTemplate']: null};

  // Set check against DC or not
  if (selected === "contest") system.check = {againstDC: false};
  if (selected === "check") system.check = {againstDC: true};
  
  item.update({system: system});
}

async function _onCreateEffectOn(type, item, key) {
  if (type === "enhancement") {
    const enhancements = item.system.enhancements;
    const enh = enhancements[key];
    if (!enh) return;

    const created = await createNewEffectOn("temporary", item, {itemUuid: item.uuid, enhKey: key});
    created.sheet.render(true);
  }
  if (type === "conditional") {
    const conditionals = item.system.conditionals;
    const cond = conditionals[key];
    if (!cond) return;

    const created = await createNewEffectOn("temporary", item, {itemUuid: item.uuid, condKey: key});
    created.sheet.render(true);
  }
}

async function _onEditEffectOn(type, item, key) {
  if (type === "enhancement") {
    const enhancements = item.system.enhancements;
    const enh = enhancements[key];
    if (!enh) return;

    const effectData = enh.modifications.addsEffect;
    if (!effectData) return;
    const created = await createEffectOn(effectData, item);
    created.sheet.render(true);
  }
  if (type === "conditional") {
    const conditionals = item.system.conditionals;
    const cond = conditionals[key];
    if (!cond) return;

    const effectData = cond.effect;
    if (!effectData) return;
    const created = await createEffectOn(effectData, item);
    created.sheet.render(true);
  }
}

function _onDeleteEffectOn(type, item, key) {
  if (type === "enhancement") item.update({[`system.enhancements.${key}.modifications.addsEffect`]: null});
  if (type === "conditional") item.update({[`system.conditionals.${key}.effect`]: null});
}

async function _onEnhancementMacroEdit(enhKey, macroKey, item) {
  const enhancements = item.system.enhancements;
  const enh = enhancements[enhKey];
  if (!enh) return;

  const macros = enh.modifications.macros;
  if (!macros) return;

  const command = macros[macroKey] || "";
  const macro = await createTemporaryMacro(command, item, {item: item, enhKey: enhKey, macroKey: macroKey});
  macro.canUserExecute = (user) => {
    ui.notifications.warn("This is an Enhancement Macro and it cannot be executed here.");
    return false;
  };
  macro.sheet.render(true);
}

async function _onHover(ev, html) {
  const document = await fromUuid(datasetOf(ev).uuid);
  const type = datasetOf(ev).type;
  if (document) {
    if (type === "Item") itemTooltip(document, ev, html);
    if (type === "JournalEntryPage") journalTooltip(datasetOf(ev).uuid, document.name, "icons/svg/item-bag.svg", ev, html);
  }
}

function duplicateItemData(context, item) {
  context.userIsGM = game.user.isGM;
  context.config = CONFIG.DC20RPG;
  context.system = item.system;
  context.flags = item.flags;

  context.itemsWithChargesIds = {};
  context.consumableItemsIds = {};
  context.weaponsOnActor = {};
  context.hasOwner = false;
  let actor = item.actor ?? null;
  if (actor) {
    context.hasOwner = true;
    const itemIds = actor.getOwnedItemsIds(item.id);
    context.itemsWithChargesIds = itemIds.withCharges;
    context.consumableItemsIds = itemIds.consumable;
    context.weaponsOnActor = itemIds.weapons;
  }
}

function prepareItemData(context, item) {
  _prepareEnhancements(context);
  _prepareAdvancements(context);
  _prepareItemUsageCosts(context, item);
}

function preprareSheetData(context, item) {
  context.sheetData = {};
  _prepareTypesAndSubtypes(context, item);
  _prepareDetailsBoxes(context);
  if (["weapon", "equipment", "consumable", "feature", "technique", "spell", "basicAction"].includes(item.type)) {
    _prepareActionInfo(context, item);
    _prepareFormulas(context);
  }
  if (item.type === "weapon") {
    const propCosts = CONFIG.DC20RPG.SYSTEM_CONSTANTS.propertiesCost;
    let propertyCost = item.system.weaponType === "ranged" ? 2 : 0;
    Object.entries(item.system.properties).forEach(([key, prop]) => {
      if (prop.active) propertyCost += propCosts[key];
    });
    context.propertyCost = propertyCost;
  }
  if (item.type === "equipment") {
    const propCosts = CONFIG.DC20RPG.SYSTEM_CONSTANTS.propertiesCost;
    let propertyCost = 0;
    Object.entries(item.system.properties).forEach(([key, prop]) => {
      const propValue = prop.value || 1;
      if (prop.active) propertyCost += (propCosts[key] * propValue);
    });
    context.propertyCost = propertyCost;
  }
  if (item.type === "feature") {
    const options = CONFIG.DC20RPG.UNIQUE_ITEM_IDS[item.system.featureType];
    context.featureSourceItems = options || null;
  }
}

function _prepareDetailsBoxes(context, item) {
  const infoBoxes = {};
  infoBoxes.rollDetails = _prepareRollDetailsBoxes(context);
  infoBoxes.properties = _preparePropertiesBoxes(context);
  infoBoxes.spellLists = _prepareSpellLists(context);
  infoBoxes.spellProperties = _prepareSpellPropertiesBoxes(context);

  context.sheetData.infoBoxes = infoBoxes;
}

function _prepareRollDetailsBoxes(context) {
  const rollDetails = {};

  // Range
  const range = context.system.range;
  if (range && range.normal) {
    const unit = range.unit ? range.unit : "Spaces";
    const max = range.max ? `/${range.max}` : "";
    rollDetails.range = `${range.normal}${max} ${unit} Range`;
  }

  if (range && range.melee && range.melee > 1) {
    const unit = range.unit ? range.unit : "Spaces";
    rollDetails.meleeRange = `${range.melee} ${unit} Melee Range`;
  }
  
  // Duration
  const duration = context.system.duration;
  if (duration && duration.type) {
    const value = duration.value ? duration.value : "";
    const type = getLabelFromKey(duration.type, CONFIG.DC20RPG.DROPDOWN_DATA.durations);
    const timeUnit = getLabelFromKey(duration.timeUnit, CONFIG.DC20RPG.DROPDOWN_DATA.timeUnits);

    if (duration.timeUnit) rollDetails.duration = `${type}<br> (${value} ${timeUnit})`;
    else rollDetails.duration = type;
  }

  // Target
  const target = context.system.target;
  if (target) {
    if (target.type) {
      const targetType = getLabelFromKey(target.type, CONFIG.DC20RPG.DROPDOWN_DATA.invidualTargets);
      rollDetails.target = `${target.count} ${targetType}`;
    }
    if (target.area) {
      const distance = target.area === "line" ? `${target.distance}/${target.width}` : target.distance;
      const arenaType = getLabelFromKey(target.area, CONFIG.DC20RPG.DROPDOWN_DATA.areaTypes);
      const unit = range.unit ? range.unit : "Spaces";
      rollDetails.area = `${distance} ${unit} ${arenaType}`;
    }
  }

  return rollDetails;
}

function _preparePropertiesBoxes(context) {
  const properties = {};
  const systemProperties = context.system.properties;
  
  if (!systemProperties) return properties;

  for (const [key, prop] of Object.entries(systemProperties)) {
    if (prop.active) {
      let label = getLabelFromKey(key, CONFIG.DC20RPG.DROPDOWN_DATA.properties);

      if (prop.value) {
        const value = prop.value !== null ? ` (${prop.value})` : "";
        label += value;
      }

      properties[key] = label;
    }
  }

  return properties;
}

function _prepareSpellLists(context) {
  const properties = {};
  const spellLists = context.system.spellLists;

  if (!spellLists) return properties;

  for (const [key, prop] of Object.entries(spellLists)) {
    if (prop.active) {
      properties[key] = getLabelFromKey(key, CONFIG.DC20RPG.DROPDOWN_DATA.spellLists);
    }
  }

  return properties;
}

function _prepareSpellPropertiesBoxes(context) {
  const properties = {};
  const spellComponents = context.system.components;

  if (!spellComponents) return properties;

  for (const [key, prop] of Object.entries(spellComponents)) {
    if (prop.active) {
      let label = getLabelFromKey(key, CONFIG.DC20RPG.DROPDOWN_DATA.components);

      if (key === "material") {
        const description = prop.description ? prop.description : "";
        const cost = prop.cost ? ` (${prop.cost} GP)` : "";
        const consumed = prop.consumed ? " [Consumed]" : "";
        label += `: ${description}${cost}${consumed}`;
      }

      properties[key] = label;
    }
  }

  return properties;
}

function _prepareEnhancements(context) {
  const enhancements = context.system.enhancements;
  if (!enhancements) return;

  // We want to work on copy of enhancements because we will wrap its 
  // value and we dont want it to break other aspects
  const enhancementsCopy = foundry.utils.deepClone(enhancements); 
  context.enhancements = enhancementsCopy;
}

function _prepareAdvancements(context) {
  const advancements = context.system.advancements;
  if (!advancements) return;
  
  // Split advancements depending on levels
  const advByLevel = {};

  Object.entries(advancements).forEach(([key, adv]) => {
    if (!advByLevel[adv.level]) advByLevel[adv.level] = {};
    advByLevel[adv.level][key] = adv;
  });

  context.advByLevel = advByLevel;
}

function _prepareItemUsageCosts(context, item) {
  context.usageCosts = getItemUsageCosts(item);
} 

function _prepareTypesAndSubtypes(context, item) {
  const itemType = item.type;
  context.sheetData.fallbackType = getLabelFromKey(itemType, CONFIG.DC20RPG.DROPDOWN_DATA.allItemTypes);

  switch (itemType) {
    case "weapon": {
      context.sheetData.type = getLabelFromKey(item.system.weaponStyle, CONFIG.DC20RPG.DROPDOWN_DATA.weaponStyles);
      context.sheetData.subtype = getLabelFromKey(item.system.weaponType, CONFIG.DC20RPG.DROPDOWN_DATA.weaponTypes);
      break;
    }
    case "equipment": {
      context.sheetData.type = getLabelFromKey(item.system.equipmentType, CONFIG.DC20RPG.DROPDOWN_DATA.equipmentTypes);
      context.sheetData.subtype = getLabelFromKey(item.type, CONFIG.DC20RPG.DROPDOWN_DATA.inventoryTypes);
      break;
    }
    case "consumable": {
      context.sheetData.type = getLabelFromKey(item.system.consumableType, CONFIG.DC20RPG.DROPDOWN_DATA.consumableTypes);
      context.sheetData.subtype = getLabelFromKey(item.type, CONFIG.DC20RPG.DROPDOWN_DATA.inventoryTypes);
      break;
    }
    case "feature": {
      context.sheetData.type = getLabelFromKey(item.system.featureType, CONFIG.DC20RPG.DROPDOWN_DATA.featureSourceTypes);
      context.sheetData.subtype = item.system.featureOrigin;
      break;
    }
    case "technique": {
      context.sheetData.type = getLabelFromKey(item.system.techniqueType, CONFIG.DC20RPG.DROPDOWN_DATA.techniqueTypes);
      context.sheetData.subtype = item.system.techniqueOrigin;
      break;
    }
    case "spell": {
      context.sheetData.type = getLabelFromKey(item.system.spellType, CONFIG.DC20RPG.DROPDOWN_DATA.spellTypes);
      context.sheetData.subtype = getLabelFromKey(item.system.magicSchool, CONFIG.DC20RPG.DROPDOWN_DATA.magicSchools);
      break;
    }
    case "basicAction": {
      context.sheetData.type = game.i18n.localize("dc20rpg.item.sheet.header.action");
      context.sheetData.subtype = getLabelFromKey(item.system.category, CONFIG.DC20RPG.DROPDOWN_DATA.basicActionsCategories);
      break;
    }
    case "subclass": {
      context.sheetData.type = game.i18n.localize("TYPES.Item.subclass");
      context.sheetData.subtype = item.system.forClass.name;
      break;
    }
    case "class": {
      const isMartial = item.system.martial;  
      const isSpellcaster = item.system.spellcaster;
      let classType = '';
      if (isMartial && isSpellcaster) classType = getLabelFromKey("hybrid", CONFIG.DC20RPG.TRANSLATION_LABELS.classTypes);
      else if (isMartial) classType = getLabelFromKey("martial", CONFIG.DC20RPG.TRANSLATION_LABELS.classTypes);
      else if (isSpellcaster) classType = getLabelFromKey("spellcaster", CONFIG.DC20RPG.TRANSLATION_LABELS.classTypes);
      
      context.sheetData.subtype = getLabelFromKey(item.type, CONFIG.DC20RPG.DROPDOWN_DATA.allItemTypes);
      context.sheetData.type = classType;
      break;
    }
  }
}

function _prepareActionInfo(context, item) {
  context.sheetData.damageFormula = getFormulaHtmlForCategory("damage", item);
  context.sheetData.healingFormula = getFormulaHtmlForCategory("healing", item);
  context.sheetData.otherFormula = getFormulaHtmlForCategory("other", item);
  context.sheetData.saves = getRollRequestHtmlForCategory("save", item);
  context.sheetData.contests = getRollRequestHtmlForCategory("contest", item);
}

function _prepareFormulas(context) {
  const damage = {};
  const healing = {};
  const other = {};

  Object.entries(context.system.formulas).forEach(([key, formula]) => {
    switch (formula.category) {
      case "damage": 
        formula.types = CONFIG.DC20RPG.DROPDOWN_DATA.damageTypes;
        damage[key] = formula; 
        break;
      case "healing": 
        formula.types = CONFIG.DC20RPG.DROPDOWN_DATA.healingTypes;
        healing[key] = formula; 
        break;
      case "other": 
        other[key] = formula; 
        break;
    }
  });
  context.formulas = [damage, healing, other];
}

/**
 * Extend the basic ItemSheet with some very simple modifications
 * @extends {ItemSheet}
 */
class DC20RpgItemSheet extends ItemSheet {

  /** @override */
  static get defaultOptions() {
    return foundry.utils.mergeObject(super.defaultOptions, {
      classes: ["dc20rpg", "sheet", "item"],
      width: 520,
      height: 520,
      tabs: [
        { navSelector: ".sheet-tabs", contentSelector: ".item-sheet-body", initial: "description" }, 
        { navSelector: ".roll-tabs", contentSelector: ".roll-content", initial: "details" },
        { navSelector: ".advanced-tabs", contentSelector: ".advanced-content", initial: "adv-core"}
      ],
      dragDrop: [
        {dragSelector: ".effects-row[data-effect-id]", dropSelector: null},
      ]
    });
  }

  /** @override */
  get template() {
    return `systems/dc20rpg/templates/item_v2/${this.item.type}.hbs`;
  }

  /* -------------------------------------------- */

  /** @override */
  async getData() {
    const context = await super.getData();
    duplicateItemData(context, this.item);
    prepareItemData(context, this.item);
    preprareSheetData(context, this.item);
    prepareActiveEffects(this.item, context);

    // Enrich text editors
    context.enriched = {};
    context.enriched.description = await TextEditor.enrichHTML(context.system.description, {secrets:true});

    return context;
  }

  /** @override */
  activateListeners(html) {
    super.activateListeners(html);
    activateCommonLinsters(html, this.item);
  }

  _onDragStart(event) {
    // Create drag data
    let dragData;

    const dataset = event.currentTarget.dataset;
    if ( dataset.effectId ) {
      const effect = this.item.effects.get(dataset.effectId);
      dragData = effect.toDragData();
    }
    if ( !dragData ) return;

    // Set data transfer
    event.dataTransfer.setData("text/plain", JSON.stringify(dragData));
  }
}

class DC20RpgCombatTracker extends CombatTracker {

  /** @override */
  static get defaultOptions() {
    return foundry.utils.mergeObject(super.defaultOptions, {
      id: "combat",
      template: "systems/dc20rpg/templates/sidebar/combat-tracker.hbs",
      title: "COMBAT.SidebarTitle",
      scrollY: [".directory-list"]
    });
  }

  /** @override */
  async getData(options={}) {
    const context = await super.getData(options);
    context.turns = await this._prepareTurnsWithCharacterType();
    return context;
  }

  async _prepareTurnsWithCharacterType() {
    const combat = this.viewed;
    const hasCombat = combat !== null;
    if ( !hasCombat ) return context;

    // Format information about each combatant in the encounter
    let hasDecimals = false;
    const turns = [];
    const skipped = new Map();
    for ( let [i, combatant] of combat.turns.entries() ) {
      if ( !combatant.visible ) continue;

      // Prepare turn data
      const resource = combatant.permission >= CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER ? combatant.resource : null;
      const turn = {
        id: combatant.id,
        name: combatant.name,
        slot: 20 - combatant.initiative,
        canRollInitiative: combatant.canRollInitiative,
        img: await this._getCombatantThumbnail(combatant),
        active: i === combat.turn,
        owner: combatant.isOwner,
        defeated: combatant.isDefeated,
        hidden: combatant.hidden,
        initiative: combatant.initiative,
        hasRolled: combatant.initiative !== null,
        hasResource: resource !== null,
        resource: resource,
        canPing: (combatant.sceneId === canvas.scene?.id) && game.user.hasPermission("PING_CANVAS"),
        companions: combatant.companions || []
      };
      if ( (turn.initiative !== null) && !Number.isInteger(turn.initiative) ) hasDecimals = true;
      turn.css = [
        turn.active ? "active" : "",
        turn.hidden ? "hidden" : "",
        turn.defeated ? "defeated" : ""
      ].join(" ").trim();

      // Actor and Token status effects
      turn.effects = new Set();
      for ( const effect of (combatant.actor?.temporaryEffects || []) ) {
        if ( effect.statuses.has(CONFIG.specialStatusEffects.DEFEATED) ) turn.defeated = true;
        else if ( effect.img ) turn.effects.add(effect.img);
      }
      if (combatant.skip) skipped.set(combatant.id, turn); 
      else turns.push(turn);
    }

    // Link skipped companions to its owners
    turns.forEach(turn => {
      const companionTurn = [];
      turn.companions.forEach(compCombatantId => {
        const compCombatant = skipped.get(compCombatantId);
        if (compCombatant) companionTurn.push(compCombatant);
      });
      turn.companionTurn = companionTurn;
    });

    // Format initiative numeric precision
    const precision = CONFIG.Combat.initiative.decimals;
    turns.forEach(t => {
      if ( t.initiative !== null ) t.initiative = t.initiative.toFixed(hasDecimals ? precision : 0);
    });

    return turns;
  }

  /** @override */
  activateListeners(html) {
    super.activateListeners(html);

    html.find('.change-dc').change(ev => this._changeinitiativeDC(valueOf(ev)));
    html.find('.initative-slot').click(ev => {
      const combatant = this.viewed.combatants.get(datasetOf(ev).combatantId);
      return combatant.update({initiative: null})
    });
  }

  _changeinitiativeDC(value) {
    const dc = parseInt(value);
    const combat = this.viewed;
    combat.update({['flags.dc20rpg.initiativeDC']: dc});
  }

    /** @override */
    async _onCombatantControl(event) {
      const dataset = datasetOf(event);
      event.stopPropagation();

      if (dataset.control === "addToSlot") {
        const combat = this.combats.find(combat => combat.active);
        const combatant = combat.combatants.get(dataset.combatantId);
        if (!combatant) return;
        
        const currentInitative = combat.combatant.initiative;
        if (currentInitative === null || currentInitative === undefined) combatant.update({initiative: 20});
        else await combatant.update({initiative: currentInitative - 1});
      }
      else if (dataset.control === "addSpecificToSlot") {
        const combat = this.combats.find(combat => combat.active);
        const combatant = combat.combatants.get(dataset.combatantId);
        if (!combatant) return;

        const selected = await getSimplePopup("select", {
          header: "Select Initiative Slot", selectOptions: {1:1, 2:2, 3:3, 4:4, 5:5, 6:6, 7:7, 8:8, 9:9, 10:10, 11:11, 12:12, 13:13, 14:14, 15:15, 16:16, 17:17, 18:18, 19:19, 20:20}
        });
        if (selected) await combatant.update({initiative: 20 - selected});
      }
      else {
        await super._onCombatantControl(event);
      }
    }
}

/**
 * Define a set of template paths to pre-load
 * Pre-loaded templates are compiled and cached for fast access when rendering
 * @return {Promise}
 */
async function preloadHandlebarsTemplates() {
  return loadTemplates(Object.values(allPartials()));
}
function allPartials() {
  return {
    ...actorPartials(),
    ...itemPartials(),
    ...sharedPartials()
  };
}

function actorPartials() {
  return {
    "Character Skills": "systems/dc20rpg/templates/actor_v2/parts/character/skills.hbs",
    
    "Dmg Reduction": "systems/dc20rpg/templates/actor_v2/parts/shared/dmg-reduction.hbs",
    "Status Resistances": "systems/dc20rpg/templates/actor_v2/parts/shared/condition-immunities.hbs",
    "Item Table": "systems/dc20rpg/templates/actor_v2/parts/shared/item-table.hbs",
    "Effects Table": "systems/dc20rpg/templates/actor_v2/parts/shared/effects-table.hbs",
    "Traits Table": "systems/dc20rpg/templates/actor_v2/parts/shared/traits-table.hbs",
    "Statuses": "systems/dc20rpg/templates/actor_v2/parts/shared/statuses.hbs"
  }
}

function itemPartials() {
  return {
    "Header": "systems/dc20rpg/templates/item_v2/parts/shared/header.hbs",
    "Advanced": "systems/dc20rpg/templates/item_v2/parts/shared/advanced.hbs",
    "Properties": "systems/dc20rpg/templates/item_v2/parts/shared/properties.hbs",
    "Roll": "systems/dc20rpg/templates/item_v2/parts/shared/roll.hbs",
    "Roll Details": "systems/dc20rpg/templates/item_v2/parts/shared/roll-tab/roll-details.hbs",
    "Usage Cost": "systems/dc20rpg/templates/item_v2/parts/shared/roll-tab/usage-cost.hbs",
    "Target": "systems/dc20rpg/templates/item_v2/parts/shared/roll-tab/target.hbs",
    "Advancements": "systems/dc20rpg/templates/item_v2/parts/shared/advancements.hbs",
    "Basic Config": "systems/dc20rpg/templates/item_v2/parts/shared/basic-config.hbs", 
    "Advancement Core": "systems/dc20rpg/templates/item_v2/parts/shared/advanced-tab/adv-core.hbs",
    "Enhancements": "systems/dc20rpg/templates/item_v2/parts/shared/advanced-tab/enhancements.hbs",
    "Conditional": "systems/dc20rpg/templates/item_v2/parts/shared/advanced-tab/conditionals.hbs"
  }
}

function sharedPartials() {
  return {
    "Tooltip": "systems/dc20rpg/templates/shared/tooltip.hbs",
    "Context Menu": "systems/dc20rpg/templates/shared/context-menu.hbs",
  }
}

const versions = [
                  "0.8.1-hf2", "0.8.2", "0.8.2-hf1", "0.8.3", "0.8.4", "0.8.4-hf1", "0.8.5", 
                  "0.9.0", "0.9.5"
                ];

async function runMigrationCheck() {
  const lastMigratedVersion = game.settings.get("dc20rpg", "lastMigration");
  const currentVersion = game.system.version;

  const totalDocuments = game.actors.size + game.scenes.size + game.items.size;
  if (!lastMigratedVersion && totalDocuments === 0) {
    // It is a new world that does not need migration
    game.settings.set("dc20rpg", "lastMigration", currentVersion);
  }
  else if (!lastMigratedVersion) {
    // This world was created before migration scripts were introduced. We want to run all the scripts
    await _runMigration("0.8.1-hf2", currentVersion);
  }
  else if (_requiresMigration(lastMigratedVersion, currentVersion)) {
    await _runMigration(lastMigratedVersion, currentVersion);
  }
}

async function forceRunMigration(fromVersion, toVersion, migrateModules) {
  await _runMigration(fromVersion, toVersion, true, migrateModules, false);
}

function _requiresMigration(lastMigration, currentVersion) {
  const last = versions.indexOf(lastMigration);
  const current = versions.indexOf(currentVersion);
  return current > last;
}

async function _runMigration(lastMigration, currentVersion, skipLastMigrationValueUpdate=false, migrateModules=new Set(), testPath=false) {
  const after = versions.indexOf(lastMigration);
  const until = versions.indexOf(currentVersion);

  for (let i = after + 1; i <= until; i++) {
    const migratingTo = versions[i];
    ui.notifications.notify(`Running system migration for version: ${migratingTo}`, {permanent: true});
    const dialog =  new SimplePopup("non-closable", {header: "Running Migration", message: `Running system migration for version: ${migratingTo}... Please wait it might take a while. This window will be closed once the migration is complete. Grab a coffee or something :D`}, {title: "Popup"});
    await dialog._render(true);
    try {
      const migrationPath = testPath ? `../../migrations/${migratingTo}.mjs` : `../migrations/${migratingTo}.mjs`;
      const migrationModule = await import(migrationPath);
      await migrationModule.runMigration(migrateModules);
      if (!skipLastMigrationValueUpdate) await game.settings.set("dc20rpg", "lastMigration", migratingTo);
      ui.notifications.notify(`Finished system migration for version: ${migratingTo}`, {permanent: true});
      dialog.close();
    }
    catch (e) {
      ui.notifications.error(`System migration for version '${migratingTo}' failed with: ${e}`, {permanent: true});
      dialog.close();
    } 
  }
}

/**
 * Returns owners of specific Actor
 */
function getActiveActorOwners(actor, allowGM) {
  if (!actor) return [];
  const ownership = Object.entries(actor.ownership).filter(([userId, value]) => value === 3).map(([userId, value]) => userId);

  const owners = [];
  for (const ownerId of ownership) {
    if (ownerId === "default") continue;
    const user = game.users.get(ownerId);
    if (user.isGM && !allowGM) continue;
    if (user.active) owners.push(user);
  }
  return owners;
}

function getActivePlayers(allowGM) {
  return game.users
      .filter(user => user.active)
      .filter(user => {
        if (user.isGM) return allowGM;
        else return true;
      })
}

function prepareDC20tools() {
  game.dc20rpg = {
    DC20RpgActor,
    DC20RpgItem,
    DC20RpgCombatant,
    DC20RpgMeasuredTemplate,
    rollItemWithName,
    forceRunMigration,
    effects: {
      createEffectOn,
      deleteEffectFrom,
      getEffectByName,
      getEffectById,
      getEffectByKey,
      toggleEffectOn,
      createOrDeleteEffect,
    },
    statuses: {
      hasStatusWithId,
      getStatusWithId,
      addStatusWithIdToActor,
      removeStatusWithIdFromActor
    },
    resources: {
      regainBasicResource,
      regainCustomResource,
      subtractBasicResource,
      subtractCustomResource,
      canSubtractBasicResource,
      canSubtractCustomResource,
      subtractAP,
    },
    tools: {
      getSelectedTokens,
      createItemOnActor,
      deleteItemFromActor,
      getItemFromActorByKey,
      promptRoll,
      promptItemRoll,
      promptRollToOtherPlayer,
      getSimplePopup,
      sendSimplePopupToUsers,
      getActiveActorOwners,
      tokenToTarget,
      calculateForTarget,
      applyDamage,
      applyHealing,
      makeMoveAction,
      prepareHelpAction,
      createRestDialog,
      sendDescriptionToChat
    },
    events: {
      runEventsFor,
      reenableEventsOn,
      registerEventTrigger,
      registerEventType,
      registerEventReenableTrigger
    },
    macros: {
      createTemporaryMacro,
      runTemporaryMacro,
      runTemporaryItemMacro,
      registerItemMacroTrigger
    },
    keywords: {
      addUpdateItemToKeyword,
      removeUpdateItemFromKeyword,
      updateKeywordValue,
      addNewKeyword,
      removeKeyword
    }
  };
}

function initDC20Config() {
  // Prepare Skill and Language default list
  const skillStore = game.settings.get("dc20rpg", "skillStore");

  const skills = {};
  Object.entries(skillStore.skills).forEach(([key, skill]) => skills[key] = CONFIG.DC20RPG.skills[key] || skill.label);
  CONFIG.DC20RPG.skills = skills;
  const tradeSkills = {};
  Object.entries(skillStore.trades).forEach(([key, skill]) => tradeSkills[key] = CONFIG.DC20RPG.tradeSkills[key] || skill.label);
  CONFIG.DC20RPG.tradeSkills = tradeSkills;
  const languages = {};
  Object.entries(skillStore.languages).forEach(([key, skill]) => languages[key] = CONFIG.DC20RPG.languages[key] || skill.label);
  CONFIG.DC20RPG.languages = languages;

  // Prepare Attribute Checks and Saves
  const saveTypes = {
    phy: "Physical Save",
    men: "Mental Save",
  };
  const attributeChecks = {};
  Object.entries(CONFIG.DC20RPG.DROPDOWN_DATA.attributesWithPrime).forEach(([key, label]) => {
    saveTypes[key] = `${label} Save`;
    attributeChecks[key] = `${label} Check`;
  });
  CONFIG.DC20RPG.ROLL_KEYS.saveTypes = saveTypes;
  CONFIG.DC20RPG.ROLL_KEYS.attributeChecks = attributeChecks;

  // Prepare Basic Checks
  CONFIG.DC20RPG.ROLL_KEYS.baseChecks = {
    att: "Attack Check",
    spe: "Spell Check",
  };
  // Martial Check requires acrobatic and athletics skills
  if (CONFIG.DC20RPG.skills.acr && CONFIG.DC20RPG.skills.ath) {
    CONFIG.DC20RPG.ROLL_KEYS.baseChecks.mar = "Martial Check";
  }

  // Prepare Skill Checks
  const skillChecks = {};
  Object.entries(CONFIG.DC20RPG.skills).forEach(([key, label]) => {
    skillChecks[key] = `${label} Check`;
  });
  CONFIG.DC20RPG.ROLL_KEYS.skillChecks = skillChecks;

  // Prepare Trade Skill Checks
  const tradeChecks = {};
  Object.entries(CONFIG.DC20RPG.tradeSkills).forEach(([key, label]) => {
    tradeChecks[key] = `${label} Check`;
  });
  CONFIG.DC20RPG.ROLL_KEYS.tradeChecks = tradeChecks;

  // Prepare Contested Checks
  CONFIG.DC20RPG.ROLL_KEYS.contests = {
    ...CONFIG.DC20RPG.ROLL_KEYS.saveTypes,
    ...CONFIG.DC20RPG.ROLL_KEYS.baseChecks,
    ...CONFIG.DC20RPG.ROLL_KEYS.skillChecks
  };

  // Prepare Core Checks
  CONFIG.DC20RPG.ROLL_KEYS.checks = {
    ...CONFIG.DC20RPG.ROLL_KEYS.baseChecks,
    ...CONFIG.DC20RPG.ROLL_KEYS.attributeChecks,
    ...CONFIG.DC20RPG.ROLL_KEYS.skillChecks
  };

  // Preapre All Checks
  CONFIG.DC20RPG.ROLL_KEYS.allChecks = {
    ...CONFIG.DC20RPG.ROLL_KEYS.checks,
    ...CONFIG.DC20RPG.ROLL_KEYS.tradeChecks
  };
}

const DC20RPG = {
  SYSTEM_CONSTANTS: {
    JOURNAL_UUID: {}
  },
  DROPDOWN_DATA: {},
  TRANSLATION_LABELS: {},
  ROLL_KEYS: {},
};

//=========================================================================
//      CHANGEABLE - We want to allow user to register new options        =
//=========================================================================
DC20RPG.eventTypes = {
  basic: "Basic",
  healing: "Apply Healing",
  damage: "Apply Damage",
  checkRequest: "Check Request",
  saveRequest: "Save Request",
  resource: "Resource Manipulation",
  macro: "Run Effect Macro"
};

DC20RPG.allEventTriggers = {
  turnStart: "Turn Start",
  turnEnd: "Turn End",
  nextTurnEnd: "Next Turn End",
  actorWithIdStartsTurn: "Caster starts its turn",
  actorWithIdEndsTurn: "Caster ends its turn",
  actorWithIdEndsNextTurn: "Caster ends its next turn",
  targetConfirm: "Target Confirmed",
  combatStart: "Combat Start",
  damageTaken: "Damage Taken",
  healingTaken: "Healing Taken",
  effectApplied: "Effect Applied",
  effectRemoved: "Effect Removed",
  effectEnabled: "Effect Enabled",
  effectDisabled: "Effect Disabled",
  rollSave: "Save Roll",
  rollCheck: "Check Roll",
  rollItem: "Any Item Roll",
  attack: "Item Attack Roll",
  move: "Actor Move",
  crit: "On Nat 20",
  critFail: "On Nat 1",
  never: "Never",
  instant: "Instant",
  rest: "On Rest End",
};

DC20RPG.reenableTriggers = {
  "": "",
  turnStart: "Turn Start",
  turnEnd: "Turn End",
  actorWithIdStartsTurn: "Caster starts its turn",
  actorWithIdEndsTurn: "Caster ends its turn",
  combatStart: "Combat Start",
  effectApplied: "Effect Applied",
  effectRemoved: "Effect Removed",
  effectEnabled: "Effect Enabled",
  effectDisabled: "Effect Disabled",
};

DC20RPG.macroTriggers = {
  onDemand: "On Demand",
  onCreate: "After Creation",
  preDelete: "Before Deletion",
  onItemToggle: "On Item Toggle/Equip",
  onRollPrompt: "On Roll Prompt",
  rollLevelCheck: "On Roll Level Check",
  preItemCost: "Before Item Cost Check",
  preItemRoll: "Before Item Roll",
  postItemRoll: "After Item Roll",
  postChatMessageCreated: "After Chat Message Created",
  enhancementReset: "On Enhancement Reset",
  onKeywordUpdate: "On Keyword Update"
};

DC20RPG.skills = {
  awa: "Awareness",
  ath: "Athletics",
  inm: "Intimidation",
  acr: "Acrobatics",
  tri: "Trickery",
  ste: "Stealth",
  inv: "Investigation",
  med: "Medicine",
  sur: "Survival",
  ani: "Animal",
  ins: "Insight",
  inf: "Influence",
};

DC20RPG.tradeSkills = {
  alc: "Alchemy",
  arc: "Arcana",
  bla: "Blacksmithing",
  bre: "Brewing",
  cap: "Carpentry",
  car: "Cartography",
  coo: "Cooking",
  cry: "Cryptography",
  dis: "Disguise",
  eng: "Engineering",
  gam: "Gaming",
  gla: "Glassblower",
  her: "Herbalism",
  his: "History",
  ill: "Illustration",
  jew: "Jeweler",
  lea: "Leatherworking",
  loc: "Lockpicking",
  mas: "Masonry",
  mus: "Musician",
  nat: "Nature",
  occ: "Occultism",
  rel: "Religion",
  scu: "Sculpting",
  the: "Theatre",
  tin: "Tinkering",
  wea: "Weaving",
  veh: "Vehicles"
};

DC20RPG.languages = {
  com: "Common",
  hum: "Human",
  dwa: "Dwarven",
  elv: "Elvish",
  gno: "Gnomish",
  hal: "Halfling",
  sig: "Common Sign",
  gia: "Giant",
  dra: "Draconic",
  orc: "Orcish",
  fey: "Fey",
  ele: "Elemental",
  cel: "Celestial",
  fie: "Fiend",
  dee: "Deep Speech"
};

//=========================================================================
//    TRANSLATION-LABELS - We are using that for key->label translation   =
//=========================================================================
DC20RPG.TRANSLATION_LABELS.classTypes = {
  martial: "Martial",
  spellcaster: "Spellcaster",
  hybrid: "Hybrid"
};

DC20RPG.TRANSLATION_LABELS.attributes = {
  mig: "Might",
  agi: "Agility",
  int: "Inteligence",
  cha: "Charisma"
};

DC20RPG.TRANSLATION_LABELS.combatTraining = {
  weapons: "Weapons",
  lightShield: "Light Shield",
  heavyShield: "Heavy Shield",
  lightArmor: "Light Armor",
  heavyArmor: "Heavy Armor",
};

//====================================================================================
//    DROPDOWN-DATA - We are using that for dropodowns and key->label translations   =
//====================================================================================
DC20RPG.DROPDOWN_DATA.sizes = {
  tiny: "Tiny",
  small: "Small",
  medium: "Medium",
  large: "Large",
  huge: "Huge",
  gargantuan: "Gargantuan",
  colossal: "Colossal",
  titanic: "Titanic",
};

DC20RPG.DROPDOWN_DATA.attributesWithPrime = {
  prime: "Prime",
  ...DC20RPG.TRANSLATION_LABELS.attributes
};

DC20RPG.DROPDOWN_DATA.shortAttributes = {
  mig: "MIG",
  agi: "AGI",
  int: "INT",
  cha: "CHA",
  prime: "PRI"
};

DC20RPG.DROPDOWN_DATA.dcCalculationTypes = {
  spell: "Spellcasting",
  martial: "Martial",
  flat: "Flat",
  ...DC20RPG.TRANSLATION_LABELS.attributes
};

DC20RPG.DROPDOWN_DATA.basicActionsCategories = {
  offensive: "Offensive",
  defensive: "Defensive",
  utility: "Utility",
  reaction: "Reaction",
  skillBased: "Skill Based"
};

DC20RPG.DROPDOWN_DATA.weaponTypes = {
  melee: "Melee",
  ranged: "Ranged",
  special: "Special"
};

DC20RPG.DROPDOWN_DATA.equipmentTypes = {
  light: "Light Armor",
  heavy: "Heavy Armor",
  lshield: "Light Shield",
  hshield: "Heavy Shield",
  clothing: "Clothing",
  trinket: "Trinket",
  other: "Other"
};

DC20RPG.DROPDOWN_DATA.consumableTypes = {
  ammunition: "Ammunition",
  food: "Food",
  poison: "Poison",
  potion: "Potion",
  rod: "Rod",
  scroll: "Scroll",
  wand: "Wand",
  trap: "Trap",
  trinket: "Trinket",
  other: "Other"
};

DC20RPG.DROPDOWN_DATA.techniqueTypes = {
  maneuver: "Maneuver",
  technique: "Technique"
};

DC20RPG.DROPDOWN_DATA.spellTypes = {
  cantrip: "Cantrip",
  spell: "Spell",
  ritual: "Ritual"
};

DC20RPG.DROPDOWN_DATA.spellLists = {
  arcane: "Arcane",
  divine: "Divine",
  primal: "Primal"
};

DC20RPG.DROPDOWN_DATA.magicSchools = {
  astromancy: "Astromancy",
  chronomancy: "Chronomancy",
  conjuration: "Conjuration",
  destruction: "Destruction",
  divination: "Divination",
  enchantment: "Enchantment",
  illusion: "Illusion",
  necromancy: "Necromancy",
  protection: "Protection",
  restoration: "Restoration",
  transmutation: "Transmutation"
};

DC20RPG.DROPDOWN_DATA.components = {
  verbal: "Verbal",
  somatic: "Somatic",
  material: "Material"
};

DC20RPG.DROPDOWN_DATA.defences = {
  area: "Area Defense",
  precision: "Precision Defense"
};

DC20RPG.DROPDOWN_DATA.precisionDefenceFormulasLabels = {
  standard: "Standard Formula",
  standardMaxAgi: "Max Agility Limit",
  berserker: "Berserker Defense",
  patient: "Patient Defense",
  custom: "Custom Formula",
  flat: "Flat",
};

DC20RPG.DROPDOWN_DATA.areaDefenceFormulasLabels = {
  standard: "Standard Formula",
  custom: "Custom Formula",
  patient: "Patient Defense",
  flat: "Flat"
};

DC20RPG.DROPDOWN_DATA.moveTypes = {
  ground: "Ground Speed",
  glide: "Gliding Speed",
  burrow: "Burrowing Speed",
  climbing: "Climbing Speed",
  swimming: "Swimming Speed",
  flying: "Flying Speed",
};

DC20RPG.DROPDOWN_DATA.logicalExpressions = {
  "==": "=",
  "!=": "!=",
  ">=": ">=",
  ">": ">",
  "<=": "<=",
  "<": "<",
  "has": "has",
  "hasNot": "hasn't"
};

DC20RPG.DROPDOWN_DATA.formulaCategories = {
  damage: "Damage Formula",
  healing: "Healing Formula"
};

DC20RPG.DROPDOWN_DATA.physicalDamageTypes = {
  bludgeoning: "Bludgeoning",
  slashing: "Slashing",
  piercing: "Piercing",
},

DC20RPG.DROPDOWN_DATA.elementalDamageTypes = {
  corrosion: "Corrosion",
  cold: "Cold",
  fire: "Fire",
  lightning: "Lightning",
  poison: "Poison",
  sonic: "Sonic",
};

DC20RPG.DROPDOWN_DATA.mysticalDamageTypes = {
  psychic: "Psychic",
  radiant: "Radiant",
  umbral: "Umbral"
};

DC20RPG.DROPDOWN_DATA.damageResistances = {
  ...DC20RPG.DROPDOWN_DATA.physicalDamageTypes,
  ...DC20RPG.DROPDOWN_DATA.elementalDamageTypes,
  ...DC20RPG.DROPDOWN_DATA.mysticalDamageTypes,
};

DC20RPG.DROPDOWN_DATA.damageTypes = {
  ...DC20RPG.DROPDOWN_DATA.physicalDamageTypes,
  ...DC20RPG.DROPDOWN_DATA.elementalDamageTypes,
  ...DC20RPG.DROPDOWN_DATA.mysticalDamageTypes,
  true: "True"
};

DC20RPG.DROPDOWN_DATA.healingTypes = {
  heal: "Health",
  temporary: "Temporary"
};

DC20RPG.DROPDOWN_DATA.inventoryTypes = {
  weapon: "Weapon",
  equipment: "Equipment",
  consumable: "Consumable",
  loot: "Loot"
};

DC20RPG.DROPDOWN_DATA.spellsTypes = {
  spell: "Spell"
};

DC20RPG.DROPDOWN_DATA.techniquesTypes = {
  technique: "Technique"
};

DC20RPG.DROPDOWN_DATA.featuresTypes = {
  feature: "Feature"
};

DC20RPG.DROPDOWN_DATA.advancementItemTypes = {
  any: "Any Type",
  ...DC20RPG.DROPDOWN_DATA.featuresTypes,
  ...DC20RPG.DROPDOWN_DATA.spellsTypes,
  ...DC20RPG.DROPDOWN_DATA.techniquesTypes
};

DC20RPG.DROPDOWN_DATA.creatableTypes = {
  ...DC20RPG.DROPDOWN_DATA.inventoryTypes,
  ...DC20RPG.DROPDOWN_DATA.spellsTypes,
  ...DC20RPG.DROPDOWN_DATA.techniquesTypes,
  ...DC20RPG.DROPDOWN_DATA.featuresTypes
};

DC20RPG.DROPDOWN_DATA.allItemTypes = {
  ...DC20RPG.DROPDOWN_DATA.inventoryTypes,
  ...DC20RPG.DROPDOWN_DATA.spellsTypes,
  ...DC20RPG.DROPDOWN_DATA.techniquesTypes,
  ...DC20RPG.DROPDOWN_DATA.featuresTypes,
  class: "Class",
  subclass: "Subclass",
  ancestry: "Ancestry",
  background: "Background"
};

DC20RPG.DROPDOWN_DATA.featureSourceTypes = {
  class: "Class Talent",
  subclass: "Subclass Talent",
  talent: "General Talent",
  ancestry: "Ancestry Trait",
  inner: "Inner Feature",
  background: "Background Talent",
  monster: "Monster Feature",
  other: "Other"
};

DC20RPG.DROPDOWN_DATA.conditions = {
  bleeding: "Bleeding",
  blinded: "Blinded",
  burning: "Burning",
  charmed: "Charmed",
  dazed: "Dazed",
  deafened: "Deafened",
  disoriented: "Disoriented",
  doomed: "Doomed",
  exhaustion: "Exhaustion",
  exposed: "Exposed",
  frightened: "Frightened",
  hindered: "Hindered",
  immobilized: "Immobilized",
  impaired: "Impaired",
  incapacitated: "Incapacitated",
  intimidated: "Intimidated",
  paralyzed: "Paralyzed",
  petrified: "Petrified",
  poisoned: "Poisoned",
  restrained: "Restrained",
  slowed: "Slowed",
  stunned: "Stunned",
  surprised: "Surprised",
  taunted: "Taunted",
  tethered: "Tethered",
  terrified: "Terrified",
  unconscious: "Unconscious",
  weakened: "Weakened",
};

DC20RPG.DROPDOWN_DATA.statusResistances = {
  magical: "Magical Effect",
  curse: "Curse",
  movement: "Forced Movement",
  prone: "Prone",
  grappled: "Grappled",
  ...DC20RPG.DROPDOWN_DATA.conditions
};

DC20RPG.DROPDOWN_DATA.currencyTypes = {
  pp: "PP",
  gp: "GP",
  sp: "SP",
  cp: "CP"
};

DC20RPG.DROPDOWN_DATA.invidualTargets = {
  self: "Self",
  ally: "Ally",
  enemy: "Enemy",
  creature: "Creature",
  object: "Object",
  space: "Space"
};

DC20RPG.DROPDOWN_DATA.areaTypes = {
  arc: "Arc",
  aura: "Aura",
  cone: "Cone",
  cube: "Cube",
  cylinder: "Cylinder",
  line: "Line",
  sphere: "Sphere",
  radius: "Radius",
  wall: "Wall",
  area: "Custom Area"
};

DC20RPG.DROPDOWN_DATA.durations = {
  instantaneous: "Instantaneous",
  continuous: "Continuous",
  sustain: "Sustained"
};

DC20RPG.DROPDOWN_DATA.timeUnits = {
  turns: "Turns",
  rounds: "Rounds",
  minutes: "Minutes",
  hours: "Hours",
  days: "Days",
  months: "Months",
  years: "Years",
  permanent: "Permanent",
  untilCanceled: "Until Canceled"
};

DC20RPG.DROPDOWN_DATA.restTypes = {
  quick: "Quick Rest",
  short: "Short Rest",
  long: "Long Rest",
  full: "Full Rest"
};

DC20RPG.DROPDOWN_DATA.resetTypes = {
  ...DC20RPG.DROPDOWN_DATA.restTypes,
  halfOnShort: "Half on Short Rest",
  combat: "Combat Start",
  round: "Round End",
};

DC20RPG.DROPDOWN_DATA.chargesResets = {
  ...DC20RPG.DROPDOWN_DATA.resetTypes,
  day: "Daily",
  charges: "Charges"
};

DC20RPG.DROPDOWN_DATA.rarities = {
  common: "Common",
  uncommon: "Uncommon",
  rare: "Rare",
  veryRare: "Very Rare",
  legendary: "Legendary"
};

DC20RPG.DROPDOWN_DATA.properties = {
  agiDis: "Agi Checks DisADV",
  ammo: "Ammunition",
  attunement: "Attunement",
  concealable: "Concealable",
  finesee: "Finesee",
  focus: "Focus",
  reach: "Reach",
  requirement: "Requirement",
  reload: "Reload",
  special: "Special",
  thrown: "Thrown",
  twoHanded: "Two-Handed",
  versatile: "Versatile",
  sturdy: "Sturdy (Agility Checks DisADV)",
  damageReduction: "Damage Reduction",
  dense: "Dense (-1 Speed)",
  mobile: "Mobile",
  impact: "Impact",
  threatening: "Threatening",
  reinforced: "Reinforced (Max Agility Limit)",
  mounted: "Mounted",
  unwieldy: "Unwieldy",
  silent: "Silent",
  toss: "Toss",
  returning: "Returning",
  capture: "Capture",
  multiFaceted: "Multi-Faceted",
  guard: "Guard",
  heavy: "Heavy",
  longRanged: "Long-Ranged",
  silent: "Silent",
  adIncrease: "AD Increase",
  pdIncrease: "PD Increase",
  edr: "EDR",
  pdr: "PDR",
  bulky: "Bulky",
  rigid: "Rigid",
  grasp: "Grasp",
  mounted: "Mounted",
},

DC20RPG.DROPDOWN_DATA.rollTemplates = {
  dynamic: "Dynamic Attack Save",
  attack: "Attack",
  check: "Check",
  save: "Save",
  contest: "Contest",
};

DC20RPG.DROPDOWN_DATA.actionTypes = {
  attack: "Attack",
  check: "Check",
  other: "Other",
  help: "Help"
};

DC20RPG.DROPDOWN_DATA.attackTypes = {
  attack: "Attack Check",
  spell: "Spell Check"
};

DC20RPG.DROPDOWN_DATA.rangeTypes = {
  melee: "Melee Attack",
  ranged: "Range Attack"
};

DC20RPG.DROPDOWN_DATA.rollRequestCategory = {
  save: "Save",
  contest: "Contest"
};

DC20RPG.DROPDOWN_DATA.checkRangeType = {
  attackmelee: "Melee Attack",
  attackranged: "Range Attack",
  spellmelee: "Melee Spell",
  spellranged: "Range Spell",
};

DC20RPG.DROPDOWN_DATA.meleeWeaponStyles = {
  axe: "Axe Style",
  chained: "Chained Style",
  hammer: "Hammer Style",
  pick: "Pick Style",
  spear: "Spear Style",
  staff: "Staff Style",
  sword: "Sword Style",
  fist: "Fist Style",
  whip: "Whip Style"
};

DC20RPG.DROPDOWN_DATA.rangedWeaponStyles = {
  bow: "Bow Style",
  crossbow: "Crossbow Style"
};

DC20RPG.DROPDOWN_DATA.weaponStyles = {
  ...DC20RPG.DROPDOWN_DATA.meleeWeaponStyles,
  ...DC20RPG.DROPDOWN_DATA.rangedWeaponStyles
};

DC20RPG.DROPDOWN_DATA.jumpCalculationKeys = {
  ...DC20RPG.DROPDOWN_DATA.attributesWithPrime,
  flat: "Flat Value"
};

DC20RPG.DROPDOWN_DATA.templatesActivationEffectTypes = {
  "": "None",
  "all": "All Tokens",
  "enemy": "Enemy Tokens",
  "ally": "Ally Tokens",
  "selector": "Token Selector"
};

//=========================================================================
//        SYSTEM CONSTANTS - Some Ids and other hardcoded stuff           =
//=========================================================================
DC20RPG.SYSTEM_CONSTANTS.rollLevelChange = {
  adv: "Advantage", 
  dis: "Disadvantage"
};

DC20RPG.SYSTEM_CONSTANTS.propertiesCost = {
  attunement: 0,
  ammo: 0,
  concealable: 1,
  reach: 1,
  reload: 0,
  thrown: 1,
  twoHanded: -1,
  versatile: 1,
  impact: 1,
  unwieldy: -1,
  silent: 1,
  toss: 1,
  returning: 1,
  capture: 0,
  multiFaceted: 1,
  guard: 1,
  heavy: 2,
  longRanged: 1,
  adIncrease: 1,
  pdIncrease: 1,
  edr: 2,
  pdr: 2,
  bulky: -1,
  rigid: -1,
  grasp: 1,
  mounted: 1,
};

DC20RPG.SYSTEM_CONSTANTS.skillMasteryLabel = {
  0: "Untrained",
  1: "Novice",
  2: "Adept",
  3: "Expert",
  4: "Master",
  5: "Grandmaster",
  6: "Grandmaster"
};

DC20RPG.SYSTEM_CONSTANTS.skillMasteryShort = {
  0: "-",
  1: "N",
  2: "A",
  3: "E",
  4: "M",
  5: "G",
  6: "G"
};

DC20RPG.SYSTEM_CONSTANTS.languageMasteryLabel = {
  0: "None",
  1: "Limited",
  2: "Fluent"
};

DC20RPG.SYSTEM_CONSTANTS.languageMasteryShort = {
  0: "-",
  1: "L",
  2: "F"
};

DC20RPG.SYSTEM_CONSTANTS.precisionDefenceFormulas = {
  standard: "8 + @combatMastery + @agi + @int + @pd.bonus",
};

DC20RPG.SYSTEM_CONSTANTS.areaDefenceFormulas = {
  standard: "8 + @combatMastery + @mig + @cha + @ad.bonus",
};

DC20RPG.SYSTEM_CONSTANTS.martialExpansion = "Compendium.dc20rpg.system-items.Item.DYjIy2EGmwfarZ8s";
DC20RPG.SYSTEM_CONSTANTS.spellcasterStamina = "Compendium.dc20rpg.system-items.Item.y7T8fH64IizcTw0K";

DC20RPG.SYSTEM_CONSTANTS.JOURNAL_UUID.deathsDoor = "Compendium.dc20rpg.rules.JournalEntry.amGWJPNztuALU8Fw";

DC20RPG.SYSTEM_CONSTANTS.JOURNAL_UUID.skillsJournal = {
  awa: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.e8d158aa79d9386e",
  ath: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.79561601ab4fde28",
  inm: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.b1b3452377f4d08c",
  acr: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.1dca3f8a2cf5a0f1",
  tri: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.ac98ee68ee06c485",
  ste: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.ec19af19bb7b55ef",
  inv: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.8fbe08ada0130e47",
  med: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.da6ce05121f5034e",
  sur: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.8c33adc637b3a1eb",
  ani: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.0ef16eb14c1a1949",
  ins: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.58253539b0e00f1d",
  inf: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.2988a8b8837f8347",
};

DC20RPG.SYSTEM_CONSTANTS.JOURNAL_UUID.tradeSkillsJournal = {
  ill: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.44af238d059dc591",
  mus: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.2c03a393671adfab",
  the: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.80d0246ffad3fc76",

  alc: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.2f1acce4ff8ae20e",
  bla: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.21198676e164533a",
  gla: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.3b0902b29165ffa4",
  her: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.c5e2ba07c7e317ac",
  jew: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.209ab0fce5b680e2",
  lea: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.b0446a7cd986ad04",
  scu: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.c6bc766b1646e931",
  tin: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.cc55cb96554dcf45",
  wea: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.193188c92ecf500c",

  bre: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.27093a368b6f7506",
  cap: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.5ffdb6f1879759c4",
  car: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.9601364d226e6bea",
  coo: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.d891f747ed539da6",
  mas: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.d547a198159fa5b5",
  veh: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.34ad7b0124f6cf1f",

  cry: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.821e481d694886da",
  dis: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.9d9eb42b43776bba",
  gam: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.0571b52f61d4a44e",
  loc: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.524e18ef64bf65c5",

  eng: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.GzTqr5DjfMF1Lqni",
  nat: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.9lx5NFMGbKa6wOug",
  his: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.3mvCqsfRkrGoNHAL",
  arc: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.2yu7rx90wveBO7W0",
  rel: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.PVUfWyrhkbBrn79q",
  occ: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.3Pq6ozSK8IoRO98l"
};

DC20RPG.SYSTEM_CONSTANTS.JOURNAL_UUID.languagesJournal = {
  com: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.5533596a44ec5abe",
  hum: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.5c5b070fd21c5dc4",
  dwa: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.b19668b820dce96e",
  elv: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.b24421fab355d18a",
  gno: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.3bddc67a463a2d4e",
  hal: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.c23a0811a9f258bf",
  sig: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.8252c71478125e56",
  gia: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.1d58911794446212",
  dra: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.4c451028e49dbba2",
  orc: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.cb00684b434793b9",
  fey: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.bd5baac08689fbf5",
  ele: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.db7bc5ab07d6aa49",
  cel: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.9e5e2ff0591520d4",
  fie: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.51a47d9b7407f9e6",
  dee: "Compendium.dc20rpg.rules.JournalEntry.Mkbcj2BN9VUgitFb.JournalEntryPage.876e8847369cd29c"
};

DC20RPG.SYSTEM_CONSTANTS.JOURNAL_UUID.weaponStylesJournal = {
  axe: "Compendium.dc20rpg.rules.JournalEntry.51wyjg5pkl8Vmh8e.JournalEntryPage.mAcVFce6zbhRTnhT",
  chained: "Compendium.dc20rpg.rules.JournalEntry.51wyjg5pkl8Vmh8e.JournalEntryPage.InTw8G1qVIu0Dp3v",
  hammer: "Compendium.dc20rpg.rules.JournalEntry.51wyjg5pkl8Vmh8e.JournalEntryPage.Gfy8diDLkPtI8gDu",
  pick: "Compendium.dc20rpg.rules.JournalEntry.51wyjg5pkl8Vmh8e.JournalEntryPage.tDkThSS22AdDCQns",
  spear: "Compendium.dc20rpg.rules.JournalEntry.51wyjg5pkl8Vmh8e.JournalEntryPage.HNZkdDlCaaGo4IhU",
  staff: "Compendium.dc20rpg.rules.JournalEntry.51wyjg5pkl8Vmh8e.JournalEntryPage.svYvbMnGphiuNJ8J",
  sword: "Compendium.dc20rpg.rules.JournalEntry.51wyjg5pkl8Vmh8e.JournalEntryPage.AAFiBV3mzk7PpRTJ",
  fist: "Compendium.dc20rpg.rules.JournalEntry.51wyjg5pkl8Vmh8e.JournalEntryPage.qfjN63bCAeQ2u6EM",
  whip: "Compendium.dc20rpg.rules.JournalEntry.51wyjg5pkl8Vmh8e.JournalEntryPage.seYjPL2iUDDmUjkx",
  bow: "Compendium.dc20rpg.rules.JournalEntry.51wyjg5pkl8Vmh8e.JournalEntryPage.oUiUr8lUymzGgi1Q",
  crossbow: "Compendium.dc20rpg.rules.JournalEntry.51wyjg5pkl8Vmh8e.JournalEntryPage.InTw8G1qVIu0Dp2v"
};

DC20RPG.SYSTEM_CONSTANTS.JOURNAL_UUID.basicActionsItems = {
  attack: "Compendium.dc20rpg.system-items.Item.hN1j1N0Bh8gTy0pG",
  disarm: "Compendium.dc20rpg.system-items.Item.Ks9SnrRBfRhVWgWo",
  grapple: "Compendium.dc20rpg.system-items.Item.Uc2lzTTEJL8GEf5y",
  shove: "Compendium.dc20rpg.system-items.Item.QDPNjfb8u5Jn3XPL",
  tackle: "Compendium.dc20rpg.system-items.Item.IolKDTVBEKdYMiGQ",
  disengage: "Compendium.dc20rpg.system-items.Item.ZK9sD2F2Sq7Jt3Kz",
  fullDisengage: "Compendium.dc20rpg.system-items.Item.KyNqnZf5DBLasmon",
  dodge: "Compendium.dc20rpg.system-items.Item.Y6oevdLqA31GPcbt",
  fullDodge: "Compendium.dc20rpg.system-items.Item.fvJRQv7oI9Pgoudk",
  hide: "Compendium.dc20rpg.system-items.Item.N5w8JDg9ddpC8nkm",
  move: "Compendium.dc20rpg.system-items.Item.GjZ8kGIOKxTzs7ZE",
  help: "Compendium.dc20rpg.system-items.Item.Tzha5zpqwpCZFIQ5",
  object: "Compendium.dc20rpg.system-items.Item.aIaSBL0WEAQbINL7",
  spell: "Compendium.dc20rpg.system-items.Item.AXG3pw2NOpxaJziA",
  analyzeCreature: "Compendium.dc20rpg.system-items.Item.5aV1c024MqxOEJFp",
  calmAnimal: "Compendium.dc20rpg.system-items.Item.d3qUXdLMHegA8yID",
  combatInsight: "Compendium.dc20rpg.system-items.Item.g41WHLdM8uNaSCWG",
  conceal: "Compendium.dc20rpg.system-items.Item.dcpEpJZlvD56Kz8E",
  feint: "Compendium.dc20rpg.system-items.Item.BzJl9QYjAprURubF",
  intimidate: "Compendium.dc20rpg.system-items.Item.Ma2kZ3i6ansJoJOC",
  investigate: "Compendium.dc20rpg.system-items.Item.0Rk0wIUIa49m1gx7",
  jump: "Compendium.dc20rpg.system-items.Item.Z9UxQK1yb6Ht05Xr",
  medicine: "Compendium.dc20rpg.system-items.Item.ePDjVwzFEldfVy3z",
  mountedDefence: "Compendium.dc20rpg.system-items.Item.1KXLpI788cdgbe4O",
  passThrough: "Compendium.dc20rpg.system-items.Item.9KgyzSQbC5xbOwZ4",
  search: "Compendium.dc20rpg.system-items.Item.ZLnCG2WI5G58tEW0",
  attackOfOpportunity: "Compendium.dc20rpg.system-items.Item.1OVlkg9k0CcbBXYj",
  spellDuel: "Compendium.dc20rpg.system-items.Item.fzPWHzvBu1EWJ7Fr",
};

DC20RPG.SYSTEM_CONSTANTS.JOURNAL_UUID.unarmedStrike = "Compendium.dc20rpg.system-items.Item.7wavDCvKyFj2HDV4";

DC20RPG.SYSTEM_CONSTANTS.JOURNAL_UUID.conditionsJournal = {
  bleeding: "Compendium.dc20rpg.rules.JournalEntry.x06moaa9pWzbdrxB.JournalEntryPage.8bb508660e223820",
  blinded: "Compendium.dc20rpg.rules.JournalEntry.x06moaa9pWzbdrxB.JournalEntryPage.3d20d56dae98e774",
  burning: "Compendium.dc20rpg.rules.JournalEntry.x06moaa9pWzbdrxB.JournalEntryPage.4a7c7ed21c99f0d5",
  charmed: "Compendium.dc20rpg.rules.JournalEntry.x06moaa9pWzbdrxB.JournalEntryPage.0f27a9c67ee55f2c",
  dazed: "Compendium.dc20rpg.rules.JournalEntry.x06moaa9pWzbdrxB.JournalEntryPage.6f64926856e0375b",
  disoriented: "Compendium.dc20rpg.rules.JournalEntry.x06moaa9pWzbdrxB.JournalEntryPage.6f64926856e0375b",
  deafened: "Compendium.dc20rpg.rules.JournalEntry.x06moaa9pWzbdrxB.JournalEntryPage.5fa74b85758bd263",
  doomed: "Compendium.dc20rpg.rules.JournalEntry.x06moaa9pWzbdrxB.JournalEntryPage.89c5df57ec2d8d0e",
  exhaustion: "Compendium.dc20rpg.rules.JournalEntry.x06moaa9pWzbdrxB.JournalEntryPage.5c530bfaa0e69dbb",
  exposed: "Compendium.dc20rpg.rules.JournalEntry.x06moaa9pWzbdrxB.JournalEntryPage.73d26b54fce004a0",
  frightened: "Compendium.dc20rpg.rules.JournalEntry.x06moaa9pWzbdrxB.JournalEntryPage.f2d19e12af30f93d",
  grappled: "Compendium.dc20rpg.rules.JournalEntry.HNPA8Fd7ynirYUBq.JournalEntryPage.TfenWPpkGi8scnt2",
  immobilized: "Compendium.dc20rpg.rules.JournalEntry.x06moaa9pWzbdrxB.JournalEntryPage.X89JSxkV4yuhRxKk",
  hindered: "Compendium.dc20rpg.rules.JournalEntry.x06moaa9pWzbdrxB.JournalEntryPage.3be8114c415718d2",
  impaired: "Compendium.dc20rpg.rules.JournalEntry.x06moaa9pWzbdrxB.JournalEntryPage.610e6b3221204f0a",
  weakened: "Compendium.dc20rpg.rules.JournalEntry.x06moaa9pWzbdrxB.JournalEntryPage.610e6b3221204f0a",
  incapacitated: "Compendium.dc20rpg.rules.JournalEntry.x06moaa9pWzbdrxB.JournalEntryPage.e49439b5f79839d3",
  intimidated: "Compendium.dc20rpg.rules.JournalEntry.x06moaa9pWzbdrxB.JournalEntryPage.25a5c54b07df5a3f",
  invisible: "Compendium.dc20rpg.rules.JournalEntry.x06moaa9pWzbdrxB.JournalEntryPage.2226eef8173ad6f0",
  paralyzed: "Compendium.dc20rpg.rules.JournalEntry.x06moaa9pWzbdrxB.JournalEntryPage.229ca0b7af175638",
  petrified: "Compendium.dc20rpg.rules.JournalEntry.x06moaa9pWzbdrxB.JournalEntryPage.60baaa6572e920ef",
  poisoned: "Compendium.dc20rpg.rules.JournalEntry.x06moaa9pWzbdrxB.JournalEntryPage.d91b084c66aa513d",
  prone: "Compendium.dc20rpg.rules.JournalEntry.HNPA8Fd7ynirYUBq.JournalEntryPage.0wgHQIXjhxgu9i0s",
  tethered: "Compendium.dc20rpg.rules.JournalEntry.x06moaa9pWzbdrxB.JournalEntryPage.w9QjKl0BzncI1DRv",
  terrified: "Compendium.dc20rpg.rules.JournalEntry.x06moaa9pWzbdrxB.JournalEntryPage.UkBt66vXWP3eyEOj",
  rattled: "Compendium.dc20rpg.rules.JournalEntry.x06moaa9pWzbdrxB.JournalEntryPage.945222f698836210",
  restrained: "Compendium.dc20rpg.rules.JournalEntry.x06moaa9pWzbdrxB.JournalEntryPage.73e24193c57aeb8a",
  slowed: "Compendium.dc20rpg.rules.JournalEntry.x06moaa9pWzbdrxB.JournalEntryPage.2370179bf647d65e",
  stunned: "Compendium.dc20rpg.rules.JournalEntry.x06moaa9pWzbdrxB.JournalEntryPage.2939ce776edfd6fa",
  fullyStunned: "Compendium.dc20rpg.rules.JournalEntry.x06moaa9pWzbdrxB.JournalEntryPage.2939ce776edfd6fa",
  surprised: "Compendium.dc20rpg.rules.JournalEntry.x06moaa9pWzbdrxB.JournalEntryPage.9b127a80c6770c71",
  taunted: "Compendium.dc20rpg.rules.JournalEntry.x06moaa9pWzbdrxB.JournalEntryPage.5b8703add31783de",
  unconscious: "Compendium.dc20rpg.rules.JournalEntry.x06moaa9pWzbdrxB.JournalEntryPage.e4b6147dfec70860",
  bloodied: "Compendium.dc20rpg.rules.JournalEntry.amGWJPNztuALU8Fw.JournalEntryPage.cb4fe4f4f35d6275",
  wellBloodied: "Compendium.dc20rpg.rules.JournalEntry.amGWJPNztuALU8Fw.JournalEntryPage.cb4fe4f4f35d6275",
  deathsDoor: "Compendium.dc20rpg.rules.JournalEntry.amGWJPNztuALU8Fw.JournalEntryPage.000a46e5db7cb982",
  partiallyConcealed: "Compendium.dc20rpg.rules.JournalEntry.UgSNzjIdhqUjQ9Yo.JournalEntryPage.da1f84c1f010eae2",
  fullyConcealed: "Compendium.dc20rpg.rules.JournalEntry.UgSNzjIdhqUjQ9Yo.JournalEntryPage.da1f84c1f010eae2",
};

DC20RPG.SYSTEM_CONSTANTS.JOURNAL_UUID.propertiesJournal = {
  ammo: "Compendium.dc20rpg.rules.JournalEntry.51wyjg5pkl8Vmh8e.JournalEntryPage.IpmJVCqJnQzf0PEh",
  concealable: "Compendium.dc20rpg.rules.JournalEntry.51wyjg5pkl8Vmh8e.JournalEntryPage.cRUMPH5Vkc4eZ26J",
  reach: "Compendium.dc20rpg.rules.JournalEntry.51wyjg5pkl8Vmh8e.JournalEntryPage.IRVvREIp7pesOtkB",
  reload: "Compendium.dc20rpg.rules.JournalEntry.51wyjg5pkl8Vmh8e.JournalEntryPage.1oVYxj3fsucBTFqv",
  thrown: "Compendium.dc20rpg.rules.JournalEntry.51wyjg5pkl8Vmh8e.JournalEntryPage.pMPVir3MnB8E5fNK",
  twoHanded: "Compendium.dc20rpg.rules.JournalEntry.51wyjg5pkl8Vmh8e.JournalEntryPage.yTWxAF1ijfAmOPFy",
  versatile: "Compendium.dc20rpg.rules.JournalEntry.51wyjg5pkl8Vmh8e.JournalEntryPage.6qKLjDuj2yFzrich",
  impact: "Compendium.dc20rpg.rules.JournalEntry.51wyjg5pkl8Vmh8e.JournalEntryPage.eRclHKhWpouQHVIY",
  unwieldy: "Compendium.dc20rpg.rules.JournalEntry.51wyjg5pkl8Vmh8e.JournalEntryPage.vRcRgNKeLkMSjO4w",
  silent: "Compendium.dc20rpg.rules.JournalEntry.51wyjg5pkl8Vmh8e.JournalEntryPage.AX0JXkpLErDw9ONa",
  toss: "Compendium.dc20rpg.rules.JournalEntry.51wyjg5pkl8Vmh8e.JournalEntryPage.iTsd5sG8SiaYCOA6",
  returning: "Compendium.dc20rpg.rules.JournalEntry.51wyjg5pkl8Vmh8e.JournalEntryPage.1NPnFMz7rkb33Cog",
  capture: "Compendium.dc20rpg.rules.JournalEntry.51wyjg5pkl8Vmh8e.JournalEntryPage.si6CLG1mtdRSJgdV",
  multiFaceted: "Compendium.dc20rpg.rules.JournalEntry.51wyjg5pkl8Vmh8e.JournalEntryPage.d5boT5j6ZPsWscm6",
  guard: "Compendium.dc20rpg.rules.JournalEntry.51wyjg5pkl8Vmh8e.JournalEntryPage.FKrFwwAOH2Ff5JKe",
  heavy: "Compendium.dc20rpg.rules.JournalEntry.51wyjg5pkl8Vmh8e.JournalEntryPage.uuQtegS7r4BqkRWY",
  longRanged: "Compendium.dc20rpg.rules.JournalEntry.51wyjg5pkl8Vmh8e.JournalEntryPage.Lyx8rDSHTUqmdupW",
  reinforced: "Compendium.dc20rpg.rules.JournalEntry.qLqSJ8hpW0yvogRt.JournalEntryPage.dXjyS6YvhubjwjSn",
  sturdy: "Compendium.dc20rpg.rules.JournalEntry.qLqSJ8hpW0yvogRt.JournalEntryPage.NyofkTW2dROcxpT5",
  dense: "Compendium.dc20rpg.rules.JournalEntry.qLqSJ8hpW0yvogRt.JournalEntryPage.lJqNgXRpjo8tlZ9B",
  thrownS: "Compendium.dc20rpg.rules.JournalEntry.qLqSJ8hpW0yvogRt.JournalEntryPage.SGMZI8qMi3C9CYc6",
  mounted: "Compendium.dc20rpg.rules.JournalEntry.qLqSJ8hpW0yvogRt.JournalEntryPage.wJIrMxAQeTnZejZk",
  attunement: "",
  adIncrease: "Compendium.dc20rpg.rules.JournalEntry.qLqSJ8hpW0yvogRt.JournalEntryPage.sL7FFcPq9tZMnsQp",
  pdIncrease: "Compendium.dc20rpg.rules.JournalEntry.qLqSJ8hpW0yvogRt.JournalEntryPage.hoFd7xUj99sFJhkf",
  edr: "Compendium.dc20rpg.rules.JournalEntry.qLqSJ8hpW0yvogRt.JournalEntryPage.wJIrMxAQeTnZejZk",
  pdr: "Compendium.dc20rpg.rules.JournalEntry.qLqSJ8hpW0yvogRt.JournalEntryPage.LR1XjGbhGGaJamtB",
  bulky: "Compendium.dc20rpg.rules.JournalEntry.qLqSJ8hpW0yvogRt.JournalEntryPage.P5hNhnMIhbqVtTeR",
  rigid: "Compendium.dc20rpg.rules.JournalEntry.qLqSJ8hpW0yvogRt.JournalEntryPage.MB8nIR0MU7A9fPmo",
  grasp: "Compendium.dc20rpg.rules.JournalEntry.qLqSJ8hpW0yvogRt.JournalEntryPage.i0kF5bqDBVrU4byE",
  mounted: "Compendium.dc20rpg.rules.JournalEntry.qLqSJ8hpW0yvogRt.JournalEntryPage.D4tbxGmWGbShvtYp",
};

DC20RPG.SYSTEM_CONSTANTS.JOURNAL_UUID.advancementToolitps = {
  martial: "Compendium.dc20rpg.rules.JournalEntry.7TW9dtmP9JvKJ1rq.JournalEntryPage.0964d3cdbf002a1f",
  spellcaster: "Compendium.dc20rpg.rules.JournalEntry.7TW9dtmP9JvKJ1rq.JournalEntryPage.60d02227dff91b93",
  basic: "Compendium.dc20rpg.rules.JournalEntry.7TW9dtmP9JvKJ1rq.JournalEntryPage.3110b5966d24d4c0",
  adept: "Compendium.dc20rpg.rules.JournalEntry.7TW9dtmP9JvKJ1rq.JournalEntryPage.9125c9f4869ec1c6",
  expert: "Compendium.dc20rpg.rules.JournalEntry.7TW9dtmP9JvKJ1rq.JournalEntryPage.99d9c79cd150de60",
  master: "Compendium.dc20rpg.rules.JournalEntry.7TW9dtmP9JvKJ1rq.JournalEntryPage.2665cabf5cd235fa",
  grandmaster: "Compendium.dc20rpg.rules.JournalEntry.7TW9dtmP9JvKJ1rq.JournalEntryPage.e4e18eee9d6f8ac2",
  legendary: "Compendium.dc20rpg.rules.JournalEntry.7TW9dtmP9JvKJ1rq.JournalEntryPage.8f1ea8b9912b1fe3"
};

DC20RPG.SYSTEM_CONSTANTS.JOURNAL_UUID.deathsDoor = "Compendium.dc20rpg.rules.JournalEntry.amGWJPNztuALU8Fw.JournalEntryPage.000a46e5db7cb982";

/**
 * Registers additional Handlebars Helpers to be used in templates later.
 * @return {void}
 */
function registerHandlebarsHelpers() {

  Handlebars.registerHelper('add', function (obj1, obj2) {
    return obj1 + obj2;
  });

  Handlebars.registerHelper('ifCond', function (v1, operator, v2, options) {
    switch (operator) {
      case '==':
        return (v1 == v2) ? options.fn(this) : options.inverse(this);
      case '===':
        return (v1 === v2) ? options.fn(this) : options.inverse(this);
      case '!=':
        return (v1 != v2) ? options.fn(this) : options.inverse(this);
      case '!==':
        return (v1 !== v2) ? options.fn(this) : options.inverse(this);
      case '<':
        return (v1 < v2) ? options.fn(this) : options.inverse(this);
      case '<=':
        return (v1 <= v2) ? options.fn(this) : options.inverse(this);
      case '>':
        return (v1 > v2) ? options.fn(this) : options.inverse(this);
      case '>=':
        return (v1 >= v2) ? options.fn(this) : options.inverse(this);
      case '&&':
        return (v1 && v2) ? options.fn(this) : options.inverse(this);
      case '||':
        return (v1 || v2) ? options.fn(this) : options.inverse(this);
      case '%':
        return (v1 % v2 === 0) ? options.fn(this) : options.inverse(this);
      default:
        return options.inverse(this);
    }
  });

  Handlebars.registerHelper('withConditional', function(condition, valueIfTrue, valueIfFalse, options) {
    return options.fn(condition ? valueIfTrue : valueIfFalse);
  });

  Handlebars.registerHelper('includes', function(string, toInclude) {
    return string.includes(toInclude);
  });

  Handlebars.registerHelper('costPrinter', function (cost, costIcon, mergeAmount, hasValueForZero, zeroIcon) {
    const costIconHtml = `<i class="${costIcon} cost-icon"></i>`;
    const zeroIconHtml = `<i class="${zeroIcon} cost-icon"></i>`;

    if (cost === undefined) return '';
    if (cost === 0 && hasValueForZero) return zeroIconHtml;
    if (cost === 0) return '';
     
    if (mergeAmount > 6 && cost > 1) return `<b>${cost}x</b>&nbsp${costIconHtml}`;

    let pointsPrinter = "";
    for (let i = 1; i <= cost; i ++) {
      pointsPrinter += costIconHtml;
    }
    return pointsPrinter;
  });

  Handlebars.registerHelper('costPrinterIcons', function (cost, iconPath, mergeAmount, hasValueForZero, zeroIconPath) {
    const costImg = `<img src=${iconPath} class="cost-img">`;
    const zeroImg = `<img src=${zeroIconPath} class="cost-img">`;

    if (cost === undefined) return '';
    if (cost === 0 && hasValueForZero) return zeroImg;
    if (cost === 0) return '';

    if (mergeAmount > 6) return `<b>${cost}x</b>&nbsp${costImg}`;
     
    let pointsPrinter = "";
    for (let i = 1; i <= cost; i ++) {
      pointsPrinter += costImg;
    }
    return pointsPrinter;
  });

  Handlebars.registerHelper('printGrit', function (current, max) {
    let fullPoint = "<a class='gp fa-solid fa-hand-fist fa-lg'></a>";
    let emptyPoint = "<a class='gp fa-light fa-hand-fist fa-lg'></a>";

    let gritPoints = "";
    for(let i = 0; i < max; i++) {
      if (i < current) gritPoints += fullPoint;
      else gritPoints += emptyPoint;
    }
    return gritPoints;
  });

  Handlebars.registerHelper('printActionPoints', function (current, max) {
    let fullPoint = "<i class='fa-solid fa-dice-d6 fa-2x ap'></i>";
    let emptyPoint = "<i class='fa-light fa-dice-d6 fa-2x ap'></i>";

    if (max >= 5) return `<b>${current}/${max}</b> ${fullPoint}`;
    
    let actionPoints = "";
    for(let i = 0; i < max; i++) {
      if (i < current) actionPoints += fullPoint;
      else actionPoints += emptyPoint;
    }
    return actionPoints;
  });

  Handlebars.registerHelper('arrayIncludes', function(object, options) {
    let arrayString = options.hash.arrayString;
    let array = arrayString.split(' ');

    return array.includes(object);
  });

  Handlebars.registerHelper('labelFromKey', function(key, objectWithLabels) {
    return getLabelFromKey(key, objectWithLabels);
  });

  Handlebars.registerHelper('printDices', function(results, faces) {
    if (!results) return;

    let final = "";
    results.forEach(result => {
      let colored = result.result === faces ? "max" 
                    : result.result === 1 ? "min" 
                    : "";
      final += `<li class="roll die d${faces} ${colored}">${result.result}</li>`;
    });
    return final;
  });

  Handlebars.registerHelper('sumDices', function(results) {
    if (!results) return;

    let diceTotal = 0;
    results.forEach(result => {
      diceTotal += result.result;
    });
    return diceTotal;
  });

  Handlebars.registerHelper('PARTIAL', function(partialName) {
    const partialPath = allPartials()[partialName];

    if (!partialPath) {
      return new Handlebars.SafeString(`Partial "${partialName}" not found`);
    }

    const template = Handlebars.partials[partialPath];
    if (template) {
      return new Handlebars.SafeString(template(this, {
        allowProtoMethodsByDefault: true,
        allowProtoPropertiesByDefault: true
      }));
    }
    return '';
  });

  Handlebars.registerHelper('varLocalize', (firstString, key, secondString) => {
    return game.i18n.localize(`${firstString}${key}${secondString}`);
  });

  Handlebars.registerHelper('repleaceLocalize', (path, ...values) => {
    let localized = game.i18n.localize(path);
    for (let i = 0; i < values.length - 1; i++) {
      localized = localized.replaceAll(`$${i}`, values[i]);
    }
    return localized;
  });
}

function registerDC20Statues() {
  return [
    _bleeding(),
    _blinded(),
    _burning(),
    _charmed(),

    _dazed(),
    _deafened(),
    _disoriented(),
    _doomed(),

    _exhaustion(),
    _exposed(),
    _frightened(),
    _grappled(),

    _hindered(),
    _immobilized(),
    _impaired(),
    _incapacitated(),

    _intimidated(),
    _invisible(),
    _paralyzed(),
    _petrified(),

    _poisoned(),
    _prone(),
    _restrained(),
    _slowed(),

    _stunned(),
    _surprised(), 
    _taunted(),
    _tethered(),

    _terrified(),
    _unconscious(),
    _weakened(),
    
    _bloodied(),
    _wellBloodied(),
    _dead(),
    _deathsDoor(),

    _partiallyConcealed(),
    _fullyConcealed(),
    _fullyStunned(),
  ]
}

//================================
//             EXTRA             =
//================================
function _bloodied() {
  return {
    id: "bloodied",
    name: "Bloodied",
    label: "Bloodied",
    stackable: false,
    system: {
      statusId: "bloodied",
      hide: true
    },
    statuses: [],
    img: "systems/dc20rpg/images/statuses/bloodied.svg",
    description: "<p>You have less than 50% max HP.</p>",
    changes: []
  }
}
function _wellBloodied() {
  return {
    id: "wellBloodied",
    name: "Well-Bloodied",
    label: "Well-Bloodied",
    stackable: false,
    system: {
      statusId: "wellBloodied",
      hide: true
    },
    statuses: [],
    img: "systems/dc20rpg/images/statuses/wellBloodied.svg",
    description: "<p>You have less than 25% max HP.</p>",
    changes: []
  }
}
function _dead() {
  return {
    id: "dead",
    name: "Dead",
    label: "Dead",
    stackable: false,
    system: {
      statusId: "dead",
    },
    statuses: [],
    img: "systems/dc20rpg/images/statuses/dead.svg",
    description: "<p>You are dead.</p>",
    changes: []
  }
}
function _deathsDoor() {
  return {
    id: "deathsDoor",
    name: "Death's Door",
    label: "Death's Door",
    stackable: false,
    system: {
      statusId: "deathsDoor",
      hide: true
    },
    statuses: [],
    img: "systems/dc20rpg/images/statuses/deathsDoor.svg",
    description: `
    <p>Until you are restored to 1 HP or higher, you suffer the following effects:</p>
    <ul>
        <li><em><strong>Action Point Limit:</strong></em> Your current and maximum <strong>AP</strong> are reduced by 3.</li>
    </ul>
    <ul>
        <li><strong>Death Save: </strong>You must make a <strong>DC 10</strong> Death Save at the start of each of your turns (see <strong>Death</strong> <strong>Saves</strong> for more information).</li>
    </ul>
    `,
    changes: [
      {
        key: "system.globalModifier.prevent.goUnderAP",
        mode: 4,
        priority: undefined,
        value: 3
      }
    ]
  }
}
function _partiallyConcealed() {
  return {
    id: "partiallyConcealed",
    name: "Partially Concealed",
    label: "Partially Concealed",
    stackable: false,
    system: {
      statusId: "partiallyConcealed"
    },
    statuses: [],
    img: "systems/dc20rpg/images/statuses/partiallyConcealed.svg",
    description: `
    <p>A creature is Partially Concealed while within an area of thin fog, moderate foliage, or Dim Light. Creatures have DisADV on Awareness Checks made to see things that are Partially Concealed.</p>
    `,
    changes: [
      {
        key: "system.rollLevel.againstYou.skills",
        mode: 2,
        priority: undefined,
        value: '"label": "Partially Concealed", "value": 1, "type": "dis", "skill": "awa"'
      }
    ]
  }
}
function _fullyConcealed() {
  return {
    id: "fullyConcealed",
    name: "Fully Concealed",
    label: "Fully Concealed",
    stackable: false,
    system: {
      statusId: "fullyConcealed"
    },
    statuses: [],
    img: "systems/dc20rpg/images/statuses/fullyConcealed.svg",
    description: `
    <p>A creature is Fully Concealed while in an area that blocks vision entirely, such as Darkness, thick fog, or dense foliage. Creatures are considered <strong>Blinded </strong>for the purposes of seeing things that are Fully Concealed.</p>
    `,
    changes: [
      {
        key: "system.rollLevel.againstYou.skills",
        mode: 2,
        priority: undefined,
        value: '"label": "Fully Concealed", "autoFail": true, "skill": "awa"'
      },
      {
        key: "system.rollLevel.againstYou.martial.melee",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Fully Concealed (Enemy cant see you)", "confirmation": true'
      },
      {
        key: "system.rollLevel.againstYou.martial.ranged",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Fully Concealed (Enemy cant see you)", "confirmation": true'
      },
      {
        key: "system.rollLevel.againstYou.spell.melee",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Fully Concealed (Enemy cant see you)", "confirmation": true'
      },
      {
        key: "system.rollLevel.againstYou.spell.ranged",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Fully Concealed (Enemy cant see you)", "confirmation": true'
      },
      {
        key: "system.rollLevel.onYou.martial.melee",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "adv", "label": "Fully Concealed (Enemy cant see you)", "confirmation": true'
      },
      {
        key: "system.rollLevel.onYou.martial.ranged",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "adv", "label": "Fully Concealed (Enemy cant see you)", "confirmation": true'
      },
      {
        key: "system.rollLevel.onYou.spell.melee",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "adv", "label": "Fully Concealed (Enemy cant see you)", "confirmation": true'
      },
      {
        key: "system.rollLevel.onYou.spell.ranged",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "adv", "label": "Fully Concealed (Enemy cant see you)", "confirmation": true'
      }
    ]
  }
}
function _fullyStunned() {
  return {
    id: "fullyStunned",
    _id: "d02g03day8pp8en6",
    name: "Fully Stunned",
    label: "Fully Stunned",
    stackable: false,
    statuses: ["incapacitated"],
    system: {
      statusId: "fullyStunned",
      hide: true
    },
    img: "systems/dc20rpg/images/statuses/fullyStunned.svg",
    description: `
    <p>You are subjected to the following effects:</p>
    <ul>
        <li>
            <p>You're <strong>Incapacitated</strong>.</p>
        </li>
        <li>
            <p><span>Attacks against you have ADV.</span></p>
        </li>
        <li>
            <p>You automatically fail Physical Saves (except against Poisons and Diseases).</p>
        </li>
    </ul>
    `,
    changes: [
      {
        key: "system.globalModifier.prevent.goUnderAP",
        mode: 4,
        priority: undefined,
        value: 99
      },
      {
        key: "system.movement.ground.bonus",
        mode: 2,
        priority: undefined,
        value: -100
      },
      {
        key: "system.rollLevel.againstYou.martial.melee",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "adv", "label": "Fully Stunned"'
      },
      {
        key: "system.rollLevel.againstYou.martial.ranged",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "adv", "label": "Fully Stunned"'
      },
      {
        key: "system.rollLevel.againstYou.spell.melee",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "adv", "label": "Fully Stunned"'
      },
      {
        key: "system.rollLevel.againstYou.spell.ranged",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "adv", "label": "Fully Stunned"'
      },
      {
        key: "system.rollLevel.onYou.saves.mig",
        mode: 5,
        priority: undefined,
        value: '"label": "Fully Stunned", "autoFail": true, "confirmation": true'
      },
      {
        key: "system.rollLevel.onYou.saves.agi",
        mode: 5,
        priority: undefined,
        value: '"label": "Fully Stunned", "autoFail": true, "confirmation": true'
      }
    ]
  }
}

//================================
//           STACKING            =
//================================
function _bleeding() {
  return {
    id: "bleeding",
    name: "Bleeding",
    label: "Bleeding",
    stackable: true,
    statuses: [],
    system: {
      statusId: "bleeding"
    },
    img: "systems/dc20rpg/images/statuses/bleeding.svg",
    description: `
    <p>You take <strong>X</strong> True damage at the start of each of your turns.</p>
    <p><em><strong>Ending Bleeding:</strong></em> All stacks of the Condition end when you're subjected to an effect that restores your HP. Alternatively, a creature can attempt to remove 1 or more stacks of the Condition by taking the Medicine Action.</p>
    <p></p>
    <h3>Medicine (Action)</h3>
    <p>You can spend <strong>1 AP</strong> to touch a creature and tend to its wounds. Make a <strong>DC 10</strong> Medicine Check. <strong>Success (each 5):</strong> You end 1 stack of <strong>Bleeding</strong> on the target.</p>
    `,
    changes: [
      {
        key: "system.events",
        mode: 2,
        priority: undefined,
        value: '"eventType": "damage", "label": "Bleeding", "trigger": "turnStart", "value": 1, "type": "true"'
      },
      {
        key: "system.events",
        mode: 2,
        priority: undefined,
        value: '"eventType": "basic", "trigger": "healingTaken", "label": "Bleeding - Healed", "postTrigger": "delete"'
      }
    ]
  }
}
function _burning() {
  return {
    id: "burning",
    name: "Burning",
    label: "Burning",
    stackable: true,
    statuses: [],
    system: {
      statusId: "burning"
    },
    img: "systems/dc20rpg/images/statuses/burning.svg",
    description: `
    <p>You take <strong>X</strong> Fire damage at the start of each of your turns.</p>
    <p><em><strong>Ending Burning:</strong></em> All stacks of the Condition end when you're doused by at least 1 gallon (4 liters) of water or fully immersed in water. Alternatively, a creature within 1 Space can spend <strong>1 AP</strong> to remove 1 stack of the Condition.</p>
    `,
    changes: [
      {
        key: "system.events",
        mode: 2,
        priority: undefined,
        value: '"eventType": "damage", "label": "Burning", "trigger": "turnStart", "value": 1, "type": "fire"'
      },
    ]
  }
}
function _doomed() {
  return {
    id: "doomed",
    name: "Doomed",
    label: "Doomed",
    stackable: true,
    statuses: [],
    system: {
      statusId: "doomed"
    },
    img: "systems/dc20rpg/images/statuses/doomed.svg",
    description: `
    <p>You are subjected to the following effects:</p>
    <ul>
        <li>
            <p>Your current and maximum HP is reduced by the value of <strong>X</strong>.</p>
        </li>
        <li>
            <p>When an effect restores your HP, you regain <strong>X</strong> less HP than normal.</p>
        </li>
    </ul>
    `,
    changes: [
      {
        key: "system.resources.health.bonus",
        mode: 2,
        priority: undefined,
        value: -1
      },
      {
        key: "system.healingReduction.flat",
        mode: 2,
        priority: undefined,
        value: 1
      },
    ]
  }
}
function _exhaustion() {
  return {
    id: "exhaustion",
    name: "Exhaustion",
    label: "Exhaustion",
    stackable: true,
    statuses: [],
    system: {
      statusId: "exhaustion"
    },
    img: "systems/dc20rpg/images/statuses/exhaustion.svg",
    description: `
    <p>You are subjected to the following effects:</p>
    <ul>
        <li>
            <p>You gain a penalty equal to <strong>X</strong> on all Checks and Saves you make.</p>
        </li>
        <li>
            <p>Your Speed and Save DC is reduced by <strong>X</strong>.</p>
        </li>
    </ul>
    <p><strong>Death:</strong> You immediately die if you reach 6 stacks of <strong>Exhaustion</strong>.</p>
    <p id="1cf1b90a-e081-810b-86ea-fe594fcbeafa" class="block-color-blue_background"><strong>Example:</strong> If you have <strong>Exhaustion 3</strong>, then you would have a <strong>-3</strong> penalty on Checks and Saves, your Speed would be reduced by 3 Spaces, and your Save DC would be reduced by 3.</p>
    `,
    changes: []
  }
}
function _exposed() {
  return {
    id: "exposed",
    name: "Exposed",
    label: "Exposed",
    stackable: true,
    statuses: [],
    system: {
      statusId: "exposed"
    },
    img: "systems/dc20rpg/images/statuses/exposed.svg",
    description: "<p>Attacks against you have ADV <strong>X</strong>.</p>",
    changes: [
      {
        key: "system.rollLevel.againstYou.martial.melee",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "adv", "label": "Exposed"'
      },
      {
        key: "system.rollLevel.againstYou.martial.ranged",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "adv", "label": "Exposed"'
      },
      {
        key: "system.rollLevel.againstYou.spell.melee",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "adv", "label": "Exposed"'
      },
      {
        key: "system.rollLevel.againstYou.spell.ranged",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "adv", "label": "Exposed"'
      },
    ]
  }
}
function _hindered() {
  return {
    id: "hindered",
    name: "Hindered",
    label: "Hindered",
    stackable: true,
    statuses: [],
    system: {
      statusId: "hindered"
    },
    img: "systems/dc20rpg/images/statuses/hindered.svg",
    description: "<p>You have DisADV <strong>X</strong> on Attacks.</p>",
    changes: [
      {
        key: "system.rollLevel.onYou.martial.melee",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Hindered"'
      },
      {
        key: "system.rollLevel.onYou.martial.ranged",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Hindered"'
      },
      {
        key: "system.rollLevel.onYou.spell.melee",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Hindered"'
      },
      {
        key: "system.rollLevel.onYou.spell.ranged",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Hindered"'
      },
      {
        key: "system.rollLevel.onYou.checks.att",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Hindered"'
      },
      {
        key: "system.rollLevel.onYou.checks.spe",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Hindered"'
      },
    ]
  }
}
function _dazed() {
  return {
    id: "dazed",
    name: "Dazed",
    label: "Dazed",
    stackable: true,
    statuses: [],
    system: {
      statusId: "dazed"
    },
    img: "systems/dc20rpg/images/statuses/dazed.svg",
    description: "<p>You have DisADV <strong>X</strong> on Mental Checks.</p>",
    changes: [
      {
        key: "system.rollLevel.onYou.checks.int",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Dazed"'
      },
      {
        key: "system.rollLevel.onYou.checks.cha",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Dazed"'
      },
      {
        key: "system.rollLevel.onYou.checks.spe",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Dazed"'
      },
      {
        key: "system.rollLevel.onYou.spell.melee",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Dazed"'
      },
      {
        key: "system.rollLevel.onYou.spell.ranged",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Dazed"'
      },
    ]
  }
}
function _disoriented() {
  return {
    id: "disoriented",
    name: "Disoriented",
    label: "Disoriented",
    stackable: true,
    statuses: [],
    system: {
      statusId: "disoriented"
    },
    img: "systems/dc20rpg/images/statuses/disoriented.svg",
    description: "<p>You have DisADV X on <b>Mental Saves</b>.</p>",
    changes: [
      {
        key: "system.rollLevel.onYou.saves.int",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Disoriented"'
      },
      {
        key: "system.rollLevel.onYou.saves.cha",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Disoriented"'
      },
    ]
  }
}
function _impaired() {
  return {
    id: "impaired",
    name: "Impaired",
    label: "Impaired",
    stackable: true,
    system: {
      statusId: "impaired"
    },
    statuses: [],
    img: "systems/dc20rpg/images/statuses/impaired.svg",
    description: "<p>You have DisADV <strong>X</strong> on Physical Checks.</p>",
    changes: [
      {
        key: "system.rollLevel.onYou.checks.mig",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Impaired"'
      },
      {
        key: "system.rollLevel.onYou.checks.agi",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Impaired"'
      },
      {
        key: "system.rollLevel.onYou.checks.att",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Impaired"'
      },
      {
        key: "system.rollLevel.onYou.martial.melee",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Impaired"'
      },
      {
        key: "system.rollLevel.onYou.martial.ranged",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Impaired"'
      },
    ]
  }
}
function _weakened() {
  return {
    id: "weakened",
    name: "Weakened",
    label: "Weakened",
    stackable: true,
    statuses: [],
    system: {
      statusId: "weakened"
    },
    img: "systems/dc20rpg/images/statuses/weakened.svg",
    description: "<p>You have DisADV <strong>X</strong> on Physical Saves.</p>",
    changes: [
      {
        key: "system.rollLevel.onYou.saves.mig",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Weakened"'
      },
      {
        key: "system.rollLevel.onYou.saves.agi",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Weakened"'
      },
    ]
  }
}
function _slowed() {
  return {
    id: "slowed",
    name: "Slowed",
    label: "Slowed",
    stackable: true,
    statuses: [],
    system: {
      statusId: "slowed"
    },
    img: "systems/dc20rpg/images/statuses/slowed.svg",
    description: "<p>Every 1 Space you move costs an extra <strong>X</strong> Spaces of movement.</p>",
    changes: [
      {
        key: "system.moveCost",
        mode: 2,
        priority: undefined,
        value: 1
      },
    ]
  }
}
function _stunned() {
  return {
    id: "stunned",
    name: "Stunned",
    label: "Stunned",
    stackable: true,
    statuses: [],
    system: {
      statusId: "stunned"
    },
    img: "systems/dc20rpg/images/statuses/stunned.svg",
    description: `
    <p>Your current and maximum <strong>AP</strong> is reduced by <strong>X</strong>.</p>
    <p>While you're Stunned 4 or higher, you are subjected to the following effects:</p>
    <ul>
        <li>
            <p>You're <strong>Incapacitated</strong>.</p>
        </li>
        <li>
            <p><span>Attacks against you have ADV.</span></p>
        </li>
        <li>
            <p>You automatically fail Physical Saves (except against Poisons and Diseases).</p>
        </li>
    </ul>
    `,
    changes: [
      {
        key: "system.globalModifier.prevent.goUnderAP",
        mode: 2,
        priority: undefined,
        value: 1
      }
    ]
  }
}

//================================
//          OVERLAPPING          =
//================================
function _charmed() {
  return {
    id: "charmed",
    name: "Charmed",
    label: "Charmed",
    stackable: false,
    statuses: [],
    system: {
      statusId: "charmed",
    },
    img: "systems/dc20rpg/images/statuses/charmed.svg",
    description: `
    <p>You are subjected to the following effects:</p>
    <ul>
        <li>
            <p>Your Charmer has ADV on Charisma Checks made against you.</p>
        </li>
        <li>
            <p>You can't target your Charmer with harmful Attacks or effects.</p>
        </li>
    </ul>
    `,
    changes: [
      {
        key: "system.rollLevel.againstYou.checks.cha",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "adv", "label": "You are the Charmer", "confirmation": true'
      }
    ]
  }
}
function _immobilized() {
  return {
    id: "immobilized",
    name: "Immobilized",
    label: "Immobilized",
    stackable: false,
    statuses: [],
    system: {
      statusId: "immobilized",
    },
    img: "systems/dc20rpg/images/statuses/immobilized.svg",
    description: "<p>You can't move and you have <strong>DisADV</strong> on Agility Saves.</p>",
    changes: [
      {
        key: "system.movement.ground.bonus",
        mode: 2,
        priority: undefined,
        value: -100
      },
      {
        key: "system.rollLevel.onYou.saves.agi",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Immobilized"'
      },
    ]
  }
}
function _intimidated() {
  return {
    id: "intimidated",
    name: "Intimidated",
    label: "Intimidated",
    stackable: false,
    statuses: [],
    system: {
      statusId: "intimidated"
    },
    img: "systems/dc20rpg/images/statuses/intimidated.svg",
    description: "<p>You have <strong>DisADV</strong> on all Checks made against the source.</p>",
    changes: [
      {
        key: "system.rollLevel.onYou.checks.mig",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Intimidated", "confirmation": true'
      },
      {
        key: "system.rollLevel.onYou.checks.agi",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Intimidated", "confirmation": true'
      },
      {
        key: "system.rollLevel.onYou.checks.int",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Intimidated", "confirmation": true'
      },
      {
        key: "system.rollLevel.onYou.checks.cha",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Intimidated", "confirmation": true'
      },
      {
        key: "system.rollLevel.onYou.checks.att",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Intimidated", "confirmation": true'
      },
      {
        key: "system.rollLevel.onYou.checks.spe",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Intimidated", "confirmation": true'
      },
      {
        key: "system.rollLevel.onYou.martial.melee",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Intimidated", "confirmation": true'
      },
      {
        key: "system.rollLevel.onYou.martial.ranged",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Intimidated", "confirmation": true'
      },
      {
        key: "system.rollLevel.onYou.spell.melee",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Intimidated", "confirmation": true'
      },
      {
        key: "system.rollLevel.onYou.spell.ranged",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Intimidated", "confirmation": true'
      },
    ]
  }
}
function _frightened() {
  return {
    id: "frightened",
    _id: "7j9cumkgsq6l264c",
    name: "Frightened",
    label: "Frightened",
    stackable: false,
    statuses: [],
    system: {
      statusId: "frightened"
    },
    img: "systems/dc20rpg/images/statuses/frightened.svg",
    description: `
    <p>You are subjected to the following effects:</p>
    <ul>
        <li>
            <p>You can't willingly move closer to the source.</p>
        </li>
        <li>
            <p>You have <strong>DisADV</strong> on all Checks made against the source.</p>
        </li>
    </ul>
    `,
    changes: [
      {
        key: "system.rollLevel.onYou.checks.mig",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Frightened", "confirmation": true'
      },
      {
        key: "system.rollLevel.onYou.checks.agi",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Frightened", "confirmation": true'
      },
      {
        key: "system.rollLevel.onYou.checks.int",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Frightened", "confirmation": true'
      },
      {
        key: "system.rollLevel.onYou.checks.cha",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Frightened", "confirmation": true'
      },
      {
        key: "system.rollLevel.onYou.checks.att",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Frightened", "confirmation": true'
      },
      {
        key: "system.rollLevel.onYou.checks.spe",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Frightened", "confirmation": true'
      },
      {
        key: "system.rollLevel.onYou.martial.melee",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Frightened", "confirmation": true'
      },
      {
        key: "system.rollLevel.onYou.martial.ranged",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Frightened", "confirmation": true'
      },
      {
        key: "system.rollLevel.onYou.spell.melee",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Frightened", "confirmation": true'
      },
      {
        key: "system.rollLevel.onYou.spell.ranged",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Frightened", "confirmation": true'
      },
    ]
  }
}
function _restrained() {
  return {
    id: "restrained",
    _id: "eo34ahey9m0d92ou",
    name: "Restrained",
    label: "Restrained",
    stackable: false,
    statuses: ["immobilized", "grappled"],
    system: {
      statusId: "restrained"
    },
    img: "systems/dc20rpg/images/statuses/restrained.svg",
    description: `
    <p>You are subjected to the following effects:</p>
    <ul>
        <li>
            <p>You're <strong>Immobilized</strong>.</p>
        </li>
        <li>
            <p>Your Attacks have DisADV.</p>
        </li>
        <li>
            <p>Attacks against you have ADV.</p>
        </li>
    </ul>
    `,
    changes: [
      {
        key: "system.movement.ground.bonus",
        mode: 2,
        priority: undefined,
        value: -100
      },
      {
        key: "system.rollLevel.onYou.saves.agi",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Restrained (Immobilized)"'
      },
      {
        key: "system.rollLevel.onYou.martial.melee",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Restrained"'
      },
      {
        key: "system.rollLevel.onYou.martial.ranged",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Restrained"'
      },
      {
        key: "system.rollLevel.onYou.spell.melee",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Restrained"'
      },
      {
        key: "system.rollLevel.onYou.spell.ranged",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Restrained"'
      },
      {
        key: "system.rollLevel.againstYou.martial.melee",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "adv", "label": "Restrained"'
      },
      {
        key: "system.rollLevel.againstYou.martial.ranged",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "adv", "label": "Restrained"'
      },
      {
        key: "system.rollLevel.againstYou.spell.melee",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "adv", "label": "Restrained"'
      },
      {
        key: "system.rollLevel.againstYou.spell.ranged",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "adv", "label": "Restrained"'
      },
    ]
  }
}
function _taunted() {
  return {
    id: "taunted",
    name: "Taunted",
    label: "Taunted",
    stackable: false,
    statuses: [],
    system: {
      statusId: "taunted"
    },
    img: "systems/dc20rpg/images/statuses/taunted.svg",
    description: "<p>You have DisADV on Attacks against targets other than the source.</p>",
    changes: [
      {
        key: "system.rollLevel.onYou.martial.melee",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Your Target is not your Taunt source", "confirmation": true'
      },
      {
        key: "system.rollLevel.onYou.martial.ranged",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Your Target is not your Taunt source", "confirmation": true'
      },
      {
        key: "system.rollLevel.onYou.spell.melee",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Your Target is not your Taunt source", "confirmation": true'
      },
      {
        key: "system.rollLevel.onYou.spell.ranged",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Your Target is not your Taunt source", "confirmation": true'
      }
    ]
  }
}
function _tethered() {
  return {
    id: "tethered",
    name: "Tethered",
    label: "Tethered",
    stackable: false,
    statuses: [],
    system: {
      statusId: "tethered"
    },
    img: "systems/dc20rpg/images/statuses/tethered.svg",
    description: "<p>You are <strong>Tethered</strong> to a creature or Space. While <strong>Tethered</strong>, you can't move farther than the specified Spaces from the location of your Tether.</p>",
    changes: []
  }
}
function _terrified() {
  return {
    id: "terrified",
    name: "Terrified",
    label: "Terrified",
    stackable: false,
    statuses: [],
    system: {
      statusId: "terrified"
    },
    img: "systems/dc20rpg/images/statuses/terrified.svg",
    description: `
    <p>You are subjected to the following effects:</p>
    <ul>
        <li>
            <p>You must spend your turns trying to move as far away as you can from the source as possible.</p>
        </li>
        <li>
            <p>The only Action you can take is the Move Action to try to run away, or the Dodge Action if you are prevented from moving or there's nowhere farther to move.</p>
        </li>
    </ul>
    `,
    changes: []
  }
}

//================================
//         NON-STACKING          =
//================================
function _poisoned() {
  return {
    id: "poisoned",
    _id: "utxmhm7ggruwq7ic",
    name: "Poisoned",
    label: "Poisoned",
    stackable: false,
    statuses: ["impaired"],
    system: {
      statusId: "poisoned"
    },
    img: "systems/dc20rpg/images/statuses/poisoned.svg",
    description: `
    <p>You are subjected to the following effects:</p>
    <ul>
        <li>
            <p>You're <strong>Impaired</strong> (DisADV on Physical Checks).</p>
        </li>
        <li>
            <p>You take 1 Poison damage at the start of each of your turns.</p>
        </li>
    </ul>
    <p></p>
    <h3>Medicine (Action)</h3>
    <p>You can spend <strong>1 AP</strong> to touch a creature and tend to its wounds. Make a Medicine Check against the DC of the Poison. <strong>Success:</strong> You end the Poison on the target.</p>
    `,
    changes: [
      {
        key: "system.events",
        mode: 2,
        priority: undefined,
        value: '"eventType": "damage", "label": "Poisoned", "trigger": "turnStart", "value": 1, "type": "poison"'
      },
      {
        key: "system.rollLevel.onYou.checks.mig",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Poisoned (Impaired)"'
      },
      {
        key: "system.rollLevel.onYou.checks.agi",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Poisoned (Impaired)"'
      },
      {
        key: "system.rollLevel.onYou.checks.att",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Poisoned (Impaired)"'
      },
      {
        key: "system.rollLevel.onYou.martial.melee",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Poisoned (Impaired)"'
      },
      {
        key: "system.rollLevel.onYou.martial.ranged",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Poisoned (Impaired)"'
      },
    ]
  }
}
function _deafened() {
  return {
    id: "deafened",
    name: "Deafened",
    label: "Deafened",
    stackable: false,
    statuses: [],
    system: {
      statusId: "deafened"
    },
    img: "systems/dc20rpg/images/statuses/deafened.svg",
    description: `
    <p>You are subjected to the following effects:</p>
    <ul>
        <li>
            <p>You can't <span>hear </span>(see the <strong>Unheard</strong> section for more information).</p>
        </li>
        <li>
            <p>You have Resistance (Half) to Sonic damage.</p>
        </li>
    </ul>
    `,
    changes: [
      {
        key: "system.damageReduction.damageTypes.sonic.resistance",
        mode: 5,
        priority: undefined,
        value: "true"
      },
      {
        key: "system.rollLevel.onYou.skills",
        mode: 2,
        priority: undefined,
        value: '"label": "Deafened", "confirmation": true, "autoFail": true, "skill": "awa"'
      }
    ]
  }
}
function _blinded() {
  return {
    id: "blinded",
    _id: "1pr5ps19nwoexcbv",
    name: "Blinded",
    label: "Blinded",
    stackable: false,
    statuses: [],
    system: {
      statusId: "blinded"
    },
    img: "systems/dc20rpg/images/statuses/blinded.svg",
    description: `
    <p>You are subjected to the following effects:</p>
    <ul>
        <li>
            <p>You can't see (see the <strong>Unseen</strong> section for more information).</p>
        </li>
        <li>
            <p>All terrain is considered Difficult Terrain for you unless you're guided by another creature.</p>
        </li>
    </ul>
    `,
    changes: [
      {
        key: "system.rollLevel.onYou.skills",
        mode: 2,
        priority: undefined,
        value: '"label": "Blinded", "confirmation": true, "autoFail": true, "skill": "awa"'
      },
      {
        key: "system.rollLevel.onYou.martial.melee",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Blinded (Hindered)"'
      },
      {
        key: "system.rollLevel.onYou.martial.ranged",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Blinded (Hindered)"'
      },
      {
        key: "system.rollLevel.onYou.spell.melee",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Blinded (Hindered)"'
      },
      {
        key: "system.rollLevel.onYou.spell.ranged",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Blinded (Hindered)"'
      },
      {
        key: "system.rollLevel.againstYou.martial.melee",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "adv", "label": "Blinded (Exposed)"'
      },
      {
        key: "system.rollLevel.againstYou.martial.ranged",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "adv", "label": "Blinded (Exposed)"'
      },
      {
        key: "system.rollLevel.againstYou.spell.melee",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "adv", "label": "Blinded (Exposed)"'
      },
      {
        key: "system.rollLevel.againstYou.spell.ranged",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "adv", "label": "Blinded (Exposed)"'
      },
    ]
  }
}
function _invisible() {
  return {
    id: "invisible",
    name: "Invisible",
    label: "Invisible",
    stackable: false,
    statuses: [],
    system: {
      statusId: "invisible"
    },
    img: "systems/dc20rpg/images/statuses/invisible.svg",
    description: "<p>Creatures can't see you unless they have the ability to see the Invisible (see the <strong>Unseen</strong> section for more information).</p>",
    changes: [
      {
        key: "system.rollLevel.againstYou.martial.melee",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Invisible (Enemy cant see you)", "confirmation": true'
      },
      {
        key: "system.rollLevel.againstYou.martial.ranged",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Invisible (Enemy cant see you)", "confirmation": true'
      },
      {
        key: "system.rollLevel.againstYou.spell.melee",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Invisible (Enemy cant see you)", "confirmation": true'
      },
      {
        key: "system.rollLevel.againstYou.spell.ranged",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Invisible (Enemy cant see you)", "confirmation": true'
      },
      {
        key: "system.rollLevel.onYou.martial.melee",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "adv", "label": "Invisible (Enemy cant see you)", "confirmation": true'
      },
      {
        key: "system.rollLevel.onYou.martial.ranged",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "adv", "label": "Invisible (Enemy cant see you)", "confirmation": true'
      },
      {
        key: "system.rollLevel.onYou.spell.melee",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "adv", "label": "Invisible (Enemy cant see you)", "confirmation": true'
      },
      {
        key: "system.rollLevel.onYou.spell.ranged",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "adv", "label": "Invisible (Enemy cant see you)", "confirmation": true'
      }
    ]
  }
}
function _prone() {
  return {
    id: "prone",
    _id: "iukn5nvr42hwe85d",
    name: "Prone",
    label: "Prone",
    stackable: false,
    statuses: [],
    system: {
      statusId: "prone"
    },
    img: "systems/dc20rpg/images/statuses/prone.svg",
    description: `
    <p>While <strong>Prone</strong>, you're subjected to the following effects:</p>
    <ul>
        <li>
            <p>You have DisADV on Attacks, Ranged Attacks have DisADV against you, and Melee Attacks against you have ADV.</p>
        </li>
        <li>
            <p><strong>Crawl Speed:</strong> Until you stand up, your only movement option it to Crawl, which costs an extra 1 Space of movement for every Space moved.</p>
        </li>
    </ul>
    <p><strong>Ending Prone:</strong> You can end being Prone by spending 2 Spaces of movement to stand up.</p>
    <p><strong><span style="text-decoration: underline;">Prone in the Air</span></strong></p>
    <p>When you become Prone, you immediately enter an Uncontrolled Fall unless you're supported by a solid surface (see the <strong>Falling</strong> section for more information).</p>
    `,
    changes: [
      {
        key: "system.moveCost",
        mode: 2,
        priority: undefined,
        value: 1
      },
      {
        key: "system.rollLevel.onYou.martial.melee",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Prone"'
      },
      {
        key: "system.rollLevel.onYou.martial.ranged",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Prone"'
      },
      {
        key: "system.rollLevel.onYou.spell.melee",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Prone"'
      },
      {
        key: "system.rollLevel.onYou.spell.ranged",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Prone"'
      },
      {
        key: "system.rollLevel.againstYou.martial.melee",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "adv", "label": "Melee vs Prone"'
      },
      {
        key: "system.rollLevel.againstYou.martial.ranged",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Ranged vs Prone"'
      },
      {
        key: "system.rollLevel.againstYou.spell.melee",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "adv", "label": "Melee vs Prone"'
      },
      {
        key: "system.rollLevel.againstYou.spell.ranged",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Ranged vs Prone"'
      },
    ]
  }
}
function _grappled() {
  return {
    id: "grappled",
    _id: "iukn5nVA42hwe85d",
    name: "Grappled",
    label: "Grappled",
    stackable: false,
    statuses: ["immobilized"],
    system: {
      statusId: "grappled"
    },
    img: "systems/dc20rpg/images/statuses/grappled.svg",
    description: `
    <p>You're <strong>Immobilized</strong>.</p>
    <p><span style="text-decoration:underline"><strong><span style="text-decoration: underline;">Escape Grapple</span></strong></span></p>
    <p>You can spend <strong>1 AP</strong> to attempt to free yourself from a <strong>Grapple</strong>. Make a Martial Check contested by the Grappler's Athletics Check. <strong>Success:</strong> The Grapple immediately ends.</p>
    <p>The Grapple also ends if any of the following occurs:</p>
    <ul>
        <li>
            <p><strong>Incapacitated:</strong> The Grappler becomes <strong>Incapacitated</strong>.</p>
        </li>
        <li>
            <p><strong>Falling:</strong> The Grappler is unable to carry your weight when you begin falling, provided they are not also falling with you.</p>
        </li>
        <li>
            <p><strong>Forced Movement:</strong> An effect forcibly moves you or the Grappler, causing you to move outside Grappler's reach. If the effect requires a Contested Check or Save, the Grappler makes the Check or Save instead of you. If the effect targets both you and the Grappler, the Grappler makes 1 Check or Save for both of you. <strong>Success:</strong> The targets are not moved. <strong>Failure:</strong> The targets are moved.</p>
        </li>
    </ul>
    <p></p>
    <p><span style="text-decoration:underline"><strong><span style="text-decoration: underline;">Grappled by Multiple Creatures</span></strong></span></p>
    <p>A creature that is <strong>Grappled</strong> by more than 1 creature only suffers the effects of being <strong>Grappled</strong> once. However, a creature <strong>Grappled</strong> by multiple sources will remain <strong>Grappled</strong> until they are free from being <strong>Grappled</strong> by all sources.</p>
    <p id="1cf1b90a-e081-81f5-b4d4-ee30bbddab42" class="block-color-blue_background" style="box-sizing: border-box; user-select: text; scrollbar-width: thin; scrollbar-color: var(--color-scrollbar) var(--color-scrollbar-track); margin: 3px 0px; padding: 6px; border: 2px solid rgb(0, 39, 124); background-color: rgb(160, 175, 209); border-radius: 2px; color: black; font-family: Signika, sans-serif; font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; text-align: start"><span style="font-family: Signika, sans-serif"><strong style="box-sizing:border-box;user-select:text;scrollbar-width:thin;scrollbar-color:var(--color-scrollbar) var(--color-scrollbar-track)">Example:</strong> While <strong>Grappled</strong>, a creature is <strong>Immobilized</strong> (DisADV on Agility Saves). A creature doesn't have DisADV 2 on Agility Saves as a result of being <strong>Grappled</strong> by 2 creatures. They only have DisADV on Agility Saves.</span></p>
    <p><strong>Moving the Target:</strong> If multiple creatures are <strong>Grappling</strong> the same target and one of them tries to move the <strong>Grappled</strong> target, it must make a Contested Athletics Check against all creatures <strong>Grappling</strong> the same target. <strong>Success:</strong> It ends the <strong>Grapple</strong> on the target by all creatures other than itself, allowing it to move the creature as normal.</p>
    `,
    changes: [
      {
        key: "system.movement.ground.bonus",
        mode: 2,
        priority: undefined,
        value: -100
      },
      {
        key: "system.rollLevel.onYou.saves.agi",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "dis", "label": "Grappled (Immobilized)"'
      },
    ]
  }
}
function _incapacitated() {
  return {
    id: "incapacitated",
    name: "Incapacitated",
    label: "Incapacitated",
    stackable: false,
    statuses: [],
    system: {
      statusId: "incapacitated"
    },
    img: "systems/dc20rpg/images/statuses/incapacitated.svg",
    description: `
    <p>You are subjected to the following effects:</p>
    <ul>
        <li>
            <p>You can't move, speak.</p>
        </li>
        <li>
            <p>You can't spend Actions Points or use Minor Actions.</p>
        </li>
    </ul>
    `,
    changes: [
      {
        key: "system.globalModifier.prevent.goUnderAP",
        mode: 4,
        priority: undefined,
        value: 99
      },
      {
        key: "system.movement.ground.bonus",
        mode: 2,
        priority: undefined,
        value: -100
      },
    ]
  }
}
function _paralyzed() {
  return {
    id: "paralyzed",
    _id: "pze6ctp9bxbfldz5",
    name: "Paralyzed",
    label: "Paralyzed",
    stackable: false,
    statuses: ["incapacitated"],
    system: {
      statusId: "paralyzed"
    },
    img: "systems/dc20rpg/images/statuses/paralyzed.svg",
    description: `
    <p>You are subjected to the following effects:</p>
    <ul>
        <li>
            <p>You're <strong>Incapacitated</strong>.</p>
        </li>
        <li>
            <p>You automatically fail Physical Saves (except against Poisons and Diseases).</p>
        </li>
        <li>
            <p>Attacks against you have <strong>ADV</strong>.</p>
        </li>
        <li>
            <p>Attacks made within 1 Space are considered Critical Hits.</p>
        </li>
    </ul>
    `,
    changes: [
      {
        key: "system.globalModifier.prevent.goUnderAP",
        mode: 4,
        priority: undefined,
        value: 99
      },
      {
        key: "system.movement.ground.bonus",
        mode: 2,
        priority: undefined,
        value: -100
      },
      {
        key: "system.rollLevel.againstYou.martial.melee",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "adv", "label": "Paralyzed"'
      },
      {
        key: "system.rollLevel.againstYou.martial.ranged",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "adv", "label": "Paralyzed"'
      },
      {
        key: "system.rollLevel.againstYou.spell.melee",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "adv", "label": "Paralyzed"'
      },
      {
        key: "system.rollLevel.againstYou.spell.ranged",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "adv", "label": "Paralyzed"'
      },
      {
        key: "system.rollLevel.onYou.saves.mig",
        mode: 5,
        priority: undefined,
        value: '"label": "Paralyzed", "autoFail": true'
      },
      {
        key: "system.rollLevel.onYou.saves.agi",
        mode: 5,
        priority: undefined,
        value: '"label": "Paralyzed", "autoFail": true'
      },
      {
        key: "system.rollLevel.againstYou.martial.melee",
        mode: 2,
        priority: undefined,
        value: '"label": "Paralyzed (Attack within 1 Space)", "autoCrit": true, "confirmation": true'
      },
      {
        key: "system.rollLevel.againstYou.martial.ranged",
        mode: 2,
        priority: undefined,
        value: '"label": "Paralyzed (Attack within 1 Space)", "autoCrit": true, "confirmation": true'
      },
      {
        key: "system.rollLevel.againstYou.spell.melee",
        mode: 2,
        priority: undefined,
        value: '"label": "Paralyzed (Attack within 1 Space)", "autoCrit": true, "confirmation": true'
      },
      {
        key: "system.rollLevel.againstYou.spell.ranged",
        mode: 2,
        priority: undefined,
        value: '"label": "Paralyzed (Attack within 1 Space)", "autoCrit": true, "confirmation": true'
      }
    ]
  }
}
function _unconscious() {
  return {
    id: "unconscious",
    _id: "k6mzhz72f3j8fjhp",
    name: "Unconscious",
    label: "Unconscious",
    stackable: false,
    statuses: ["incapacitated"],
    system: {
      statusId: "unconscious"
    },
    img: "systems/dc20rpg/images/statuses/unconscious.svg",
    description: `
    <p>When you become <strong>Unconscious</strong>, you immediately drop whatever you are holding and fall <strong>Prone</strong>. While <strong>Unconscious</strong>, you're subjected to the following effects:</p>
    <ul>
        <li>
            <p>You're <strong>Incapacitated</strong>.</p>
        </li>
        <li>
            <p>You're not aware of your surroundings.</p>
        </li>
        <li>
            <p>You automatically fail Physical Saves (except against Poisons and Diseases).</p>
        </li>
        <li>
            <p>Attacks against you have ADV.</p>
        </li>
        <li>
            <p>Attacks made within 1 Space are considered Critical Hits.</p>
        </li>
    </ul>
    `,
    changes: [
      {
        key: "system.globalModifier.prevent.goUnderAP",
        mode: 4,
        priority: undefined,
        value: 99
      },
      {
        key: "system.movement.ground.bonus",
        mode: 2,
        priority: undefined,
        value: -100
      },
      {
        key: "system.rollLevel.againstYou.martial.melee",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "adv", "label": "Unconscious"'
      },
      {
        key: "system.rollLevel.againstYou.martial.ranged",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "adv", "label": "Unconscious"'
      },
      {
        key: "system.rollLevel.againstYou.spell.melee",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "adv", "label": "Unconscious"'
      },
      {
        key: "system.rollLevel.againstYou.spell.ranged",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "adv", "label": "Unconscious"'
      },
      {
        key: "system.rollLevel.onYou.saves.mig",
        mode: 5,
        priority: undefined,
        value: '"label": "Unconscious", "autoFail": true, "confirmation": true'
      },
      {
        key: "system.rollLevel.onYou.saves.agi",
        mode: 5,
        priority: undefined,
        value: '"label": "Unconscious", "autoFail": true, "confirmation": true'
      },
      {
        key: "system.rollLevel.againstYou.martial.melee",
        mode: 2,
        priority: undefined,
        value: '"label": "Unconscious (Attack within 1 Space)", "autoCrit": true, "confirmation": true'
      },
      {
        key: "system.rollLevel.againstYou.martial.ranged",
        mode: 2,
        priority: undefined,
        value: '"label": "Unconscious (Attack within 1 Space)", "autoCrit": true, "confirmation": true'
      },
      {
        key: "system.rollLevel.againstYou.spell.melee",
        mode: 2,
        priority: undefined,
        value: '"label": "Unconscious (Attack within 1 Space)", "autoCrit": true, "confirmation": true'
      },
      {
        key: "system.rollLevel.againstYou.spell.ranged",
        mode: 2,
        priority: undefined,
        value: '"label": "Unconscious (Attack within 1 Space)", "autoCrit": true, "confirmation": true'
      }
    ]
  }
}
function _petrified() {
  return {
    id: "petrified",
    _id: "658otyjsr2571jto",
    name: "Petrified",
    label: "Petrified",
    stackable: false,
    statuses: ["incapacitated"],
    system: {
      statusId: "petrified"
    },
    img: "systems/dc20rpg/images/statuses/petrified.svg",
    description: `
    <p>You and your mundane belongings are turned into a inanimate substance (often stone). While <strong>Petrified</strong>, you count as both an object and a creature, and you're subjected to the following effects:</p>
    <ul>
        <li>
            <p>You're not aware of your surroundings.</p>
        </li>
        <li>
            <p>You're 10 times heavier than normal.</p>
        </li>
        <li>
            <p>You're <strong>Incapacitated</strong>.</p>
        </li>
        <li>
            <p>You automatically fail Physical Saves.</p>
        </li>
        <li>
            <p>Attacks against you have ADV.</p>
        </li>
        <li>
            <p>You gain Bludgeoning Vulnerability (Double) and Resistance (Half) to all other damage.</p>
        </li>
        <li>
            <p>Curses, Diseases, Poisons, or Conditions afflicting you are suspended (unless it imposed the Petrified Condition), and you're immune to gaining new ones.</p>
        </li>
    </ul>
    `,
    changes: [
      {
        key: "system.globalModifier.prevent.goUnderAP",
        mode: 4,
        priority: undefined,
        value: 99
      },
      {
        key: "system.movement.ground.bonus",
        mode: 2,
        priority: undefined,
        value: -100
      },
      {
        key: "system.rollLevel.againstYou.martial.melee",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "adv", "label": "Petrified"'
      },
      {
        key: "system.rollLevel.againstYou.martial.ranged",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "adv", "label": "Petrified"'
      },
      {
        key: "system.rollLevel.againstYou.spell.melee",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "adv", "label": "Petrified"'
      },
      {
        key: "system.rollLevel.againstYou.spell.ranged",
        mode: 2,
        priority: undefined,
        value: '"value": 1, "type": "adv", "label": "Petrified"'
      },
      {
        key: "system.damageReduction.damageTypes.corrosion.resistance",
        mode: 5,
        priority: undefined,
        value: "true"
      },
      {
        key: "system.damageReduction.damageTypes.cold.resistance",
        mode: 5,
        priority: undefined,
        value: "true"
      },
      {
        key: "system.damageReduction.damageTypes.fire.resistance",
        mode: 5,
        priority: undefined,
        value: "true"
      },
      {
        key: "system.damageReduction.damageTypes.radiant.resistance",
        mode: 5,
        priority: undefined,
        value: "true"
      },
      {
        key: "system.damageReduction.damageTypes.lightning.resistance",
        mode: 5,
        priority: undefined,
        value: "true"
      },
      {
        key: "system.damageReduction.damageTypes.poison.resistance",
        mode: 5,
        priority: undefined,
        value: "true"
      },
      {
        key: "system.damageReduction.damageTypes.psychic.resistance",
        mode: 5,
        priority: undefined,
        value: "true"
      },
      {
        key: "system.damageReduction.damageTypes.sonic.resistance",
        mode: 5,
        priority: undefined,
        value: "true"
      },
      {
        key: "system.damageReduction.damageTypes.sonic.resistance",
        mode: 5,
        priority: undefined,
        value: "true"
      },
      {
        key: "system.damageReduction.damageTypes.umbral.resistance",
        mode: 5,
        priority: undefined,
        value: "true"
      },
      {
        key: "system.damageReduction.damageTypes.piercing.resistance",
        mode: 5,
        priority: undefined,
        value: "true"
      },
      {
        key: "system.damageReduction.damageTypes.slashing.resistance",
        mode: 5,
        priority: undefined,
        value: "true"
      },
      {
        key: "system.damageReduction.damageTypes.bludgeoning.vulnerability",
        mode: 5,
        priority: undefined,
        value: "true"
      },
      {
        key: "system.rollLevel.onYou.saves.mig",
        mode: 5,
        priority: undefined,
        value: '"label": "Petrified", "autoFail": true'
      },
      {
        key: "system.rollLevel.onYou.saves.agi",
        mode: 5,
        priority: undefined,
        value: '"label": "Petrified", "autoFail": true'
      }
    ]
  }
}
function _surprised() {
  return {
    id: "surprised",
    _id: "658otyjsr1572jto",
    name: "Surprised",
    label: "Surprised",
    stackable: false,
    statuses: [],
    system: {
      statusId: "surprised"
    },
    img: "systems/dc20rpg/images/statuses/surprised.svg",
    description: "<p>Your current and maximum <strong>AP</strong> is reduced by 2.</p>",
    changes: [
      {
        key: "system.globalModifier.prevent.goUnderAP",
        mode: 2,
        priority: undefined,
        value: 2
      },
    ]
  }
}

function prepareColorPalette() {
  // Set color Palette
  const colorPalette = game.settings.get("dc20rpg", "colorPaletteStore");
  let color = colorPalette[game.settings.get("dc20rpg", "selectedColor")];
  if (!color) color = colorPalette["default"];

  const root = document.documentElement.style;
  Object.entries(color).forEach(([key, color]) => root.setProperty(key, color));
}

function defaultColorPalette() {
  return {
    default: _defaultColors(),
    dark: _darkColors()
  }
}
function _defaultColors() {
  return {
    ['--primary-color']: "#741a89",
    ['--primary-light']: "#917996",
    ['--primary-dark']: "#5a265f",
    ['--primary-darker']: "#3f0344",

    ['--background-color']: "transparent",
    ['--background-banner']: "#6c0097b0",
    
    ['--secondary-color']: "#c0c0c0",
    ['--secondary-dark']: "#646464",
    ['--secondary-darker']: "#262626",
    ['--secondary-lighter']: "#dfdfdf",
    ['--secondary-light-alpha']: "#dfdfdfcc",

    ['--table-1']: "#5a265f",
    ['--table-2']: "#48034e",

    ['--dark-red']: "#b20000",
    ['--unequipped']: "#c5c5c5a3",
    ['--equipped']: "#88a16f",
    ['--attuned']: "#c7c172",
    ['--activated-effect']: "#77adad",
    ['--item-selected']: "#ac45d5a6",

    ['--action-point']: "#610064",
    ['--stamina']: "#b86b0d",
    ['--mana']: "#124b8b",
    ['--health-point']: "#921a1a",
    ['--health']: "#138241",
    ['--grit']: "#7a0404",

    ['--health-bar']: "#6fde75",
    ['--temp-health-bar']: "#ccac7d",
    ['--stamina-bar']: "#e1d676",
    ['--mana-bar']: "#81a3e7",
    ['--grit-bar']: "#b36363",

    ['--crit']: "#0e8b1e",
    ['--crit-background']: "#4f9f5c",
    ['--fail']: "#b10000",
    ['--fail-background']: "#914a4a",

    // NPC Sheet
    ['--npc-main']: "#1f268d",
    ['--npc-main-light']: "#534d69",
    ['--npc-main-lighter']: "#6876a7",
    ['--npc-main-dark']: "#0e1250",
    ['--npc-secondary']: "#c0c0c0",
    ['--npc-secondary-light']: "#dfdfdf",
    ['--npc-secondary-light-alpha']: "#dfdfdfcc",
    ['--npc-secondary-dark']: "#646464",
    ['--npc-secondary-darker']: "#262626",
    ['--npc-text-color-1']: "#ffffff",
    ['--npc-text-color-2']: "#000000",
    ['--npc-background']: "transparent",
    ['--npc-table-1']: "#262a69",
    ['--npc-table-2']: "#050947",
    ['--npc-header-image-color']: "#2442c9a3",
    ['--npc-sidetab-image-color']: "#2442c9a3",

    // PC Sheet
    ['--pc-main']: "#5d178b",
    ['--pc-main-light']: "#534d69",
    ['--pc-main-lighter']: "#786188",
    ['--pc-main-dark']: "#2b0e50",
    ['--pc-secondary']: "#c0c0c0",
    ['--pc-secondary-light']: "#dfdfdf",
    ['--pc-secondary-light-alpha']: "#dfdfdfcc",
    ['--pc-secondary-dark']: "#646464",
    ['--pc-secondary-darker']: "#262626",
    ['--pc-text-color-1']: "#ffffff",
    ['--pc-text-color-2']: "#000000",
    ['--pc-background']: "transparent",
    ['--pc-table-1']: "#573085",
    ['--pc-table-2']: "#290547",
    ['--pc-header-image-color']: "#44116ba3",
    ['--pc-sidetab-image-color']: "#431169a3",
    ['--pc-unique-item-color']: "#ac45d5a6",
  }
}

function _darkColors() {
  return {
    ['--primary-color']: "#741a89",
    ['--primary-light']: "#917996",
    ['--primary-dark']: "#5a265f",
    ['--primary-darker']: "#3f0344",

    ['--background-color']: "transparent",
    ['--background-banner']: "#6c0097b0",
    
    ['--secondary-color']: "#c0c0c0",
    ['--secondary-dark']: "#646464",
    ['--secondary-darker']: "#262626",
    ['--secondary-lighter']: "#dfdfdf",
    ['--secondary-light-alpha']: "#dfdfdfcc",

    ['--table-1']: "#5a265f",
    ['--table-2']: "#48034e",

    ['--dark-red']: "#b20000",
    ['--unequipped']: "#c5c5c5a3",
    ['--equipped']: "#88a16f",
    ['--attuned']: "#c7c172",
    ['--activated-effect']: "#77adad",
    ['--item-selected']: "#ac45d5a6",

    ['--action-point']: "#610064",
    ['--stamina']: "#b86b0d",
    ['--mana']: "#124b8b",
    ['--health-point']: "#921a1a",
    ['--health']: "#138241",
    ['--grit']: "#7a0404",

    ['--health-bar']: "#6fde75",
    ['--temp-health-bar']: "#ccac7d",
    ['--stamina-bar']: "#e1d676",
    ['--mana-bar']: "#81a3e7",
    ['--grit-bar']: "#b36363",

    ['--crit']: "#0e8b1e",
    ['--crit-background']: "#4f9f5c",
    ['--fail']: "#b10000",
    ['--fail-background']: "#914a4a",

    // NPC Sheet
    ['--npc-main']: "#1f268d",
    ['--npc-main-light']: "#534d69",
    ['--npc-main-lighter']: "#6876a7",
    ['--npc-main-dark']: "#0e1250",
    ['--npc-secondary']: "#c0c0c0",
    ['--npc-secondary-light']: "#dfdfdf",
    ['--npc-secondary-light-alpha']: "#dfdfdfcc",
    ['--npc-secondary-dark']: "#646464",
    ['--npc-secondary-darker']: "#262626",
    ['--npc-text-color-1']: "#ffffff",
    ['--npc-text-color-2']: "#9fa3d1",
    ['--npc-background']: "#303030",
    ['--npc-table-1']: "#262a69",
    ['--npc-table-2']: "#050947",
    ['--npc-header-image-color']: "#2442c9a3",
    ['--npc-sidetab-image-color']: "#2442c9a3",

    // PC Sheet
    ['--pc-main']: "#3d0f5c",
    ['--pc-main-light']: "#534d69",
    ['--pc-main-lighter']: "#786188",
    ['--pc-main-dark']: "#2b0e50",
    ['--pc-secondary']: "#c0c0c0",
    ['--pc-secondary-light']: "#dfdfdf",
    ['--pc-secondary-light-alpha']: "#dfdfdfcc",
    ['--pc-secondary-dark']: "#646464",
    ['--pc-secondary-darker']: "#262626",
    ['--pc-text-color-1']: "#ffffff",
    ['--pc-text-color-2']: "#d0c1e2",
    ['--pc-background']: "#303030",
    ['--pc-table-1']: "#573085",
    ['--pc-table-2']: "#290547",
    ['--pc-header-image-color']: "#371452a3",
    ['--pc-sidetab-image-color']: "#371452a3",
    ['--pc-unique-item-color']: "#ac45d5a6",
  }
}

class ColorSetting extends FormApplication {

  constructor(dialogData = {title: "Color Palette Selection"}, options = {}) {
    super(dialogData, options);
    this.selectedKey = game.settings.get("dc20rpg", "selectedColor");
    this.liveRefresh = false;
  }

  static get defaultOptions() {
    return foundry.utils.mergeObject(super.defaultOptions, {
      template: "systems/dc20rpg/templates/dialogs/color-settings.hbs",
      classes: ["dc20rpg", "dialog", "flex-dialog"],
      tabs: [{ navSelector: ".navigation", contentSelector: ".body", initial: "core" }],
    });
  }

  getData() {
    let selectedKey = this.selectedKey;
    let selected = this._getColor(selectedKey);
    if (!selected) {
      selectedKey = "default";
      selected = this._getColor(selectedKey);
    }
    return {
      choices: this._getColorChoices(),
      selectedKey: selectedKey,
      selected: this._groupColors(selected),
      userIsGM: game.user.isGM,
      liveRefresh: this.liveRefresh
    };
  }

  _groupColors(selected) {
    const core = {};
    const pc = {};
    const npc = {};
    const other = {};

    Object.entries(selected).forEach(([key, color]) => {
      if (key.startsWith("--pc")) pc[key] = color;
      else if (key.startsWith("--npc")) npc[key] = color;
      else if (key.startsWith("--primary") || key.startsWith("--secondary")) core[key] = color;
      else other[key] = color;
    });
    return {
      core: core,
      pc: pc,
      npc: npc,
      other: other
    }
  }

  _getColorChoices() {
    const colorPalette = game.settings.get("dc20rpg", "colorPaletteStore");
    const keys = {};
    for(let colorKey of Object.keys(colorPalette)) {
      keys[colorKey] = colorKey;
    }
    return keys;
  }

  _getColor(selectedKey) {
    const colorPalette = game.settings.get("dc20rpg", "colorPaletteStore");
    return colorPalette[selectedKey];
  }

  _updateObject() {}

  _liveUpdateStyles() {
    if (!this.liveRefresh) return;
    const selectedColor = this._getColor(this.selectedKey);
    const root = document.documentElement.style;
    Object.entries(selectedColor).forEach(([key, color]) => root.setProperty(key, color));
  }

   /** @override */
  activateListeners(html) {
    super.activateListeners(html);
    html.find(".selectable").change(ev => this._onSelection(ev));
    html.find(".save").click(ev => this._onSave(ev));
    html.find(".update").click(ev => this._onUpdate(ev, html));

    html.find('.add-new').click(ev => this._createNewColor(ev, html));
    html.find('.remove-selected').click(ev => this._removeSelected(ev));
    html.find('.live-refresh').click(ev => this._onLiveRefresh(ev));

    // Export/Import
    html.find('.export').click(() => this._onExport());
    html.find('.import').click(() => this._onImport());
  }

  async _createNewColor(event, html) {
    event.preventDefault();
    const newColorKey = html.find('.new-color-selector')[0].value;
    if (newColorKey && newColorKey !== "default") {
      const colorPalette = game.settings.get("dc20rpg", "colorPaletteStore");
      const dft = this._getColor("default");
      colorPalette[newColorKey] = dft;
      await game.settings.set("dc20rpg", "colorPaletteStore", colorPalette);
      this.selectedKey = newColorKey;
      this.render(true);
    }
    else {
      ui.notifications.error("You need to provide valid key first"); 
    }
  }

  async _removeSelected(event){
    event.preventDefault();
    const selectedKey = event.currentTarget.dataset.key;
    const colorPalette = game.settings.get("dc20rpg", "colorPaletteStore");
    delete colorPalette[selectedKey];
    await game.settings.set("dc20rpg", "colorPaletteStore", colorPalette);
    this.selectedKey = "default";
    this.render(true);
  }

  _onSelection(event) {
    event.preventDefault();
    this.selectedKey = event.currentTarget.value;
    this._liveUpdateStyles();
    this.render(true);
  }

  async _onSave(event) {
    event.preventDefault();
    await game.settings.set("dc20rpg", "selectedColor", this.selectedKey);
    this.close();
  }

  async _onUpdate(event, html) {
    event.preventDefault();
    const selectedKey = this.selectedKey;
    const selected = this._getColor(selectedKey);
    const inputs = html.find('.update-color-value');

    Object.values(inputs).forEach(input => {
      if (input.dataset) {
        const key = input.dataset.key;
        const value = input.value;
        selected[key] = value;
      }
    });

    const colorPalette = game.settings.get("dc20rpg", "colorPaletteStore");
    colorPalette[selectedKey] = selected;
    await game.settings.set("dc20rpg", "colorPaletteStore", colorPalette);
    this._liveUpdateStyles();
  }

  _onLiveRefresh(event) {
    event.preventDefault();
    this.liveRefresh = !this.liveRefresh;
    this._liveUpdateStyles();
    this.render(true);
  }

  _onExport() {
    const colorPalette = game.settings.get("dc20rpg", "colorPaletteStore");
    const toExport = colorPalette[this.selectedKey];
    toExport.paletteKey = this.selectedKey;
    createTextDialog(JSON.stringify(toExport), "Export Palette");
  }

  async _onImport() {
    const toImport = await createTextDialog("", "Import Palette");
    if (toImport) {
      try {
        const newPalette = JSON.parse(toImport);
        const newKey = newPalette.paletteKey;
        delete newPalette.paletteKey;

        const colorPalette = game.settings.get("dc20rpg", "colorPaletteStore");
        if (colorPalette[newKey]) {
          ui.notifications.warn(`Color Palette with key '${newKey}' already exist.`); 
          return;
        }
        colorPalette[newKey] = newPalette;
        await game.settings.set("dc20rpg", "colorPaletteStore", colorPalette);
        this.selectedKey = newKey;
        this.render(true);
      } 
      catch(error) {
        ui.notifications.error(`Cannot import Color Palette - error: ${error}`); 
      }
    }
  }
}

async function createTextDialog(text, title) {
  return new Promise((resolve, reject) => {
    // Create the dialog
    let dialog = new Dialog({
      title: title,
      content: `
        <div>
          <textarea id="input-string" name="input-string" rows="5" style="width: 383px; height: 500px">${text}</textarea>
        </div>
      `,
      buttons: {
        submit: {
          icon: '<i class="fas fa-check"></i>',
          label: title,
          callback: (html) => {
            const userInput = html.find('[name="input-string"]').val();
            resolve(userInput);
          }
        },
      },
      default: "submit", 
    });
    dialog.render(true);
  });
}

function defaultSkillList() {
  return {
    skills: {
      awa: SkillConfiguration.skill("prime", "dc20rpg.skills.awa"),
      acr: SkillConfiguration.skill("agi", "dc20rpg.skills.acr"),
      ani: SkillConfiguration.skill("cha", "dc20rpg.skills.ani"),
      ath: SkillConfiguration.skill("mig", "dc20rpg.skills.ath"),
      inf: SkillConfiguration.skill("cha", "dc20rpg.skills.inf"),
      inm: SkillConfiguration.skill("mig", "dc20rpg.skills.inm"),
      ins: SkillConfiguration.skill("cha", "dc20rpg.skills.ins"),
      inv: SkillConfiguration.skill("int", "dc20rpg.skills.inv"),
      med: SkillConfiguration.skill("int", "dc20rpg.skills.med"),
      ste: SkillConfiguration.skill("agi", "dc20rpg.skills.ste"),
      sur: SkillConfiguration.skill("int", "dc20rpg.skills.sur"),
      tri: SkillConfiguration.skill("agi", "dc20rpg.skills.tri")
    },
    trades: {
      arc: SkillConfiguration.skill("int", "dc20rpg.trades.arc"),
      his: SkillConfiguration.skill("int", "dc20rpg.trades.his"),
      nat: SkillConfiguration.skill("int", "dc20rpg.trades.nat"),
      occ: SkillConfiguration.skill("int", "dc20rpg.trades.occ"),
      rel: SkillConfiguration.skill("int", "dc20rpg.trades.rel"),
      eng: SkillConfiguration.skill("int", "dc20rpg.trades.eng"),
      alc: SkillConfiguration.skill("int", "dc20rpg.trades.alc"),
      bla: SkillConfiguration.skill("mig", "dc20rpg.trades.bla"),
      bre: SkillConfiguration.skill("int", "dc20rpg.trades.bre"),
      cap: SkillConfiguration.skill("agi", "dc20rpg.trades.cap"),
      car: SkillConfiguration.skill("int", "dc20rpg.trades.car"),
      coo: SkillConfiguration.skill("int", "dc20rpg.trades.coo"),
      cry: SkillConfiguration.skill("int", "dc20rpg.trades.cry"),
      dis: SkillConfiguration.skill("cha", "dc20rpg.trades.dis"),
      gam: SkillConfiguration.skill("cha", "dc20rpg.trades.gam"),
      gla: SkillConfiguration.skill("mig", "dc20rpg.trades.gla"),
      her: SkillConfiguration.skill("int", "dc20rpg.trades.her"),
      ill: SkillConfiguration.skill("agi", "dc20rpg.trades.ill"),
      jew: SkillConfiguration.skill("agi", "dc20rpg.trades.jew"),
      lea: SkillConfiguration.skill("agi", "dc20rpg.trades.lea"),
      loc: SkillConfiguration.skill("agi", "dc20rpg.trades.loc"),
      mas: SkillConfiguration.skill("mig", "dc20rpg.trades.mas"),
      mus: SkillConfiguration.skill("cha", "dc20rpg.trades.mus"),
      scu: SkillConfiguration.skill("agi", "dc20rpg.trades.scu"),
      the: SkillConfiguration.skill("cha", "dc20rpg.trades.the"),
      tin: SkillConfiguration.skill("int", "dc20rpg.trades.tin"),
      wea: SkillConfiguration.skill("agi", "dc20rpg.trades.wea"),
      veh: SkillConfiguration.skill("agi", "dc20rpg.trades.veh")
    },
    languages: {
      com: {
        mastery: 2 ,
        category: "mortal",
        label: "dc20rpg.languages.com"
      },
      hum: SkillConfiguration.lang("mortal", "dc20rpg.languages.hum"),
      dwa: SkillConfiguration.lang("mortal", "dc20rpg.languages.dwa"),
      elv: SkillConfiguration.lang("mortal", "dc20rpg.languages.elv"),
      gno: SkillConfiguration.lang("mortal", "dc20rpg.languages.gno"),
      hal: SkillConfiguration.lang("mortal", "dc20rpg.languages.hal"),
      sig: SkillConfiguration.lang("mortal", "dc20rpg.languages.sig"),
      gia: SkillConfiguration.lang("exotic", "dc20rpg.languages.gia"),
      dra: SkillConfiguration.lang("exotic", "dc20rpg.languages.dra"),
      orc: SkillConfiguration.lang("exotic", "dc20rpg.languages.orc"),
      fey: SkillConfiguration.lang("exotic", "dc20rpg.languages.fey"),
      ele: SkillConfiguration.lang("exotic", "dc20rpg.languages.ele"),
      cel: SkillConfiguration.lang("divine", "dc20rpg.languages.cel"),
      fie: SkillConfiguration.lang("divine", "dc20rpg.languages.fie"),
      dee: SkillConfiguration.lang("outer", "dc20rpg.languages.dee"),
    }
  }
}

class SkillConfiguration extends FormApplication {

  static skill(baseAttribute, label) {
    return {
      modifier: 0,
      bonus: 0,
      mastery: 0,
      baseAttribute: baseAttribute,
      custom: false,
      label: label,
    };
  }
  
  static lang(category, label) {
    return {
      mastery: 0,
      category: category,
      label: label
    }
  }

  constructor(dialogData = {title: "Customize Skill List"}, options = {}) {
    super(dialogData, options);
    this.skillStore = game.settings.get("dc20rpg", "skillStore");
  }

  static get defaultOptions() {
    return foundry.utils.mergeObject(super.defaultOptions, {
      template: "systems/dc20rpg/templates/dialogs/skill-config.hbs",
      classes: ["dc20rpg", "dialog", "flex-dialog"],
    });
  }

  getData() {
    return {
      attributes: CONFIG.DC20RPG.DROPDOWN_DATA.attributesWithPrime,
      skillStore: this.skillStore,
      langCategories: {
        "mortal": "Mortal",
        "exotic": "Exotic",
        "divine": "Divine",
        "outer": "Outer"
      }
    };
  }

  activateListeners(html) {
    super.activateListeners(html);
    html.find('.change-value').change(ev => this._onChangeValue(valueOf(ev), datasetOf(ev).field, datasetOf(ev).key, datasetOf(ev).category));
    html.find('.add-skill').click(ev => this._onAdd(datasetOf(ev).category));
    html.find('.remove-skill').click(ev => this._onRemove(datasetOf(ev).key, datasetOf(ev).category));
    html.find('.save-and-update').click(ev => this._onSave(ev));
    html.find('.restore-default').click(ev => this._onDefault(ev));
  }

  _onChangeValue(value, field, key, category) {
    this.skillStore[category][key][field] = value;
    this.render();
  }

  async _onAdd(category) {
    const key = await getSimplePopup("input", {header: "Provide Key (4 characters)"});
    if (!key || key.length !== 4) {
      ui.notifications.error("Incorrect key!");
      return;
    }
    const skill = category === "languages" ? SkillConfiguration.lang("mortal", "New Languange") : SkillConfiguration.skill("mig", "New Skill");
    this.skillStore[category][key] = skill;
    this.render();
  }

  _onRemove(key, category) {
    delete this.skillStore[category][key];
    this.render();
  }

  _onDefault(event) {
    event.preventDefault();
    this.skillStore = defaultSkillList();
    this.render();
  }

  async _onSave(event) {
    event.preventDefault();
    const proceed = await getSimplePopup("confirm", {header: "The update will take a moment and then the game will be refreshed. Proceed?"});
    if (!proceed) return;

    await game.settings.set("dc20rpg", "skillStore", this.skillStore);

    // Iterate over actors
    for (const actor of game.actors) await actor.refreshSkills();
    // Iterate over tokens
    const allTokens = [];
    game.scenes.forEach(scene => {if (scene) scene.tokens.forEach(token => {if (token && !token.actorLink) allTokens.push(token);});});
    for (const token of allTokens) await token.actor.refreshSkills();

    this.close();
    window.location.reload();
  }
}

//=============================================================================
//																	SCOPES																		=
// types: Supported types: "string", "number", "boolean", "object"						=
// scopes: 																																		=
//		- "world" - system-specific settings																		=
//		- "client" - client-side settings - not synchronized between users. 		=
//								 They are only stored locally and are not saved to the 			=
//								 server. These settings are typically used for interface 		=
//								 customization or client-specific behavior.									=
//		- "user" - individual-user settings	- synchronized between devices for 	=
//							 that user.	They are stored on the server and can be accessed =
//							 from any device where the user is logged in. These settings 	=						
// 							 are often used for personal preferences or configurations.		=
//=============================================================================


// For more custom settings (with popups for example) see DND5e system
function registerGameSettings(settings) {
  settings.register("dc20rpg", "lastMigration", {
    name: "Latest System Migration Applied",
    scope: "world",
    config: false,
    type: String,
    default: ""
  });

  settings.register("dc20rpg", "skillStore", {
    scope: "world",
    config: false,
    default: defaultSkillList(),
    type: Object
  });

  settings.register("dc20rpg", "suppressAdvancements", {
    name: "Suppress Advancements",
    scope: "client",
    config: false,
    type: Boolean,
    default: false
  });

  // TODO: No longer required?
  // settings.register("dc20rpg", "defaultInitiativeKey", {
  //   name: "Default Initiative Check",
  //   scope: "user",
  //   hint: "What check should be a default choice when you roll for initiative.",
  //   config: true,
  //   default: "att",
  //   type: new foundry.data.fields.StringField({required: true, blank: false, initial: "att", choices: _getInitiativeSkills()})
  // });

  settings.register("dc20rpg", "useMovementPoints", {
    name: "Use Movement Points",
    hint: "Select, when Movement Points should be subtracted.",
    scope: "world",
    config: true,
    default: "onTurn",
    type: new foundry.data.fields.StringField({required: true, blank: false, initial: "onTurn", choices: {
      onTurn: "Only on Actor's Turn",
      onCombat: "When Actor in Combat",
      always: "Always",
      never: "Never"
    }}),
	});

  settings.register("dc20rpg", "snapMovement", {
    name: "Snap Movement",
    hint: "If selected, Token will move to the closest space when there is not enough Move Points to its final destination.",
    scope: "world",
    config: true,
    default: false,
    type: Boolean
  });

  settings.register("dc20rpg", "askToSpendMoreAP", {
    name: "Allow for Move Action popup",
    hint: "If selected, Not enough Move Points will cause a popup to appear asking to spend more AP for the movement.",
    scope: "world",
    config: true,
    default: false,
    type: Boolean
  });

  settings.register("dc20rpg", "disableDifficultTerrain", {
    name: "Disable Difficult Terrain",
    hint: "If selected, Difficult Terrain won't influence token movement costs.",
    scope: "world",
    config: true,
    default: false,
    type: Boolean
	});

  settings.register("dc20rpg", "enablePositionCheck", {
    name: "Enable Position Check",
    hint: "If selected, Token positioning rules will be respected (e.g. Close Quarters, Flanking).",
    scope: "world",
    config: true,
    default: false,
    type: Boolean
	});

  settings.register("dc20rpg", "neutralDispositionIdentity", {
    name: "Neutral Tokens Disposition Identity",
    hint: "How neutral disposition tokens should be treated (e.g. during Flanking check or effect application from Measured Templates).",
    scope: "world",
    config: true,
    default: "separated",
    type: new foundry.data.fields.StringField({required: true, blank: false, initial: "separated", choices: {
      separated: "Separated Group",
      hostile: "Part of the Hostile Group",
      friendly: "Part of the Friendly Group"
    }}),
	});

  settings.register("dc20rpg", "enableRangeCheck", {
    name: "Enable Range Check",
    hint: "If selected, Normal/Long/Out of Range rulles will be respected (e.g. Weapon Ranges).",
    scope: "world",
    config: true,
    default: false,
    type: Boolean
	});

  settings.register("dc20rpg", "autoRollLevelCheck", {
    name: "Run Roll Level Check Automatically",
    hint: "If selected, Roll Level Check will run automatically when performing a roll and modifing roll level with enhancement or range change.",
    scope: "world",
    config: true,
    default: false,
    type: Boolean
	});

  settings.register("dc20rpg", "showEventChatMessage", {
    name: "Show Event Chat Messages to Players",
    hint: "If selected damage/healing taken and effect removed messages will be send to public chat instead of being GM only.",
    scope: "world",
    config: true,
    default: false,
    type: Boolean
	});

  settings.register("dc20rpg", "showDamageForPlayers", {
    name: "Show Damage and Healing on Chat Message",
    hint: "If false, only GM will be able to see expected damage and healing target will receive.",
    scope: "world",
    config: true,
    default: true,
    type: Boolean
	});

  settings.register("dc20rpg", "mergeDamageTypes", {
    name: "Merge the same damage type to one Formula",
    hint: "If selected, damage/healing of the same type will be combined into one formula unless the formula itself states otherwise.",
    scope: "world",
    config: true,
    default: true,
    type: Boolean
	});

  settings.register("dc20rpg", "useMaxPrime", {
    name: "Prime Modifer Equals Attribute Limit",
    hint: "Variant Rule: If selected Attribute Limit will be used as Prime Modifier value.",
    scope: "world",
    config: true,
    default: false,
    type: Boolean
	});

  settings.register("dc20rpg", "outsideOfCombatRule", {
    name: "Use Outside of combat rules",
    hint: "If selected 'Outside of Combat' rules for AP, MP and SP will be respected. See 'Combat Resources' chapter.",
    scope: "world",
    config: true,
    default: false,
    type: Boolean
	});

  settings.register("dc20rpg", "selectedColor", {
    scope: "user",
    config: false,
    default: "default",
    type: String
  });

  settings.register("dc20rpg", "colorPaletteStore", {
    scope: "world",
    config: false,
    default: defaultColorPalette(),
    type: Object
  });

  settings.registerMenu("dc20rpg", "colorPaletteConfig", {
    name: "Select Color Palette",
    label: "Open Color Palette Selection",
    icon: "fas fa-palette",
    config: true,
    type: ColorSetting,
    restricted: false
  });

  settings.registerMenu("dc20rpg", "skillConfig", {
    name: "Customize Skill List",
    label: "Open Skill List Customization",
    icon: "fas fa-table-list",
    config: true,
    type: SkillConfiguration,
    restricted: true  
  });
}

function registerHandlebarsCreators() {

  Handlebars.registerHelper('data', (...params) => {
    const size = params.length - 1;
    let dataBindings = "";
    for(let i = 0; i < size; i=i+2) {
      dataBindings += `data-${params[i]}=${params[i+1]} `; 
    }
    return dataBindings;
  });
  
  Handlebars.registerHelper('small-button', (listener, icon, title, data) => {
    title = title ? `title="${title}"` : "";
    data = data || "";
    const component = `
    <a class="small-button ${listener}" ${title} ${data}>
      <i class="${icon}"></i>
    </a> 
    `;
    return component;
  });

  Handlebars.registerHelper('icon-printer-empty', (current, max, limit, fullClass, emptyClass, recolorValue, color) => {
    if (!recolorValue) recolorValue = 0;
    const fullPoint = `<i class="${fullClass}"></i>`;
    const emptyPoint = `<i class="${emptyClass}"></i>`;
    const coloredPoint = `<i class="${fullClass}" style="color:${color}"></i>`;

    if (max > limit) return `<b>${current}/${max}</b> ${fullPoint}`;

    let icons = "";
    for(let i = 0; i < max; i++) {
      if (i < recolorValue) icons += coloredPoint;
      else if (i < current) icons += fullPoint;
      else icons += emptyPoint;
    }
    return icons;
  });

  Handlebars.registerHelper('unique-item', (item, itemType, defaultName, defaultImg, level, editMode) => {
    let buttons = "";
    let hasItem = "empty";
    let dataItemId = '';
    let showTooltip = '';
    let missing = '';
    if (item) {
      dataItemId = `data-item-id="${item._id}" data-inside="true"`;
      defaultName = item.name;
      defaultImg = item.img;
      hasItem = "item editable";
      showTooltip = 'item-tooltip';

      if (editMode) {
        const editTooltip = game.i18n.localize('dc20rpg.sheet.editItem');
        const deleteTooltip = game.i18n.localize('dc20rpg.sheet.deleteItem');

        buttons = `
        <div class="item-buttons">
          <a class="item-edit fa-solid fa-edit" title="${editTooltip}" ${dataItemId}></a>
          <a class="item-delete fa-solid fa-trash" title="${deleteTooltip}" ${dataItemId}></a>
        </div>
        `;
      }
    }
    else {
      if (itemType === "subclass") missing = level >= 3 ? "missing" : "";
      else missing = "missing";
      const openCompendium = game.i18n.localize('dc20rpg.sheet.openCompendium');
      const mixAncestery = itemType === "ancestry" ? `
      <a class="mix-ancestry fa-solid fa-network-wired fa-lg" title="${game.i18n.localize('dc20rpg.sheet.mixAncestery')}"></a>
    ` : "";
      buttons = `
      <div class="item-buttons" style="border-left:0;">${mixAncestery}
        <a class="open-compendium fa-solid fa-book-atlas fa-lg" title="${openCompendium}" data-item-type="${itemType}"></a>
      </div>
      `;
    }

    const title = game.i18n.localize(`dc20rpg.sheet.${itemType}`);
    const component = `
    <div class="unique-item ${missing} ${itemType} ${hasItem} ${showTooltip}" title=${title} ${dataItemId}>
    <img class="item-image" src="${defaultImg}"/>
    <span class="item-name">${defaultName}</span>
    ${buttons}
    </div>
    `;
    return component;
  });

  Handlebars.registerHelper('unique-item-icon', (item, defaultName, defaultImg) => {
    let tooltip = "";
    if (item) {
      defaultName = `${defaultName}: ${item.name}`;
      defaultImg = item.img;
      tooltip = `class="item-tooltip" data-item-id="${item.id}"`;
    }

    const component = `
    <img src="${defaultImg}" title="${defaultName}" ${tooltip}/>
    `;
    return component;
  });

  Handlebars.registerHelper('show-hide-toggle', (flag, path, oneliner) => {
    let icon = flag ? "fa-eye-slash" : "fa-eye";
    if (oneliner === "true") icon = flag ? "fa-table" : "fa-table-list";
    return `<a class="activable fa-solid ${icon}" data-path="${path}"></a>`;
  });

  Handlebars.registerHelper('item-table', (editMode, items, navTab, weaponsOnActor) => {
    const partialPath = allPartials()["Item Table"];
    const template = Handlebars.partials[partialPath];
    if (template) {
      const context = {
        editMode: editMode,
        navTab: navTab,
        items: items,
        weaponsOnActor: weaponsOnActor
      };
      return new Handlebars.SafeString(template(context));
    }
    return '';
  });


  Handlebars.registerHelper('effects-table', (editMode, active, inactive, showInactiveEffects) => {
    const partialPath = allPartials()["Effects Table"];
    const template = Handlebars.partials[partialPath];
    if (template) {
      const context = {
        editMode: editMode,
        active: active,
        inactive: inactive,
        showInactiveEffects: showInactiveEffects,
      };
      return new Handlebars.SafeString(template(context));
    }
    return '';
  });

  Handlebars.registerHelper('traits-table', (editMode, active, inactive, type) => {
    const partialPath = allPartials()["Traits Table"];
    const template = Handlebars.partials[partialPath];
    if (template) {
      const context = {
        editMode: editMode,
        active: active,
        inactive: inactive,
        type: type,
      };
      return new Handlebars.SafeString(template(context));
    }
    return '';
  });

  Handlebars.registerHelper('grid-template', (navTab, isHeader, rollMenuRow) => {
    const headerOrder = isHeader  ? "35px" : '';

    if (navTab === "favorites" || navTab === "main" || navTab === "basic") {
      const rollMenuPart1 = rollMenuRow ? '' : "60px";
      const rollMenuPart2 = rollMenuRow ? "30px" : "40px";
      const enhNumber = rollMenuRow ? "35px" : "";
      return `grid-template-columns: ${headerOrder}${enhNumber} 1fr 90px ${rollMenuPart1} 70px ${rollMenuPart2};`;
    }
    if (rollMenuRow) {
      return `grid-template-columns: 35px 1fr 120px 70px 60px;`;
    }
    const inventoryTab = navTab === "inventory" ? "35px 40px" : '';
    const spellTab = navTab === "spells" ? "120px" : '';
    return `grid-template-columns: ${headerOrder} 1fr 120px ${spellTab}${inventoryTab} 60px 70px 70px 70px;`;
  });

  Handlebars.registerHelper('item-label', (sheetData) => {
    if (sheetData.type) {
      return `
      <div class="item-label">
        <div class="item-type">
          <span>${sheetData.type}</span>
        </div>
        <div class="item-subtype">
          <span>${sheetData.subtype}</span>
        </div>
      </div>
      `;
    }
    else {
      return `
      <div class="item-label">
        <div class="item-type">
         <span>${sheetData.fallbackType}</span>
        </div>
      </div>
      `;
    }
  });

  Handlebars.registerHelper('item-roll-details', (item, sheetData) => {
    const actionType = item.system.actionType;
    if (!actionType) return '';

    let content = '';
    let attackIcon = 'fa-question';
    const attackCheck = item.system.attackFormula.checkType;
    const attackRange = item.system.attackFormula.rangeType;
    if (attackCheck === "attack" && attackRange === "melee") attackIcon = 'fa-gavel';
    if (attackCheck === "attack" && attackRange === "ranged") attackIcon = 'fa-crosshairs';
    if (attackCheck === "spell" && attackRange === "melee") attackIcon = 'fa-hand-sparkles';
    if (attackCheck === "spell" && attackRange === "ranged") attackIcon = 'fa-wand-magic-sparkles';
    const rollMod = item.system.attackFormula.rollModifier > 0 ? `+${item.system.attackFormula.rollModifier}` : item.system.attackFormula.rollModifier;
    const check = item.system.check;
    const checkDC = check.againstDC && check.checkDC ? ` (DC ${check.checkDC})` : ""; 
    const checkType = getLabelFromKey(item.system.check.checkKey, CONFIG.DC20RPG.ROLL_KEYS.allChecks);

    switch (actionType) {    
      case "attack": 
        content += `<div class="wrapper" title="${game.i18n.localize('dc20rpg.item.sheet.header.attackMod')}"><i class="fa-solid ${attackIcon}"></i><p> ${rollMod}</p></div>`;
        break;

      case "check": 
        content += `<div class="wrapper" title="${game.i18n.localize('dc20rpg.item.sheet.header.check')}"><i class="fa-solid fa-user-check"></i><p> ${checkType}${checkDC}</p></div>`;
        break;
    }

    if (sheetData.damageFormula !== "") content += `<div class="wrapper" title="${game.i18n.localize('dc20rpg.item.sheet.header.damage')}"><i class="fa-solid fa-droplet"></i><p> ${sheetData.damageFormula}</p></div>`;
    if (sheetData.healingFormula !== "")content += `<div class="wrapper" title="${game.i18n.localize('dc20rpg.item.sheet.header.healing')}"><i class="fa-solid fa-heart"></i><p> ${sheetData.healingFormula}</p></div>`;
    if (sheetData.otherFormula !== "")content += `<div class="wrapper" title="${game.i18n.localize('dc20rpg.item.sheet.header.other')}"><i class="fa-solid fa-gear"></i><p> ${sheetData.otherFormula}</p></div>`;
    if (sheetData.saves !== "")content += `<div class="wrapper" title="${game.i18n.localize('dc20rpg.item.sheet.header.save')}"><i class="fa-solid fa-shield"></i><p> ${sheetData.saves}</p></div>`;
    if (sheetData.contests !== "")content += `<div class="wrapper" title="${game.i18n.localize('dc20rpg.item.sheet.header.contest')}"><i class="fa-solid fa-hand-back-fist"></i><p> ${game.i18n.localize('dc20rpg.rollType.contest')} ${sheetData.contests}</p></div>`;
    return content;
  });

  Handlebars.registerHelper('item-properties', (item) => {
    return itemDetailsToHtml(item);
  });

  Handlebars.registerHelper('charges-printer', (charges, source) => {
    if (!charges) return "";

    const icon = source === "self" ? "fa-bolt" : "fa-right-from-bracket";
    let component = "";
    for (let i = 0; i < charges; i++) {
      component += `<i class="fa-solid ${icon} cost-icon" title=${game.i18n.localize('dc20rpg.sheet.itemTable.charges')}></i>`;
    }
    return component;
  });

  Handlebars.registerHelper('cost-printer', (costs, mergeAmount, enh) => {
    if (!costs) return '';

    let component = '';
    const icons = {
      actionPoint: "ap fa-dice-d6",
      stamina: "sp fa-hand-fist",
      mana: "mp fa-star",
      health: "hp fa-heart",
      grit: "grit fa-clover",
      restPoints: "rest fa-campground"
    };
    if (typeof costs === 'number') return _printNonZero(costs, mergeAmount, icons["actionPoint"]);

    // Print core resources
    Object.entries(costs).forEach(([key, resCost]) => {
      const cost = resCost?.cost || resCost;
      switch (key) {
        case "custom": break;
        case "actionPoint":
          component += _printWithZero(cost, mergeAmount, icons[key]);
          break;
        default: 
          component += _printNonZero(cost, mergeAmount, icons[key]);
          break;
      }
    });

    if (!costs.custom) return component;
    // Print custom resources
    Object.values(costs.custom).forEach(resource => {
      component += _printImg(resource.value, mergeAmount, resource.img);
    });
    return component;
  });

  Handlebars.registerHelper('item-config', (item, editMode, tab) => {
    if (!item) return '';
    let component = '';

    // Configuration 
    if (editMode && tab !== "favorites") {
      component += `<a class="item-edit fas fa-edit" title="${game.i18n.localize('dc20rpg.sheet.items.editItem')}" data-item-id="${item._id}"></a>`;
      component += `<a class="item-copy fas fa-copy" title="${game.i18n.localize('dc20rpg.sheet.items.copyItem')}" data-item-id="${item._id}"></a>`;
      component += `<a class="item-delete fas fa-trash" title="${game.i18n.localize('dc20rpg.sheet.items.deleteItem')}" data-item-id="${item._id}"></a>`;
      return component;
    }

    // On Demand Item Macro
    const macros = item.system.macros;
    if (macros) {
      let onDemandTitle = "";
      let hasOnDemandMacro = false;
      for (const macro of Object.values(macros)) {
        if (macro.trigger === "onDemand" && !macro.disabled) {
          hasOnDemandMacro = true;
          if (onDemandTitle !== "") onDemandTitle += "\n";
          onDemandTitle += macro.title;
        }
      }
      if (hasOnDemandMacro) {
        component +=  `<a class="run-on-demand-macro fas fa-code" title="${onDemandTitle}" data-item-id="${item._id}"></a>`;
      }
    }

    // Activable Effects
    if (item.system.toggle?.toggleable) {
      const active = item.system.toggle.toggledOn ? 'fa-toggle-on' : 'fa-toggle-off';
      const title = item.system.toggle.toggledOn 
                  ? game.i18n.localize(`dc20rpg.sheet.itemTable.deactivateItem`)
                  : game.i18n.localize(`dc20rpg.sheet.itemTable.activateItem`);

      component += `<a class="item-activable fa-lg fa-solid ${active}" title="${title}" data-item-id="${item._id}" data-path="system.toggle.toggledOn" style="margin-top: 2px;"></a>`;
    }

    // Can be equipped/attuned
    const statuses = item.system.statuses;
    if (statuses) {
      const equipped = statuses.equipped ? 'fa-solid' : 'fa-regular';
      const equippedTitle = statuses.equipped 
                          ? game.i18n.localize(`dc20rpg.sheet.itemTable.unequipItem`)
                          : game.i18n.localize(`dc20rpg.sheet.itemTable.equipItem`);
      
      component += `<a class="item-activable ${equipped} fa-suitcase-rolling" title="${equippedTitle}" data-item-id="${item._id}" data-path="system.statuses.equipped"></a>`;

      if (item.system.properties.attunement.active) {
        const attuned = statuses.attuned ? 'fa-solid' : 'fa-regular';
        const attunedTitle = statuses.attuned 
                            ? game.i18n.localize(`dc20rpg.sheet.itemTable.unattuneItem`)
                            : game.i18n.localize(`dc20rpg.sheet.itemTable.attuneItem`);
        
        component += `<a class="item-activable ${attuned} fa-hat-wizard" title="${attunedTitle}" data-item-id="${item._id}" data-path="system.statuses.attuned"></a>`;
      }
    }
    if (tab === "favorites" || tab === "main") return component;

    // Favorites
    const isFavorite = item.flags.dc20rpg.favorite;
    const active = isFavorite ? 'fa-solid' : 'fa-regular';
    const title = isFavorite
                ? game.i18n.localize(`dc20rpg.sheet.itemTable.removeFavorite`)
                : game.i18n.localize(`dc20rpg.sheet.itemTable.addFavorite`);
    component += `<a class="item-activable ${active} fa-star" title="${title}" data-item-id="${item._id}" data-path="flags.dc20rpg.favorite"></a>`;

    // Known Toggle
    if (tab === "techniques" || tab === "spells") {
      const knownLimit = item.system.knownLimit;
      const active = knownLimit ? 'fa-solid' : 'fa-regular';
      const title = tab === "techniques" 
                    ? game.i18n.localize("dc20rpg.item.sheet.technique.countToLimitTitle")
                    : game.i18n.localize("dc20rpg.item.sheet.spell.countToLimitTitle");
      component += `<a class="item-activable ${active} fa-book" title="${title}" data-item-id="${item._id}" data-path="system.knownLimit"></a>`;  
    }

    return component;
  });

  Handlebars.registerHelper('components', (item) => {
    let component = '';

    // Components
    Object.entries(item.system.components).forEach(([key, cmp]) => {
      if (cmp.active) {
        let description = getLabelFromKey(key, CONFIG.DC20RPG.DROPDOWN_DATA.components);
        const letter = cmp.char;
        
        if (key === "material" && cmp.description) {
          const dsc = cmp.description ? `"${cmp.description}"` : "";
          const cost = cmp.cost ? ` (${cmp.cost} Gold)` : "";
          const consumes = cmp.consumed ? `<br>[${game.i18n.localize('dc20rpg.sheet.itemTable.consumeOnUse')}]` : "";
          description += `<br>${dsc}${cost}${consumes}`;
        }
        component += _descriptionChar(description, letter);
      }
    });

    const sustain = item.system.duration.type === "sustain";
    if (sustain) component += _descriptionIcon(getLabelFromKey("sustain", CONFIG.DC20RPG.DROPDOWN_DATA.durations), "fa-hand-holding-droplet");
    return component;
  });

  Handlebars.registerHelper('should-expand', (item, navTab) => {
    if (!["favorites", "main", "basic"].includes(navTab)) return 'expandable';

    let counter = 0;
    if (item.system.actionType === "dynamic") counter = 2;
    else counter = 1;

    const formulas = item.formulas;
    if (formulas) {
      let dmg = 0;
      let heal = 0;
      let other = 0;
      Object.values(formulas).forEach(formula => {
        switch(formula.category) {
          case "damage": dmg = 1; break;
          case "healing": heal = 1; break;
          case "other": other = 1; break;
        }
      });
      counter += (dmg + heal + other);
    }
    return counter > 2 ? 'expandable' : "";
  });

  Handlebars.registerHelper('action-type', (item) => {
    if (item.unidefined) return '';
    const system = item.system;
    switch (system.actionType) {
      case "attack": return _attack(system.attackFormula);
      case "check": return _check(system.check);
      default: return '';
    }
  });

  Handlebars.registerHelper('roll-requests', (item) => {
    if (item.unidefined) return '';
    const contests = [];
    const saves = [];

    const rollRequests = item.system.rollRequests;
    if (!rollRequests) return "";
    for (const request of Object.values(rollRequests)) {
      if (request.category === "save") saves.push(request);
      if (request.category === "contest") contests.push(request);
    }

    let component = "";
    if (saves.length > 0) component += _save(saves);
    if (contests.length > 0) component +=  _contest(contests);
    return component;
  });

  Handlebars.registerHelper('formula-rolls', (item) => {
    if (item.unidefined) return '';
    const formulas = item.formulas;
    if (!formulas) return '';

    const dmg = [];
    const heal = [];
    const other = [];
    Object.values(formulas).forEach(formula => {
      switch(formula.category) {
        case "damage": dmg.push(formula); break;
        case "healing": heal.push(formula); break;
        case "other": other.push(formula); break;
      }
    });
    let component = _formulas(dmg, "fa-droplet", CONFIG.DC20RPG.DROPDOWN_DATA.damageTypes);
    component += _formulas(heal, "fa-heart", CONFIG.DC20RPG.DROPDOWN_DATA.healingTypes);
    component += _formulas(other, "fa-gear", {});
    return component;
  });

  Handlebars.registerHelper('enhancement-mods', (enh) => {
    const mods = enh.modifications;
    let component = '';
    if (mods.addsNewRollRequest) {
      switch(mods.rollRequest.category) {
        case "save": component += _save([mods.rollRequest]); break;
        case "contest": component += _contest([mods.rollRequest]); break;
      }
    }
    if (mods.hasAdditionalFormula) {
      const description = `+${mods.additionalFormula} ${game.i18n.localize('dc20rpg.sheet.itemTable.additional')}`;
      let char = mods.additionalFormula.replace(" ", "");
      if (!(char.includes("+") || char.includes("-"))) char = `+${char}`;
      component += _descriptionChar(description, `${char}`);
    }
    if (mods.modifiesCoreFormula) {
      const description = `${mods.coreFormulaModification} ${game.i18n.localize('dc20rpg.sheet.itemTable.coreFormulaModification')}`;
      component += _descriptionIcon(description, "fa-dice");
    }
    if (mods.overrideTargetDefence) {
      const description = `${game.i18n.localize('dc20rpg.sheet.itemTable.overrideTargetDefence')}<br><b>${getLabelFromKey(mods.targetDefenceType, CONFIG.DC20RPG.DROPDOWN_DATA.defences)}</b>`;
      component += _descriptionIcon(description, "fa-share");
    }
    if (mods.overrideDamageType) {
      const description = `${game.i18n.localize('dc20rpg.sheet.itemTable.changeDamageType')} <b>${getLabelFromKey(mods.damageType, CONFIG.DC20RPG.DROPDOWN_DATA.damageTypes)}</b>`;
      component += _descriptionIcon(description, "fa-fire");
    }
    if (mods.addsNewFormula) {
      switch(mods.formula.category) {
        case "damage": component += _formulas([mods.formula], "fa-droplet", CONFIG.DC20RPG.DROPDOWN_DATA.damageTypes); break;
        case "healing": component += _formulas([mods.formula], "fa-heart", CONFIG.DC20RPG.DROPDOWN_DATA.healingTypes); break;
      }
    }
    return component;
  });
}

function _printWithZero(cost, mergeAmount, icon) {
  if (cost === undefined) return '';
  if (cost === 0) return `<i class="${icon} fa-light cost-icon"></i>`;
  const costIconHtml = cost < 0 ? `<i class="${icon} fa-solid cost-icon">+</i>` : `<i class="${icon} fa-solid cost-icon"></i>`;
  return _print(Math.abs(cost), mergeAmount, costIconHtml);
}

function _printNonZero(cost, mergeAmount, icon) {
  if (!cost) return '';
  const costIconHtml = cost < 0 ? `<i class="${icon} fa-solid cost-icon">+</i>` : `<i class="${icon} fa-solid cost-icon"></i>`;
  return _print(Math.abs(cost), mergeAmount, costIconHtml);
}

function _printImg(cost, mergeAmount, iconPath) {
  if (!cost) return '';
  const costImg = cost < 0 ? `<img src=${iconPath} class="cost-img">+` : `<img src=${iconPath} class="cost-img">`;
  return _print(Math.abs(cost), mergeAmount, costImg);
}

function _print(cost, mergeAmount, costIconHtml) {
  if (mergeAmount > 4 && cost > 1) return `<b>${cost}x</b>${costIconHtml}`;
  let pointsPrinter = "";
  for (let i = 1; i <= cost; i ++) pointsPrinter += costIconHtml;
  return pointsPrinter;
}

function _attack(attack) {
  let icon = "fa-question";
  if (attack.checkType === "attack" && attack.rangeType === "melee") icon = 'fa-gavel';
  if (attack.checkType === "attack" && attack.rangeType === "ranged") icon = 'fa-crosshairs';
  if (attack.checkType === "spell" && attack.rangeType === "melee") icon = 'fa-hand-sparkles';
  if (attack.checkType === "spell" && attack.rangeType === "ranged") icon = 'fa-wand-magic-sparkles';
  const description = `${getLabelFromKey(attack.checkType + attack.rangeType, CONFIG.DC20RPG.DROPDOWN_DATA.checkRangeType)}<br>vs<br>${getLabelFromKey(attack.targetDefence, CONFIG.DC20RPG.DROPDOWN_DATA.defences)}`;
  return _descriptionIcon(description, icon);
}

function _save(saves) {
  let description = "";
  for(let i = 0; i < saves.length; i++) {
    description += `DC ${saves[i].dc} <b>${getLabelFromKey(saves[i].saveKey, CONFIG.DC20RPG.ROLL_KEYS.saveTypes)}</b>`;
    if (i !== saves.length - 1) description += "<br>or ";
  }
  return _descriptionIcon(description, 'fa-shield');
}

function _check(check) {
  const checkDC = (check.againstDC && check.checkDC) ? `DC ${check.checkDC} ` : "";
  const description = `${checkDC}<b>${getLabelFromKey(check.checkKey, CONFIG.DC20RPG.ROLL_KEYS.allChecks)}</b>`;
  return _descriptionIcon(description, 'fa-user-check');
}

function _contest(contests) {
  let description = "";
  for(let i = 0; i < contests.length; i++) {
    if (i === 0) description += game.i18n.localize('dc20rpg.rollType.contest') + ":<br>";
    description += `<b>${getLabelFromKey(contests[i].contestedKey, CONFIG.DC20RPG.ROLL_KEYS.contests)}</b>`;
    if (i !== contests.length - 1) description += "<br>or ";
  }
  return _descriptionIcon(description, 'fa-hand-back-fist');
}

function _formulas(formulas, icon, types) {
  if (formulas.length <= 0) return '';
  let description = '';
  for(let i = 0; i < formulas.length; i++) {
    if (i !== 0) description += '<br>+ ';
    const type = getLabelFromKey(formulas[i].type, types);
    const value = formulas[i].formula;
    description += `${value} ${type}`;
  }
  return _descriptionIcon(description, icon);
}

function _descriptionIcon(description, icon) {
  return `
  <div class="description-icon" title="">
    <div class="letter-circle-icon">
      <i class="fa-solid ${icon}"></i>
    </div>
    <div class="description">
      <div class="description-wrapper"> 
        <span>${description}</span>
      </div>
    </div>
  </div>
  `
}

function _descriptionChar(description, char) {
  return `
  <div class="description-icon" title="">
    <div class="letter-circle-icon">
      <span class="char">${char}</span>
    </div>
    <div class="description">
      <div class="description-wrapper"> 
        <span>${description}</span>
      </div>
    </div>
  </div>
  `
}

/**
 * Extend the base ActiveEffect class to implement system-specific logic.
 */
class DC20RpgActiveEffect extends ActiveEffect {

  get roundsLeft() {
    const useCounter = this.flags.dc20rpg?.duration?.useCounter;
    const activeCombat = game.combats.active;
    if (useCounter && activeCombat) {
      const duration = this.duration;
      const beforeTurn = duration.startTurn > activeCombat.turn ? 1 : 0;
      const roundsLeft = duration.rounds + duration.startRound + beforeTurn - activeCombat.round;
      return roundsLeft;
    }
    else {
      return null;
    }
  }

  get isLinkedToItem() {
    if (!this.transfer) return false;
    const item = this.getSourceItem();
    if (!item) return false;
    const effectConfig = item.system.effectsConfig;
    if (!effectConfig) return false;

    if (item.system.toggle?.toggleable) return effectConfig.linkWithToggle;
    else return effectConfig.mustEquip;
  }

  get stateChangeLocked() {
    if (!this.transfer) return false;
    const item = this.getSourceItem();
    if (!item) return false;
    const effectConfig = item.system.effectsConfig;
    if (!effectConfig) return false;

    const toggleable = item.system.toggle?.toggleable;
    if (toggleable && effectConfig.linkWithToggle && !effectConfig.toggleItem) return true;
    if (toggleable && effectConfig.linkWithToggle && effectConfig.toggleItem) return false;
    return effectConfig.mustEquip
  }

  async disable({ignoreStateChangeLock}={}) {
    if (this.disabled) return;
    if (this.isLinkedToItem) {
      if (this.stateChangeLocked && !ignoreStateChangeLock) {
        ui.notifications.error(`Effect '${this.name}' is linked to the item named '${this.getSourceItem().name}'. You need to change the state of the connected item`);
        return;
      }
      else {
        const parentItem = this.getSourceItem();
        await parentItem.update({["system.toggle.toggledOn"]: false});
      }
    }
    await this.update({disabled: true});
    const actor = this.getOwningActor();
    if (actor) {
      await runEventsFor("effectDisabled", actor, effectEventsFilters(this.name, this.statuses, this.flags.dc20rpg?.effectKey), {effectDisabled: this});
      await reenableEventsOn("effectDisabled", actor, effectEventsFilters(this.name, this.statuses, this.flags.dc20rpg?.effectKey));
    }
  }

  async enable({dontUpdateTimer, ignoreStateChangeLock}={}) {
    if (!this.disabled) return;
    if (this.isLinkedToItem) {
      if (this.stateChangeLocked && !ignoreStateChangeLock) {
        ui.notifications.error(`Effect '${this.name}' is linked to the item named '${this.getSourceItem().name}'. You need to change the state of the connected item`);
        return;
      }
      else {
        const parentItem = this.getSourceItem();
        await parentItem.update({["system.toggle.toggledOn"]: true});
      }
    }

    const updateData = {disabled: false};
    // Check If we should use round counter
    const duration = this.flags.dc20rpg?.duration;
    if (duration?.useCounter && duration?.resetWhenEnabled && !dontUpdateTimer) {
      const initial =  this.constructor.getInitialDuration();
      updateData.duration = initial.duration;
    }
    await this.update(updateData);
    const actor = this.getOwningActor();
    if (actor) {
      await runEventsFor("effectEnabled", actor, effectEventsFilters(this.name, this.statuses, this.flags.dc20rpg?.effectKey), {effectEnabled: this});
      await reenableEventsOn("effectEnabled", actor, effectEventsFilters(this.name, this.statuses, this.flags.dc20rpg?.effectKey));
    }
  }

  /**@override */
  apply(actor, change) {
    this._injectEffectIdToChange(change);
    super.apply(actor, change);
  }

  _injectEffectIdToChange(change) {
    const effect = change.effect;
    if (!effect) return;

    // We want to inject effect id only for events and roll levels
    if (change.key.includes("system.events") || change.key.includes("system.rollLevel")) {
      change.value = `"effectId": "${effect.id}", ` + change.value;
    }
  }

  /**@override */
  static async fromStatusEffect(statusId, options={}) {
    const effect = await super.fromStatusEffect(statusId, options);
    return effect;
  }

  /**
   * Returns item that is the source of that effect. If item isn't the source it will return null;
   */
  getSourceItem() {
    if (this.parent.documentName === "Item") {
      return this.parent;
    }
    return null;
  }
  
  getOwningActor() {
    if (this.parent.documentName === "Item") {
      return this.parent.actor;
    }
    if (this.parent.documentName === "Actor") {
      return this.parent;
    }
    return null;
  }

  // If we are removing a status from effect we need to run check 
  async _preUpdate(changed, options, user) {
    this._runStatusChangeCheck(changed);
    super._preUpdate(changed, options, user);
  }

  async _preCreate(data, options, user) {
    if (this.parent.documentName === "Actor") {
      await runEventsFor("effectApplied", this.parent, effectEventsFilters(this.name, this.statuses, this.flags.dc20rpg?.effectKey), {createdEffect: this});
      await reenableEventsOn("effectApplied", this.parent, effectEventsFilters(this.name, this.statuses, this.flags.dc20rpg?.effectKey));
      if (this.preventCreation) return false;
    }
    this._runStatusChangeCheck(data);
    super._preCreate(data, options, user);
  }

  async _preDelete(options, user) {
    if (this.parent.documentName === "Actor") {
      await runEventsFor("effectRemoved", this.parent, effectEventsFilters(this.name, this.statuses, this.flags.dc20rpg?.effectKey), {removedEffect: this});
      await reenableEventsOn("effectRemoved", this.parent, effectEventsFilters(this.name, this.statuses, this.flags.dc20rpg?.effectKey));
      if (this.preventRemoval) return false;
    }
    return await super._preDelete(options, user);
  }

  _onCreate(data, options, userId) {
    super._onCreate(data, options, userId);
    if (userId === game.user.id) {
      // FORGE BUG FIX: For some reason Forge hosting does not update turn and round by default so we need to do it manually 
      if (data.duration.startTime === null) {
        this.update(this.constructor.getInitialDuration());
      }
      runInstantEvents(this, this.parent);
    }
  }

  _runStatusChangeCheck(updateData) {
    const newStatusId = updateData.system?.statusId;
    const oldStatusId = this.system?.statusId;
    if (newStatusId === undefined) return;
    if (newStatusId === oldStatusId) return;

    // remove old changes
    if(oldStatusId) {
      const oldStatus = CONFIG.statusEffects.find(e => e.id === oldStatusId);
      if (oldStatus) {
        const newChanges = [];
        updateData.changes.forEach(change => {
          if (!this.isChangeFromStatus(change, oldStatus)) newChanges.push(change);
        });
        updateData.changes = newChanges;
      }
    }
    // add new changes
    const newStatus = CONFIG.statusEffects.find(e => e.id === newStatusId);
    if (newStatus) updateData.changes = updateData.changes.concat(newStatus.changes);
  }

  _statusDif(current, updated) {
    return {
      toAdd: new Set(updated).difference(current),
      toRemove: current.difference(new Set(updated))
    }
  }

  isChangeFromStatus(change, status) {
    let hasChange = false;
    status.changes.forEach(statusChange => {
      if (statusChange.key === change.key && 
          statusChange.value === change.value && 
          statusChange.mode === change.mode) {
            hasChange = true;
          }
    });
    return hasChange;
  }

  async respectRoundCounter() {
    const durationFlag = this.flags.dc20rpg?.duration;
    if (!durationFlag) return;
    if (!durationFlag.useCounter) return;
    if (this.roundsLeft === null) return;
    if (this.roundsLeft > 0) return;

    const onTimeEnd = durationFlag.onTimeEnd;
    if (!onTimeEnd) return;

    if (onTimeEnd === "disable") await this.disable();
    if (onTimeEnd === "delete") {
      sendEffectRemovedMessage(this.parent, this);
      await this.delete();
    }
  }
}

class DC20RpgTokenHUD extends TokenHUD {

  get template() {
    return `systems/dc20rpg/templates/hud/token-hud.hbs`;
  }

  /** @overload */
  getData(options={}) {
    let data = super.getData(options);
    this.oldDisplay = this.document.displayBars;
    this.document.displayBars = 40;
    data.actionPoints = this._prepareActionPoints();
    data.statusEffects = this._prepareStatusEffects(data.statusEffects);
    data.movePoints = this.actor.system.movePoints;
    return data;
  }

  clear() {
    if(this.oldDisplay) {
      this.document.displayBars = this.oldDisplay;
      this.oldDisplay = undefined;
    }
    super.clear();
  }

  activateListeners(html) {
    super.activateListeners(html);
    const actor = this.actor;
    if (!actor) return;

    html.find(".effect-control").mousedown(ev => toggleStatusOn(datasetOf(ev).statusId, actor, ev.which));
    html.find(".effect-control").click(ev => {ev.preventDefault(); ev.stopPropagation();});         // remove default behaviour
    html.find(".effect-control").contextmenu(ev => {ev.preventDefault(); ev.stopPropagation();});   // remove default behaviour

    // Ap Spend/Regain
    html.find(".regain-ap").click(() => regainBasicResource("ap", actor, 1, true));
    html.find(".spend-ap").click(() => subtractAP(actor, 1));

    // Move Points
    html.find(".move-points").change(ev => this._onMovePointsChange(valueOf(ev), actor));
    html.find(".move-icon").mousedown(ev => this._onMoveAction(actor, ev.which === 1));
  }

  async _onMoveAction(actor, simple) {
    const subtracted = await subtractAP(actor, 1);
    if (!subtracted) return;

    if (simple) makeMoveAction(actor);
    else {
      const key = await getSimplePopup("select", {
          selectOptions: CONFIG.DC20RPG.DROPDOWN_DATA.moveTypes, 
          header: game.i18n.localize("dc20rpg.dialog.movementType.title"), 
          preselect: "ground"
        });
      if (key) makeMoveAction(actor, {moveType: key});
    }
  }

  _onMovePointsChange(newValue, actor) {
    let isDelta = false;
    let add = false;
    if (newValue.startsWith("+")) {
      isDelta = true;
      add = true;
    }
    if (newValue.startsWith("-")) {
      isDelta = true;
      add = false;
    }

    let movePoints = parseFloat(newValue);
    if (isNaN(movePoints)) return;
    else movePoints = Math.abs(movePoints);

    if(isDelta) {
      const currentMovePoints = actor.system.movePoints || 0;
      const newMovePoints = add ? currentMovePoints + movePoints : currentMovePoints - movePoints;
      actor.update({["system.movePoints"]: Math.max(newMovePoints, 0)});
    }
    else {
      actor.update({["system.movePoints"]: movePoints});
    }
  }

  _prepareActionPoints() {
    const actionPoints = this.actor.system.resources.ap;
    if (!actionPoints) return;

    return {
      value: actionPoints.value, 
      max: actionPoints.max
    }
  }

  _prepareStatusEffects(statusEffects) {
    const actorStatuses = this.actor?.statuses || [];
    actorStatuses.forEach(status => {
      const statEff = statusEffects[status.id];
      if (!statEff.stack) statEff.stack = status.stack;
      else statEff.stack += status.stack;

      statEff.stackable = CONFIG.statusEffects.find(e => e.id === status.id)?.stackable;
      if (statEff.stack > 0) {
        // This means that status comes from some other effect
        if (!statEff.isActive) {
          statEff.isActive = true;
          statEff.fromOther = true;
        }
        statEff.isActive = true;
        statEff.cssClass = "active";
      } 
    });

    // Filter out hidden statuses (We dont want to show them in the UI)
    return Object.fromEntries(
      Object.entries(statusEffects).filter(([key, status]) => {
        const original = CONFIG.statusEffects.find(e => e.id === status.id);
        if (original?.system?.hide) return false;
        return true;
      })
    );
  }

  //NEW UPDATE CHECK: We need to make sure it works fine with future foundry updates
  _getStatusEffectChoices() {

    // Include all HUD-enabled status effects
    const choices = {};
    for ( const status of CONFIG.statusEffects ) {
      if ( (status.hud === false) || ((foundry.utils.getType(status.hud) === "Object")
        && (status.hud.actorTypes?.includes(this.document.actor.type) === false)) ) {
        continue;
      }
      choices[status.id] = {
        _id: status._id,
        id: status.id,
        title: game.i18n.localize(status.name),
        src: status.img,
        isActive: false,
        isOverlay: false
      };
    }

    // Update the status of effects which are active for the token actor
    const activeEffects = this.actor?.appliedEffects || [];
    for ( const effect of activeEffects ) {
      if (effect.disabled) continue;
      for ( const statusId of effect.statuses ) {
        const status = choices[statusId];
        if (effect.sourceName === "None") status.fromOther = false;
        if ( !status ) continue;
        if ( status._id ) {
          if ( status._id !== effect.id ) continue;
        } else {
          if ( effect.statuses.size !== 1 ) continue;
        }
        if (effect.sourceName !== "None" && status.fromOther === undefined) status.fromOther = true;
        status.isActive = true;
        if ( effect.getFlag("core", "overlay") ) status.isOverlay = true;
        break;
      }
    }

    // Flag status CSS class
    for ( const status of Object.values(choices) ) {
      status.cssClass = [
        status.isActive ? "active" : null,
        status.isOverlay ? "overlay" : null
      ].filterJoin(" ");
    }
    return choices;
  }
}

class DC20RpgToken extends Token {

  get isFlanked() {
    if (this.actor.system.globalModifier.ignore.flanking) return;
    if (!game.settings.get("dc20rpg", "enablePositionCheck")) return;
    const neutralDispositionIdentity = game.settings.get("dc20rpg", "neutralDispositionIdentity");
    const coreDisposition = [this.document.disposition];
    if (neutralDispositionIdentity === "friendly" && coreDisposition[0] === 1) coreDisposition.push(0);
    if (neutralDispositionIdentity === "hostile" && coreDisposition[0] === -1) coreDisposition.push(0);

    const neighbours = this.neighbours;
    for (let [key, token] of neighbours) {
      // Prone/Incapacitated tokens cannot flank
      if (token.actor.hasAnyStatus(["incapacitated", "prone", "dead"])) neighbours.delete(key);
      if (coreDisposition.includes(token.document.disposition)) neighbours.delete(key);
    }
    if (neighbours.size <= 1) return false;

    for (const [id, neighbour] of neighbours) {
      // To check if token is flankig we need to see if at least one neighbour of
      // the core token is not also a neighbour of supposedly flanking token
      const coreNeighbours = new Map(neighbours);
      coreNeighbours.delete(id); // We want to skip ourself

      const tokenNeighbours = neighbour.neighbours;
      let mathingNeighbours = 0;
      for (let [key, token] of tokenNeighbours) {
        if (token.actor.hasAnyStatus(["incapacitated", "prone", "dead"])) continue; // Prone/Incapacitated tokens cannot help with flanking
        if (key === this.id) continue; // We want to skip core token
        if (coreDisposition.includes(token.document.disposition)) continue; // Tokens of the same disposition shouldn't flank themself - most likely allies
        if (coreNeighbours.has(key)) mathingNeighbours++;
      }
      if (mathingNeighbours !== coreNeighbours.size) {
        return true;
      }
    }
    return false;
  }

  get enemyNeighbours() {
    const neutralDispositionIdentity = game.settings.get("dc20rpg", "neutralDispositionIdentity");
    const coreDisposition = [this.document.disposition];
    if (neutralDispositionIdentity === "friendly" && coreDisposition[0] === 1) coreDisposition.push(0);
    if (neutralDispositionIdentity === "hostile" && coreDisposition[0] === -1) coreDisposition.push(0);

    const neighbours = this.neighbours;
    for (let [key, token] of neighbours) {
      if (coreDisposition.includes(token.document.disposition)) neighbours.delete(key);
    }
    return neighbours;
  }

  get neighbours() {
    const tokens = canvas.tokens.placeables;
    const neighbours = new Map();
    if (canvas.grid.isGridless) {
      const rangeArea = getRangeAreaAroundGridlessToken(this, 0.5);
      for (const token of tokens) {
        const pointsToContain = getGridlessTokenPoints(token);
        let isNeighbour = false;
        for (const point of pointsToContain) {
          if (isPointInSquare(point.x, point.y, rangeArea)) isNeighbour = true;
        }
        if (isNeighbour) neighbours.set(token.id, token);
      }
    }
    else {
      const neighbouringSpaces = this.getNeighbouringSpaces();
      for (const token of tokens) {
        const tokenSpaces = token.getOccupiedGridSpacesMap();
        let isNeighbour = false;
        tokenSpaces.keys().forEach(key => {
          if(neighbouringSpaces.has(key)) isNeighbour = true;
        });
        if (isNeighbour) neighbours.set(token.id, token);
      }
    }
    return neighbours;
  }

  get adjustedHitArea() {
    const hitArea = this.hitArea;
    let points = [];
    // Hex grid
    if (hitArea.type === 0) points = hitArea.points;
    // Square grid
    if (hitArea.type === 1) {
      points = [
        0, 0,
        hitArea.width, 0,
        0, hitArea.height,
        hitArea.width, hitArea.height
      ];
    }

    const area = [];
    for(let i = 0; i < points.length; i += 2) {
      const p = {
        x: this.x + points[i],
        y: this.y + points[i+1]
      };
      area.push(p);
    }
    return area;
  }

  /** @override */
  //NEW UPDATE CHECK: We need to make sure it works fine with future foundry updates
  async _drawEffects() {
    this.effects.renderable = false;

    // Clear Effects Container
    this.effects.removeChildren().forEach(c => c.destroy());
    this.effects.bg = this.effects.addChild(new PIXI.Graphics());
    this.effects.overlay = null;

    // Categorize effects
    const activeEffects = this.actor?.temporaryEffects || [];
    let hasOverlay = false;

    // Collect from active statuses 
    const statuses = this.actor?.statuses;
    if (statuses) {
      statuses.forEach(st => {
        const status = CONFIG.statusEffects.find(e => e.id === st.id);
        if (status) {
          status.tint = new Number(16777215);
          activeEffects.push(status);
        }
      });
    }

    // Flatten the same active effect images
    const flattenedImages = [];
    const uniqueImages = [];
    activeEffects.forEach(effect => {
      const flattened = {img: effect.img, tint: effect.tint};
      if (uniqueImages.indexOf(effect.img) === -1) {
        flattenedImages.push(flattened);
        uniqueImages.push(effect.img);
      }
    });

    // Draw effects
    const promises = [];
    for ( const effect of flattenedImages ) {
      if ( !effect.img ) continue;
      if ( effect.flags && effect.getFlag("core", "overlay") && !hasOverlay ) {
        promises.push(this._drawOverlay(effect.img, effect.tint));
        hasOverlay = true;
      }
      else promises.push(this._drawEffect(effect.img, effect.tint));
    }
    await Promise.allSettled(promises);

    this.effects.renderable = true;
    this.renderFlags.set({refreshEffects: true});
  }

  async _draw(options) {
    await super._draw(options);

    // Add apDisplay bellow bars
    const bars = this.bars;
    bars.apDisplay = bars.addChild(new PIXI.Container());
    bars.apDisplay.width = this.getSize().width;
  }

  /** @override */
  drawBars() {
    super.drawBars();
    this._drawApDisplay();
  }

  async _drawApDisplay() {
    if ( !this.actor || (this.document.displayBars === CONST.TOKEN_DISPLAY_MODES.NONE) ) return;
    const actionPoints = this.actor.system.resources.ap;
    if (!actionPoints) return;

    const max = actionPoints.max;
    const current = actionPoints.value;
    const full = await loadTexture('systems/dc20rpg/images/sheet/header/ap-full.svg');
    const empty = await loadTexture('systems/dc20rpg/images/sheet/header/ap-empty.svg');

    const {width, height} = this.getSize();
    const step = width / max;
    const shift = step/2;

    const tokenX = this.document.height;
    const gridX = canvas.grid.sizeX;
    const size = tokenX * (0.7 * gridX)/max;
    
    this.bars.apDisplay.removeChildren();
    for(let i = 0; i < max; i++) {
      if (i < current) this._addIcon(full, i, size, height, step, shift, max); 
      else this._addIcon(empty, i, size, height, step, shift, max); 
    }
  }

  _addIcon(texture, index, size, height, step, shift, max) {
    const bottomBarHeight = this.bars.bar1.height;

    let distanceFromTheMiddle = 0;
    if (max % 2 === 0) {
      const middleOver = Math.ceil(max/2);
      const middleUnder = Math.floor(max/2);
      if (index+1 < middleUnder) {
        distanceFromTheMiddle = Math.abs(middleUnder - index - 1);
      }
      else if (index+1 > middleOver) {
        distanceFromTheMiddle = Math.abs(middleOver - index);
      }
    }
    else {
      const middle = Math.ceil(max/2);
      distanceFromTheMiddle = Math.abs(middle - index - 1);
    }

    const icon = new PIXI.Sprite(texture);
    icon.width = size;
    icon.height = size;
    icon.x = (shift - (size/2)) + (step * index);
    icon.y = height - bottomBarHeight - size - (0.5 * distanceFromTheMiddle * size);
    this.bars.apDisplay.addChild(icon);
  }

  getNeighbouringSpaces() {
    const occupiedSpaces = this.getOccupiedGridSpacesMap();
    const adjacents = new Map();
    for (const space of occupiedSpaces.values()) {
      this.#adjacentSpacesFor(space).forEach(adjSpace => {
        const key = `i#${adjSpace.i}_j#${adjSpace.j}`; 
        if (!adjacents.has(key) && !occupiedSpaces.has(key)) {
          adjacents.set(key, adjSpace);
        }
      });
    }
    return adjacents;
  }

  #adjacentSpacesFor(space) {
    const grid = canvas.grid;
    if (grid.isSquare) return grid.getAdjacentOffsets(space);
    if (grid.isHexagonal) {
      if (grid.columns) {
        let d = 1;
        const spaceEven = (space.i % 2 === 0);
        if (grid.even) d = spaceEven ? 1 : -1;
        else d = spaceEven ? -1 : 1;
        return [
          {i: space.i, j: space.j + 1},
          {i: space.i, j: space.j - 1},
          {i: space.i + 1, j: space.j},
          {i: space.i - 1, j: space.j},
          {i: space.i + 1, j: space.j + d},
          {i: space.i - 1, j: space.j + d},
        ]
      }
      else {
        let d = 1;
        const spaceEven = (space.j % 2 === 0);
        if (grid.even) d = spaceEven ? 1 : -1;
        else d = spaceEven ? -1 : 1;
        return [
          {i: space.i + 1, j: space.j},
          {i: space.i - 1, j: space.j},
          {i: space.i, j: space.j + 1},
          {i: space.i, j: space.j - 1},
          {i: space.i + d, j: space.j + 1},
          {i: space.i + d, j: space.j - 1},
        ]
      }
    }
    return [];
  }

  getOccupiedGridSpacesMap() {
    return new Map(this.getOccupiedGridSpaces().map(occupied => [`i#${occupied[0]}_j#${occupied[1]}`, {i: occupied[0], j: occupied[1]}]));
  }

  getOccupiedGridSpaces() {
    // Gridless - no spaces to occupy
    if (canvas.grid.isGridless) return [];

    // Square
    if (canvas.grid.isSquare) {
      const tokenWidth = this.document.width;   // Width in spaces
      const tokenHeight = this.document.height; // Height in spaces
      const range = canvas.grid.getOffsetRange(this);
      const startX = range[1];
      const startY = range[0];

      // We need to move the layout to the starting position of the token
      const occupiedSpaces = [];
      for (let i = 0; i < tokenWidth; i++) {
        for (let j = 0; j < tokenHeight; j++) {
          const x = startX + i;
          const y = startY + j;
          occupiedSpaces.push([x, y]);
        }
      }
      return occupiedSpaces;
    }
    // Hex
    else if (canvas.grid.isHexagonal) {
      // For Hex we want to collect more spaces and then check which
      // belong to token hitArea, we do that because hex tokens have
      // more irregular shapes
     
      const startCordX = this.document.x; // X cord of token
      const startCordY = this.document.y; // Y cord of token

      // We convert hit area to polygon so we will be able to check which spaces belong to it
      const points = this.hitArea.points;
      const polygon = [];
      const borderPoints = new Map();
      const rowOriented = canvas.grid.type === CONST.GRID_TYPES.HEXEVENR || canvas.grid.type === CONST.GRID_TYPES.HEXODDR;
      for (let i = 0; i < points.length; i=i+2) {
        const x = startCordX + points[i];
        const y = startCordY + points[i+1];
        polygon.push({x: x, y: y});

        // We also want to collect center points from nereby hexes
        const center = canvas.grid.getCenterPoint({x: x, y: y});
        this.#prepareBorderPoints(center, borderPoints, rowOriented);
      }
      const layout = this.#prepareHexLayout(borderPoints, rowOriented);

      // We check if center points belong to 
      const occupiedSpaces = [];
      for (let i = 0; i < layout.length; i++) {
        const centerX = layout[i].x;
        const centerY = layout[i].y;

        const topLeft = canvas.grid.getTopLeftPoint(layout[i]);
        const [y, x] = canvas.grid.getOffsetRange(topLeft);
        if (isPointInPolygon(centerX, centerY, polygon)) {
          occupiedSpaces.push([x, y]);
        }
      }
      return occupiedSpaces;
    }
    // Unsupported grid type
    else {
      ui.notifications.error("Unsupported grid type");
      return [];
    }
  }

  #prepareBorderPoints(center, borderPoints, rowOriented) {
    const mainCord = rowOriented ? "x" : "y";
    const otherCord = rowOriented ? "y" : "x";

    const key = Math.floor(center[otherCord]); // Get rid of rounding issues
    const borderPoint = borderPoints.get(key);
    if (borderPoint) {
      const first = borderPoint.first > center[mainCord] ? center[mainCord] : borderPoint.first;
      const last = borderPoint.last < center[mainCord] ?  center[mainCord] : borderPoint.last;
      borderPoints.set(key, {
        first: first,
        last: last,
        otherPoint: center[otherCord]
      });
    }
    else {
      borderPoints.set(key, {
        first: center[mainCord],
        last: center[mainCord],
        otherPoint: center[otherCord]
      });
    }
  }

  #prepareHexLayout(borderPoints, rowOriented) {
    const sizeKey = rowOriented ? "sizeX" : "sizeY";
    const mainCord = rowOriented ? "x" : "y";
    const otherCord = rowOriented ? "y" : "x";

    const layout = [];
    const gridSize = canvas.grid[sizeKey];
    borderPoints.forEach(point => {
      const otherPoint = point.otherPoint;
      const first = point.first;
      const last = point.last;

      let nextCenter = first; // We start with first center point
      while(last - nextCenter > gridSize/4) {
        layout.push({[mainCord]: nextCenter, [otherCord]: otherPoint});
        nextCenter += gridSize;
      }
      layout.push({[mainCord]: last, [otherCord]: otherPoint});
    });
    return layout;
  }

  isTokenInRange(tokenToCheck, range) {
    if (canvas.grid.isGridless) return this._isTokenInRangeGridless(tokenToCheck, range);
    return this._isTokenInRangeGrid(tokenToCheck, range);
  }

  _isTokenInRangeGridless(tokenToCheck, range) {
    const rangeArea = getRangeAreaAroundGridlessToken(this, range);
    const pointsToContain = getGridlessTokenPoints(tokenToCheck);
    for (const point of pointsToContain) {
      if (isPointInSquare(point.x, point.y, rangeArea)) return true;
    }
    return false;
  }

  _isTokenInRangeGrid(tokenToCheck, range) {
    const fromArea = this.adjustedHitArea;
    const toArea = tokenToCheck.adjustedHitArea;

    let shortestDistance = 999;
    for (let from of fromArea) {
      for (let to of toArea) {
        const distance = Math.round(canvas.grid.measurePath([from, to]).distance); 
        if (shortestDistance > distance) shortestDistance = distance;
      }
    }
    return shortestDistance < range;
  }
}

class SystemsBuilder extends Dialog {

  constructor(type, currentValue, specificSkill, dialogData = {}, options = {}) {
    super(dialogData, options);
    this.type = type;
    this.specificSkill = specificSkill;
    this._prepareData(type, currentValue);
  }

  static get defaultOptions() {
    return foundry.utils.mergeObject(super.defaultOptions, {
      template: "systems/dc20rpg/templates/dialogs/systems-builder.hbs",
      classes: ["dc20rpg", "dialog", "flex-dialog"]
    });
  }

  _prepareData(type, stringFormatValue) {
    // Arrays also have ',' we need to cut those nad put those back later
    const arrayRegex = /\[[^\]]*\]/g; 
    const arrayHolder = {};
    let counter = 0;

    stringFormatValue = stringFormatValue.replace(arrayRegex, (match) => {
      const placeholder = `ARRAY_${counter++}`;
      arrayHolder[placeholder] = match;
      return placeholder;
    });

    let keyValuePairs = stringFormatValue.split(",");
    const fields = this._getFieldsForType(type);
    keyValuePairs.forEach(pairString => {
      if (pairString && pairString.includes(":")) {
        const pair = pairString.split(":");
        const key = parseFromString(pair[0].trim());
        const value = parseFromString(pair[1].trim());
        if (fields[key] !== undefined) {
          // If it is an array placeholder we want to swap it with the array itself
          if (arrayHolder[value] !== undefined) fields[key].value = arrayHolder[value]; 
          else fields[key].value = value;
        }
      }
    });
    this.fields = fields;
  }

  _getFieldsForType(type) {
    // Global Formula Modifier
    if (type === "globalFormulaModifiers") {
      return {
        value: {
          value: "",
          format: "string"
        },
        source: {
          value: "",
          format: "string"
        }
      }
    }
    // Roll Level
    if (type === "rollLevel") {
      return {
        value: {
          value: "1",
          format: "number"
        },
        type: {
          value: "adv",
          format: "string",
          selectOptions: {
            "": "",
            adv: "Advantage",
            dis: "Disadvantage"
          }
        },
        label: {
          value: "",
          format: "string"
        },
        applyOnlyForId: {
          value: "",
          format: "string",
          selectOptions: {
            "": "Works for any Actor",
            ["#SPEAKER_ID#"]: "Works only for Caster"
          }
        },
        confirmation: {
          value: false,
          format: "boolean"
        },
        autoCrit: {
          value: false,
          format: "boolean"
        },
        autoFail: {
          value: false,
          format: "boolean"
        },
        skill: {
          value: "",
          format: "string"
        },
        afterRoll: {
          value: false,
          format: "string",
          selectOptions: {
            "": "",
            "disable": "Disable Effect",
            "delete": "Delete Effect"
          }
        }
      }
    }
    // Events
    if (type === "events") {
      return {
        eventType: {
          value: "basic",
          format: "string",
          selectOptions: CONFIG.DC20RPG.eventTypes
        },
        trigger: {
          value: "turnStart",
          format: "string",
          selectOptions: CONFIG.DC20RPG.allEventTriggers
        },
        label: {
          value: "",
          format: "string",
        },
        preTrigger: {
          value: "",
          format: "string",
          selectOptions: {
            "": "",
            "disable": "Disable Effect",
            "skip": "Skip Effect for that Roll"
          }
        },
        postTrigger: {
          value: "",
          format: "string",
          selectOptions: {
            "": "",
            "disable": "Disable Effect",
            "delete": "Delete Effect"
          }
        },
        reenable: {
          value: "",
          format: "string",
          selectOptions: CONFIG.DC20RPG.reenableTriggers
        },
        alwaysActive:  {
          value: false,
          format: "boolean"
        },
        // damage/healing/resource eventType
        value: {
          value: "",
          format: "number",
          skip: {
            key: "eventType",
            dontSkipFor: ["damage", "healing", "resource"]
          }
        },
        // resource eventType
        resourceKey: {
          value: "",
          format: "string",
          skip: {
            key: "eventType",
            dontSkipFor: ["resource"]
          }
        },
        custom: {
          value: false,
          format: "boolean",
          skip: {
            key: "eventType",
            dontSkipFor: ["resource"]
          }
        },
        // damage/healing eventType
        type: {
          value: "",
          format: "string",
          damageTypes: CONFIG.DC20RPG.DROPDOWN_DATA.damageTypes,
          healingTypes: CONFIG.DC20RPG.DROPDOWN_DATA.healingTypes,
          skip: {
            key: "eventType",
            dontSkipFor: ["damage", "healing"]
          }
        },
        continuous: {
          value: false,
          format: "boolean",
          skip: {
            key: "eventType",
            dontSkipFor: ["damage"]
          }
        },
        // checkRequest/saveRequest eventType
        checkKey: {
          value: "mig",
          format: "string",
          checkTypes: CONFIG.DC20RPG.ROLL_KEYS.allChecks,
          saveTypes: CONFIG.DC20RPG.ROLL_KEYS.saveTypes,
          skip: {
            key: "eventType",
            dontSkipFor: ["checkRequest", "saveRequest"]
          }
        },
        against: {
          value: "",
          format: "string",
          skip: {
            key: "eventType",
            dontSkipFor: ["checkRequest", "saveRequest"]
          }
        },
        statuses: {
          value: "",
          format: "array",
          skip: {
            key: "eventType",
            dontSkipFor: ["checkRequest", "saveRequest"]
          }
        },
        onSuccess: {
          value: "",
          format: "string",
          selectOptions: {
            "": "",
            "disable": "Disable Effect",
            "delete": "Delete Effect",
            "runMacro": "Run Macro"
          },
          skip: {
            key: "eventType",
            dontSkipFor: ["checkRequest", "saveRequest"]
          }
        },
        onFail: {
          value: "",
          format: "string",
          selectOptions: {
            "": "",
            "disable": "Disable Effect",
            "delete": "Delete Effect",
            "runMacro": "Run Macro"
          },
          skip: {
            key: "eventType",
            dontSkipFor: ["checkRequest", "saveRequest"]
          }
        },
        // trigger specific - configurable
        triggerOnlyForId: {
          value: "",
          format: "string",
          selectOptions: {
            "": "Works for any Actor",
            ["#SPEAKER_ID#"]: "Works only for Caster"
          },
          skip: {
            key: "trigger",
            dontSkipFor: ["targetConfirm"]
          }
        },
        minimum: {
          value: "",
          format: "number",
          skip: {
            key: "trigger",
            dontSkipFor: ["damageTaken", "healingTaken"]
          }
        },
        withEffectName: {
          value: "",
          format: "string",
        },
        withEffectKey: {
          value: "",
          format: "string",
        },
        withStatus: {
          value: "",
          format: "string",
        },
        restType: {
          value: "long",
          format: "string",
          selectOptions: CONFIG.DC20RPG.DROPDOWN_DATA.restTypes,
          skip: {
            key: "trigger",
            dontSkipFor: ["rest"]
          }
        },
        // trigger specific - auto filled
        actorId: {
          value: "#SPEAKER_ID#",
          format: "string",
        }
      }
    }
  }

  getData() {
    return {
      specificSkill: this.specificSkill,
      type: this.type,
      fields: this.fields,
      displayEffectAppliedFields: this._displayEffectAppliedFields()
    }
  }

  _displayEffectAppliedFields() {
    let display = this.fields.trigger?.value === "effectApplied";
    if (!display) display = this.fields.trigger?.value === "effectRemoved";
    if (!display) display = this.fields.reenable?.value === "effectApplied";
    if (!display) display = this.fields.reenable?.value === "effectRemoved";
    if (!display) display = this.fields.trigger?.value === "effectEnabled";
    if (!display) display = this.fields.trigger?.value === "effectDisabled";
    if (!display) display = this.fields.reenable?.value === "effectEnabled";
    if (!display) display = this.fields.reenable?.value === "effectDisabled";
    return display;
  }

   /** @override */
  activateListeners(html) {
    super.activateListeners(html);
    activateDefaultListeners(this, html);
    html.find(".save-change").click(ev => this._onSave(ev));
  }

  async _onSave(event) {
    event.preventDefault();
    let finalString = "";

    Object.entries(this.fields).forEach(([key, field]) => {
      if (this._shouldSkip(field)) return;
      let value = field.value;
      if (value) {
        if (field.format === "string") value = `"${field.value}"`;
        finalString += `"${key}": ${value}, `;
      }
    });
    finalString = finalString.substring(0, finalString.length - 2);

    this.promiseResolve(finalString);
    this.close();
  }

  _shouldSkip(field) {
    const fieldToCheck = this.fields[field.skip?.key]?.value;
    if (!fieldToCheck) return false;
    return !field.skip.dontSkipFor.includes(fieldToCheck);
  }

  static async create(type, currentValue, specificSkill, dialogData = {}, options = {}) {
    const prompt = new SystemsBuilder(type, currentValue, specificSkill, dialogData, options);
    return new Promise((resolve) => {
      prompt.promiseResolve = resolve;
      prompt.render(true);
    });
  }

  /** @override */
  close(options) {
    if (this.promiseResolve) this.promiseResolve(null);
    super.close(options);
  }
}

async function createSystemsBuilder(type, currentValue, specificSkill) {
  return await SystemsBuilder.create(type, currentValue, specificSkill, {title: "Builder"});
}

class DC20RpgActiveEffectConfig extends ActiveEffectConfig {

  constructor(dialogData = {}, options = {}) {
    super(dialogData, options);
    this.keys = getEffectModifiableKeys();
  }

  /** @override */
  static get defaultOptions() {
    return foundry.utils.mergeObject(super.defaultOptions, {
      classes: ["dc20rpg", "sheet", "active-effect-sheet"], //css classes
      width: 680,
      height: 460,
      tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "description" }],
    });
  }

  /** @override */
  get template() {
    return `systems/dc20rpg/templates/shared/active-effect-config.hbs`;
  }

  /** @override */
  async getData(options={}) {
    const data = await super.getData(options);
    data.keys = this.keys;
    this._customKeyCheck(data.data.changes, data.keys);

    const statusIds = {};
    CONFIG.statusEffects.forEach(status => statusIds[status.id]= status.name);
    return {
      ...data,
      logicalExpressions: CONFIG.DC20RPG.DROPDOWN_DATA.logicalExpressions,
      statusIds: statusIds,
      itemEnhancements: this._getItemEnhacements(),
      onTimeEndOptions: {
        "": "",
        "disable": "Disable Effect",
        "delete": "Delete Effect"
      }
    }
  }

  _getItemEnhacements() {
    const item = this.object.parent;
    if (item.documentName !== "Item") return {};
    else {
      const dropdownData = {};
      item.allEnhancements.forEach((value, key) => dropdownData[key] = value.name);
      return dropdownData;
    }
  }

  _customKeyCheck(changes, keys) {
    for (let i = 0; i < changes.length; i++) {
      if (changes[i].useCustom !== undefined) continue;
      if (!changes[i].key) changes[i].useCustom = false;
      else if (keys[changes[i].key]) changes[i].useCustom = false;
      else changes[i].useCustom = true;
    }
  }

  /** @override */
  activateListeners(html) {
    super.activateListeners(html);
    html.find('.activable').click(ev => this._onActivable(datasetOf(ev).path));
    html.find('.open-systems-builder').click(ev => this._onSystemsBuilder(datasetOf(ev).type, datasetOf(ev).index, datasetOf(ev).isSkill));
    html.find('.update-key').change(ev => this._onUpdateKey(valueOf(ev), datasetOf(ev).index));
    html.find('.effect-macro').click(() => this._onEffectMacro());
  }

  _onActivable(pathToValue) {
    const value = getValueFromPath(this.object, pathToValue);
    setValueForPath(this.object, pathToValue, !value);
    this.render(true);
  }

  async _onUpdateKey(key, index) {
    index = parseFromString(index);
    const changes = this.object.changes; 
    changes[index].key = key;
    await this.object.update({changes: changes});
  }

  async _onSystemsBuilder(type, changeIndex, isSkill) {
    const changes = this.object.changes;
    if (!changes) return;
    const change = changes[changeIndex];
    if (change === undefined) return;

    const result = await createSystemsBuilder(type, change.value, isSkill);
    if (result) {
      changes[changeIndex].value = result;
      this.object.update({changes: changes});
    }
  }

  async _onEffectMacro() {
    const command = this.object.flags.dc20rpg?.macro || "";
    const macro = await createTemporaryMacro(command, this.object, {effect: this.object});
    macro.canUserExecute = (user) => {
      ui.notifications.warn("This is an Effect Macro and it cannot be executed here.");
      return false;
    };
    macro.sheet.render(true);
  }

  async close(options) {
    await super.close(options);
    const flags = this.object.flags.dc20rpg;
    if (flags?.enhKey || flags?.condKey) {
      const item = this.object.parent;
      if (item.documentName !== "Item") return;

      const effectData = this.object.toObject();
      effectData.origin = null;
      if (flags.condKey) item.update({[`system.conditionals.${flags.condKey}.effect`]: effectData});
      if (flags.enhKey) item.update({[`system.enhancements.${flags.enhKey}.modifications.addsEffect`]: effectData});
      await this.object.delete();
    }
  }
}

function createTokenEffectsTracker() {
  const tokenEffectsTracker = new TokenEffectsTracker();
  Hooks.on('controlToken', (token, controlled) => {
    if (controlled) tokenEffectsTracker.render(true);
  });
}

class TokenEffectsTracker extends Application {

  constructor(data = {}, options = {}) {
    super(data, options);
  }

  static get defaultOptions() {
    return foundry.utils.mergeObject(super.defaultOptions, {
      template: "systems/dc20rpg/templates/sidebar/token-effects-tracker.hbs",
      classes: ["dc20rpg", "tokenEffects"],
      popOut: false,
      dragDrop: [
        {dragSelector: ".help-dice[data-key]", dropSelector: null},
      ],
    });
  }

  async getData() {
    const tokens = getSelectedTokens();
    if (tokens.length !== 1) return {active: [], disabled: []} 
    const actor = tokens[0].actor;
    const [active, disabled] = await this._prepareTemporaryEffects(actor);
    const help = this._prepareHelpDice(actor);
    const heldAction = this._prepareHeldAction(actor);
    return {
      help: help,
      active: active,
      disabled: disabled,
      tokenId: tokens[0].id,
      actorId: actor.id,
      heldAction: heldAction,
      sustain: actor.system.sustain,
    }
  }

  _prepareHelpDice(actor) {
    const dice = {};
    for (const [key, help] of Object.entries(actor.system.help.active)) {
      let icon = "fa-diamond";
      switch (help.value) {
        case "d8": case "-d8": icon = "fa-diamond"; break; 
        case "d6": case "-d6": icon = "fa-square"; break; 
        case "d4": case "-d4": icon = "fa-play fa-rotate-270"; break; 
      }
      dice[key] = {
        formula: help.value,
        icon: icon,
        subtraction: help.value.includes("-"),
        doNotExpire: help.doNotExpire
      };
    }
    this.helpDice = dice;
    return dice
  }

  _prepareHeldAction(actor) {
    const actionHeld = actor.flags.dc20rpg.actionHeld;
    if (!actionHeld?.isHeld) return;
    return actionHeld;
  }

  async _prepareTemporaryEffects(actor) {
    const active = [];
    const disabled = [];
    if (actor.allEffects.length === 0) return [active, disabled];

    for(const effect of actor.allEffects) {
      if (effect.isTemporary) {
        effect.descriptionHTML = await TextEditor.enrichHTML(effect.description, {secrets:true});
        effect.timeLeft = effect.roundsLeft;
        effect.allStatauses = await this._statusObjects(effect.statuses, effect.name);

        // If effect is toggleable from item we want to change default behaviour
        const item = effect.getSourceItem();
        if (item) {
          // Equippable
          if (item.system.effectsConfig?.mustEquip) effect.equippable = true; 

          // Toggleable
          if (item.system.toggle?.toggleable && item.system.effectsConfig?.linkWithToggle) effect.itemId = item.id; 
          else effect.itemId = ""; 
        }

        if(effect.disabled) disabled.push(effect);
        else active.push(effect);
      }
    }

    // Merge stackable conditions
    const mergedActive = this._mergeStackableConditions(active);
    const mergedDisabled = this._mergeStackableConditions(disabled);
    return [mergedActive, mergedDisabled];
  }

  _mergeStackableConditions(effects) {
    const mergedEffects = [];
    for (const effectDoc of effects) {
      const effect = {...effectDoc};
      effect._id = effectDoc._id;
      const statusId = effect.system.statusId;
      if (statusId && isStackable(statusId)) {
        const alreadyPushed = mergedEffects.find(e => e.system.statusId === statusId);
        if (alreadyPushed) {
          alreadyPushed._id = effect._id;
          alreadyPushed.system.stack++;
        }
        else {
          effect.system.stack = 1;
          mergedEffects.push(effect);
        }
      }
      else {
        mergedEffects.push(effect);
      }
    }
    return mergedEffects;
  }

  async _statusObjects(statuses, effectName) {
    const statusObjects = [];
    for (const status of CONFIG.statusEffects) {
      if (statuses.has(status.id) && effectName !== status.name) {
        statusObjects.push(status);
      }
    }
    return statusObjects;
  }

  activateListeners(html) {
    super.activateListeners(html);
    html.find('.toggle-effect').click(ev => this._onToggleEffect(datasetOf(ev).effectId, datasetOf(ev).actorId, datasetOf(ev).tokenId, datasetOf(ev).turnOn));
    html.find('.remove-effect').click(ev => this._onRemoveEffect(datasetOf(ev).effectId, datasetOf(ev).actorId, datasetOf(ev).tokenId));
    html.find('.toggle-item').click(ev => this._onToggleItem(datasetOf(ev).itemId, datasetOf(ev).actorId, datasetOf(ev).tokenId));
    html.find('.editable').mousedown(ev => ev.which === 2 ? this._onEditable(datasetOf(ev).effectId, datasetOf(ev).actorId, datasetOf(ev).tokenId) : ()=>{});
    html.find('.held-action').click(ev => this._onHeldAction(datasetOf(ev).actorId, datasetOf(ev).tokenId));
    html.find('.help-dice').contextmenu(ev => this._onHelpActionRemoval(datasetOf(ev).key , datasetOf(ev).actorId, datasetOf(ev).tokenId));
    html.find('.sustain-action').click(ev => this._onDropSustain(datasetOf(ev).index , datasetOf(ev).actorId, datasetOf(ev).tokenId));
  }

  _onEditable(effectId, actorId, tokenId) {
    const owner = getActorFromIds(actorId, tokenId);
    if (owner) editEffectOn(effectId, owner);
  }

  _onToggleEffect(effectId, actorId, tokenId, turnOn) {
    const owner = getActorFromIds(actorId, tokenId);
    if (owner) toggleEffectOn(effectId, owner, turnOn === "true");
    this.render();
  }

  _onToggleItem(itemId, actorId, tokenId) {
    const owner = getActorFromIds(actorId, tokenId);
    if (owner) {
      const item = getItemFromActor(itemId, owner);
      if (item) changeActivableProperty("system.toggle.toggledOn", item);
    }
    this.render();
  }

  _onRemoveEffect(effectId, actorId, tokenId) {
    const owner = getActorFromIds(actorId, tokenId);
    if (owner) deleteEffectFrom(effectId, owner);
    this.render();
  } 

  _onHeldAction(actorId, tokenId) {
    const owner = getActorFromIds(actorId, tokenId);
    if (owner) triggerHeldAction(owner);
    this.render();
  }

  async _onHelpActionRemoval(key, actorId, tokenId) {
    const owner = getActorFromIds(actorId, tokenId);
    if (owner) {
      const confirmed = await getSimplePopup("confirm", {header: "Do you want to remove that Help Dice?"});
      if (confirmed) clearHelpDice(owner, key);
    }
    this.render();
  }

  async _onDropSustain(index, actorId, tokenId) {
    const owner = getActorFromIds(actorId, tokenId);
    if (owner) {
      index = parseInt(index);
      const sustain = owner.system.sustain;
      if (!sustain[index]) return;

      const confirmed = await getSimplePopup("confirm", {header: "Do you want to remove that Sustain Action?"});
      if (confirmed) {
        const sustained = [];
        for (let i = 0; i < sustain.length; i++) {
          if (index !== i) sustained.push(sustain[i]); 
        }
        await owner.update({[`system.sustain`]: sustained});
      }
    }
    this.render();
  }

  _onDragStart(event) {
    super._onDragStart(event);
    const dataset = event.currentTarget.dataset;
    const key = dataset.key;

    const actorId = dataset.actorId;
    const tokenId = dataset.tokenId;
    const helpDice = this.helpDice[key];
    if (helpDice) {
      const dto = {
        key: key,
        formula: helpDice.formula,
        type: "help",
        actorId: actorId,
        tokenId: tokenId,
      };
      event.dataTransfer.setData("text/plain", JSON.stringify(dto));
    }
  }

  _canDragDrop(selector) {
    return true;
  }

  _canDragStart(selector) {
    return true;
  }
}

class AttributeFields extends foundry.data.fields.SchemaField {
  constructor(initialValue=0, saveMastery=false, fields={}, options={}) {
    const f = foundry.data.fields;
    const numberInitial = { required: true, nullable: false, integer: true, initial: initialValue };
    const init0 = { required: true, nullable: false, integer: true, initial: 0 };

    const attribute = () => ({
      saveMastery: new f.BooleanField({required: true, initial: saveMastery}),
      value: new f.NumberField(numberInitial),
      current: new f.NumberField(numberInitial),
      save: new f.NumberField(numberInitial),
      bonuses: new f.SchemaField({
        check: new f.NumberField(init0),
        value: new f.NumberField(init0),
        save: new f.NumberField(init0)
      })
    });
    fields = {
      mig: new f.SchemaField({...attribute(), label: new f.StringField({initial: "dc20rpg.attributes.mig"})}),
      agi: new f.SchemaField({...attribute(), label: new f.StringField({initial: "dc20rpg.attributes.agi"})}),
      cha: new f.SchemaField({...attribute(), label: new f.StringField({initial: "dc20rpg.attributes.cha"})}),
      int: new f.SchemaField({...attribute(), label: new f.StringField({initial: "dc20rpg.attributes.int"})}),
      ...fields
    };
    super(fields, options);
  }
}

class ConditionsFields extends foundry.data.fields.SchemaField {
  constructor(fields={}, options={}) {
    const f = foundry.data.fields;
    const init0 = { required: true, nullable: false, integer: true, initial: 0 };

    const condition = () => ({
      resistance: new f.NumberField(init0),
      vulnerability: new f.NumberField(init0),
      immunity: new f.BooleanField({required: true, initial: false}),
    });

    fields = {
      magical: new f.SchemaField({
        ...condition(),
        label: new f.StringField({initial: "dc20rpg.conditions.magical"})
      }),
      curse: new f.SchemaField({
        ...condition(),
        label: new f.StringField({initial: "dc20rpg.conditions.curse"})
      }),
      movement: new f.SchemaField({
        ...condition(),
        label: new f.StringField({initial: "dc20rpg.conditions.movement"})
      }),
      bleeding: new f.SchemaField({
        ...condition(),
        label: new f.StringField({initial: "dc20rpg.conditions.bleeding"})
      }),
      blinded: new f.SchemaField({
        ...condition(),
        label: new f.StringField({initial: "dc20rpg.conditions.blinded"})
      }),
      burning: new f.SchemaField({
        ...condition(),
        label: new f.StringField({initial: "dc20rpg.conditions.burning"})
      }),
      charmed: new f.SchemaField({
        ...condition(),
        label: new f.StringField({initial: "dc20rpg.conditions.charmed"})
      }),
      dazed: new f.SchemaField({
        ...condition(),
        label: new f.StringField({initial: "dc20rpg.conditions.dazed"})
      }),
      deafened: new f.SchemaField({
        ...condition(),
        label: new f.StringField({initial: "dc20rpg.conditions.deafened"})
      }),
      disoriented: new f.SchemaField({
        ...condition(),
        label: new f.StringField({initial: "dc20rpg.conditions.disoriented"})
      }),
      doomed: new f.SchemaField({
        ...condition(),
        label: new f.StringField({initial: "dc20rpg.conditions.doomed"})
      }),
      exhaustion: new f.SchemaField({
        ...condition(),
        label: new f.StringField({initial: "dc20rpg.conditions.exhaustion"})
      }),
      exposed: new f.SchemaField({
        ...condition(),
        label: new f.StringField({initial: "dc20rpg.conditions.exposed"})
      }),
      frightened: new f.SchemaField({
        ...condition(),
        label: new f.StringField({initial: "dc20rpg.conditions.frightened"})
      }),
      grappled: new f.SchemaField({
        ...condition(),
        label: new f.StringField({initial: "dc20rpg.conditions.grappled"})
      }),
      hindered: new f.SchemaField({
        ...condition(),
        label: new f.StringField({initial: "dc20rpg.conditions.hindered"})
      }),
      impaired: new f.SchemaField({
        ...condition(),
        label: new f.StringField({initial: "dc20rpg.conditions.impaired"})
      }),
      immobilized: new f.SchemaField({
        ...condition(),
        label: new f.StringField({initial: "dc20rpg.conditions.immobilized"})
      }),
      incapacitated: new f.SchemaField({
        ...condition(),
        label: new f.StringField({initial: "dc20rpg.conditions.incapacitated"})
      }),
      intimidated: new f.SchemaField({
        ...condition(),
        label: new f.StringField({initial: "dc20rpg.conditions.intimidated"})
      }),
      paralyzed: new f.SchemaField({
        ...condition(),
        label: new f.StringField({initial: "dc20rpg.conditions.paralyzed"})
      }),
      petrified: new f.SchemaField({
        ...condition(),
        label: new f.StringField({initial: "dc20rpg.conditions.petrified"})
      }),
      prone: new f.SchemaField({
        ...condition(),
        label: new f.StringField({initial: "dc20rpg.conditions.prone"})
      }),
      poisoned: new f.SchemaField({
        ...condition(),
        label: new f.StringField({initial: "dc20rpg.conditions.poisoned"})
      }),
      restrained: new f.SchemaField({
        ...condition(),
        label: new f.StringField({initial: "dc20rpg.conditions.restrained"})
      }),
      slowed: new f.SchemaField({
        ...condition(),
        label: new f.StringField({initial: "dc20rpg.conditions.slowed"})
      }),
      stunned: new f.SchemaField({
        ...condition(),
        label: new f.StringField({initial: "dc20rpg.conditions.stunned"})
      }),
      surprised: new f.SchemaField({
        ...condition(),
        label: new f.StringField({initial: "dc20rpg.conditions.surprised"})
      }),
      taunted: new f.SchemaField({
        ...condition(),
        label: new f.StringField({initial: "dc20rpg.conditions.taunted"})
      }),
      tethered: new f.SchemaField({
        ...condition(),
        label: new f.StringField({initial: "dc20rpg.conditions.tethered"})
      }),
      terrified: new f.SchemaField({
        ...condition(),
        label: new f.StringField({initial: "dc20rpg.conditions.terrified"})
      }),
      unconscious: new f.SchemaField({
        ...condition(),
        label: new f.StringField({initial: "dc20rpg.conditions.unconscious"})
      }),
      weakened: new f.SchemaField({
        ...condition(),
        label: new f.StringField({initial: "dc20rpg.conditions.weakened"})
      }),
    };
    super(fields, options);
  }
}

class DamageReductionFields extends foundry.data.fields.SchemaField {
  constructor(fields={}, options={}) {
    const f = foundry.data.fields;
    const init0 = { required: true, nullable: false, integer: true, initial: 0 };

    const dmgType = (category) => ({
      vulnerable: new f.NumberField(init0),
      resist: new f.NumberField(init0),
      vulnerability: new f.BooleanField({required: true, initial: false}),
      resistance: new f.BooleanField({required: true, initial: false}),
      immune: new f.BooleanField({required: true, initial: false}),
      category: new f.StringField({required: true, initial: category}) 
    });

    fields = {
      flat: new f.NumberField(init0),
      flatHalf: new f.BooleanField({required: true, initial: false}),
      pdr: new f.SchemaField({
        active: new f.BooleanField({required: true, initial: false}),
        label: new f.StringField({initial: "dc20rpg.damageReduction.pdr"}),
      }),
      edr: new f.SchemaField({
        active: new f.BooleanField({required: true, initial: false}),
        label: new f.StringField({initial: "dc20rpg.damageReduction.edr"}),
      }),
      mdr: new f.SchemaField({
        active: new f.BooleanField({required: true, initial: false}),
        label: new f.StringField({initial: "dc20rpg.damageReduction.mdr"}),
      }),
      damageTypes: new f.SchemaField({
        corrosion: new f.SchemaField({
          ...dmgType("elemental"),
          label: new f.StringField({initial: "dc20rpg.reductions.corrosion"})
        }),
        cold: new f.SchemaField({
          ...dmgType("elemental"),
          label: new f.StringField({initial: "dc20rpg.reductions.cold"})
        }),
        fire: new f.SchemaField({
          ...dmgType("elemental"),
          label: new f.StringField({initial: "dc20rpg.reductions.fire"})
        }),
        lightning: new f.SchemaField({
          ...dmgType("elemental"),
          label: new f.StringField({initial: "dc20rpg.reductions.lightning"})
        }),
        poison: new f.SchemaField({
          ...dmgType("elemental"),
          label: new f.StringField({initial: "dc20rpg.reductions.poison"})
        }),
        radiant: new f.SchemaField({
          ...dmgType("mystical"),
          label: new f.StringField({initial: "dc20rpg.reductions.radiant"})
        }),
        psychic: new f.SchemaField({
          ...dmgType("mystical"),
          label: new f.StringField({initial: "dc20rpg.reductions.psychic"})
        }),
        sonic: new f.SchemaField({
          ...dmgType("mystical"),
          label: new f.StringField({initial: "dc20rpg.reductions.sonic"})
        }),
        umbral: new f.SchemaField({
          ...dmgType("mystical"),
          label: new f.StringField({initial: "dc20rpg.reductions.umbral"})
        }),
        piercing: new f.SchemaField({
          ...dmgType("physical"),
          label: new f.StringField({initial: "dc20rpg.reductions.piercing"})
        }),
        slashing: new f.SchemaField({
          ...dmgType("physical"),
          label: new f.StringField({initial: "dc20rpg.reductions.slashing"})
        }),
        bludgeoning: new f.SchemaField({
          ...dmgType("physical"),
          label: new f.StringField({initial: "dc20rpg.reductions.bludgeoning"})
        }),
      }), 
    };
    super(fields, options);
  }
}

class DefenceFields extends foundry.data.fields.SchemaField {
  constructor(formulaKey="standard", fields={}, options={}) {
    const f = foundry.data.fields;
    const init8 = { required: true, nullable: false, integer: true, initial: 8 };
    const init0 = { required: true, nullable: false, integer: true, initial: 0 };

    const defence = () => ({
      formulaKey: new f.StringField({required: false, initial: formulaKey}),
      customFormula: new f.StringField({required: true, initial: ""}),
      value: new f.NumberField(init8),
      normal: new f.NumberField(init8),
      heavy: new f.NumberField(init0),
      brutal: new f.NumberField(init0),
    });

    fields = {
      precision: new f.SchemaField({
        ...defence(), 
        label: new f.StringField({initial: "dc20rpg.defence.precision"}),
        bonuses: new f.SchemaField({
          noArmor: new f.NumberField(init0),
          noHeavy: new f.NumberField(init0),
          always: new f.NumberField(init0),
        })
      }),
      area: new f.SchemaField({
        ...defence(), 
        label: new f.StringField({initial: "dc20rpg.defence.area"}),
        bonuses: new f.SchemaField({
          noArmor: new f.NumberField(init0),
          noHeavy: new f.NumberField(init0),
          always: new f.NumberField(init0),
        })
      }),
      ...fields
    };
    super(fields, options);
  }
}

class GFModFields extends foundry.data.fields.SchemaField {
  constructor(fields={}, options={}) {
    const f = foundry.data.fields;
    fields = {
      attributeCheck: new f.ArrayField(new f.StringField(), {required: true}),
      attackCheck: new f.ArrayField(new f.StringField(), {required: true}),
      spellCheck: new f.ArrayField(new f.StringField(), {required: true}),
      skillCheck: new f.ArrayField(new f.StringField(), {required: true}),
      save: new f.ArrayField(new f.StringField(), {required: true}),
      attackDamage: new f.SchemaField({
        martial: new f.SchemaField({
          melee: new f.ArrayField(new f.StringField(), {required: true}),
          ranged: new f.ArrayField(new f.StringField(), {required: true}),
        }),
        spell: new f.SchemaField({
          melee: new f.ArrayField(new f.StringField(), {required: true}),
          ranged: new f.ArrayField(new f.StringField(), {required: true}),
        }),
      }),
      healing: new f.ArrayField(new f.StringField(), {required: true}),
      ...fields
    };
    super(fields, options);
  }
}

class JumpFields extends foundry.data.fields.SchemaField {
  constructor(key="agi", fields={}, options={}) {
    const f = foundry.data.fields;
    const init0 = { required: true, nullable: false, integer: true, initial: 0 };

    fields = {
      current: new f.NumberField(init0),
      value: new f.NumberField(init0),
      bonus: new f.NumberField(init0),
      key: new f.StringField({required: true, initial: key}),
      label: new f.StringField({initial: "dc20rpg.speed.jump"}),
    };
    super(fields, options);
  }
}

class CombatTraining extends foundry.data.fields.SchemaField {
  constructor(all=false, fields={}, options={}) {
    const f = foundry.data.fields;
    fields = {
      weapons: new f.BooleanField({required: true, initial: all}),
      lightShield: new f.BooleanField({required: true, initial: all}),
      heavyShield: new f.BooleanField({required: true, initial: all}),
      lightArmor: new f.BooleanField({required: true, initial: true}),
      heavyArmor: new f.BooleanField({required: true, initial: all}),
      ...fields
    };
    
    super(fields, options);
  }
}

class MovementFields extends foundry.data.fields.SchemaField {
  constructor(custom=true, fields={}, options={}) {
    const f = foundry.data.fields;
    const init0 = { required: true, nullable: false, integer: true, initial: 0 };

    const movement = () => ({
      useCustom: new f.BooleanField({required: true, initial: custom}),
      fullSpeed: new f.BooleanField({required: true, initial: false}),
      halfSpeed: new f.BooleanField({required: true, initial: false}),
      current: new f.NumberField(init0),
      value: new f.NumberField(init0),
      bonus: new f.NumberField(init0),
    });

    fields = {
      ground: new f.SchemaField({
        useCustom: new f.BooleanField({required: true, initial: custom}),
        current: new f.NumberField({ required: true, nullable: false, integer: true, initial: 5 }),
        value: new f.NumberField({ required: true, nullable: false, integer: true, initial: 5 }),
        bonus: new f.NumberField(init0), 
        label: new f.StringField({initial: "dc20rpg.speed.ground"}),
      }),
      climbing: new f.SchemaField({
        ...movement(),
        label: new f.StringField({initial: "dc20rpg.speed.climbing"}),
      }),
      swimming: new f.SchemaField({
        ...movement(),
        label: new f.StringField({initial: "dc20rpg.speed.swimming"}),
      }),
      burrow: new f.SchemaField({
        ...movement(),
        label: new f.StringField({initial: "dc20rpg.speed.burrow"}),
      }),
      glide: new f.SchemaField({
        ...movement(),
        label: new f.StringField({initial: "dc20rpg.speed.glide"}),
      }),
      flying: new f.SchemaField({
        ...movement(),
        label: new f.StringField({initial: "dc20rpg.speed.flying"}),
      }),
    };
    super(fields, options);
  }
}

class PointFields extends foundry.data.fields.SchemaField {
  constructor(max=0, fields={}, options={}) {
    const f = foundry.data.fields;
    fields = {
      max: new f.NumberField({ required: true, nullable: false, integer: true, initial: max }),
      extra: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
      bonus: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
      spent: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
      override: new f.BooleanField({required: true, initial: false}),
      overridenMax: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
      ...fields
    };
    
    super(fields, options);
  }
}

class ResourceFields extends foundry.data.fields.SchemaField {
  constructor(pcResources, fields={}, options={}) {
    const f = foundry.data.fields;
    const init0 = { required: true, nullable: false, integer: true, initial: 0 };

    const resource = () => ({
      value: new f.NumberField(init0),
      bonus: new f.NumberField(init0),
      max: new f.NumberField(init0),
    });
    fields = {
      ap: new f.SchemaField({
        value: new f.NumberField({ required: true, nullable: false, integer: true, initial: 4 }),
        bonus: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
        max: new f.NumberField({ required: true, nullable: false, integer: true, initial: 4 }),
      }),
      custom: new f.ObjectField({required: true}),
      ...fields
    };

    if (pcResources) {
      fields = {
        stamina: new f.SchemaField({
          ...resource(), 
          label: new f.StringField({initial: "dc20rpg.resource.stamina"}),
          maxFormula: new f.StringField({ required: true, initial: "@resources.stamina.bonus + @details.class.bonusStamina"}),
        }),
        mana: new f.SchemaField({
          ...resource(), 
          label: new f.StringField({initial: "dc20rpg.resource.mana"}),
          maxFormula: new f.StringField({ required: true, initial: "@resources.mana.bonus + @details.class.bonusMana"}),
        }),
        grit: new f.SchemaField({ 
          ...resource(),
          label: new f.StringField({initial: "dc20rpg.resource.grit"}),
          maxFormula: new f.StringField({ required: true, initial: "2 + @cha + @resources.grit.bonus"}),
        }),
        restPoints: new f.SchemaField({
          ...resource(), 
          label: new f.StringField({initial: "dc20rpg.resource.restPoints"}), 
        }),
        health: new f.SchemaField({
          ...resource(),
          value: new f.NumberField({ required: true, nullable: false, integer: true, initial: 6 }),
          current: new f.NumberField({ required: true, nullable: false, integer: true, initial: 6 }),
          max: new f.NumberField({ required: true, nullable: false, integer: true, initial: 6 }),
          temp: new f.NumberField({ required: true, nullable: true, integer: true, initial: null }),
          useFlat: new f.BooleanField({required: true, initial: false}),
        }),
        ...fields
      };
    }
    else {
      fields = {
        health: new f.SchemaField({
          ...resource(),
          value: new f.NumberField({ required: true, nullable: false, integer: true, initial: 6 }),
          current: new f.NumberField({ required: true, nullable: false, integer: true, initial: 6 }),
          max: new f.NumberField({ required: true, nullable: false, integer: true, initial: 6 }),
          temp: new f.NumberField({ required: true, nullable: true, integer: true, initial: null }),
          useFlat: new f.BooleanField({required: true, initial: true}),
        }),
        ...fields
      };
    }
    super(fields, options);
  }
}

class RestFields extends foundry.data.fields.SchemaField {
  constructor(fields={}, options={}) {
    const f = foundry.data.fields;
    fields = {
      longRest: new f.SchemaField({
        exhSaveDC: new f.NumberField({ required: true, nullable: false, integer: true, initial: 10 }),
        half: new f.BooleanField({required: true, initial: false}),
        noActivity: new f.BooleanField({required: true, initial: false}),
      }),
      ...fields
    };
    
    super(fields, options);
  }
}

class RollLevelFields extends foundry.data.fields.SchemaField {
  constructor(fields={}, options={}) {
    const f = foundry.data.fields;
    fields = {
      onYou: new f.SchemaField({
        martial: new f.SchemaField({
          melee: new f.ArrayField(new f.StringField(), {required: true}),
          ranged: new f.ArrayField(new f.StringField(), {required: true}),
        }),
        spell: new f.SchemaField({
          melee: new f.ArrayField(new f.StringField(), {required: true}),
          ranged: new f.ArrayField(new f.StringField(), {required: true}),
        }),
        checks: new f.SchemaField({
          mig: new f.ArrayField(new f.StringField(), {required: true}),
          agi: new f.ArrayField(new f.StringField(), {required: true}),
          int: new f.ArrayField(new f.StringField(), {required: true}),
          cha: new f.ArrayField(new f.StringField(), {required: true}),
          att: new f.ArrayField(new f.StringField(), {required: true}),
          spe: new f.ArrayField(new f.StringField(), {required: true}),
        }),
        saves: new f.SchemaField({
          mig: new f.ArrayField(new f.StringField(), {required: true}),
          agi: new f.ArrayField(new f.StringField(), {required: true}),
          int: new f.ArrayField(new f.StringField(), {required: true}),
          cha: new f.ArrayField(new f.StringField(), {required: true}),
        }),
        deathSave: new f.ArrayField(new f.StringField(), {required: true}),
        initiative: new f.ArrayField(new f.StringField(), {required: true}),
        skills: new f.ArrayField(new f.StringField(), {required: true}),
        tradeSkills: new f.ArrayField(new f.StringField(), {required: true}),
      }),
      againstYou: new f.SchemaField({
        martial: new f.SchemaField({
          melee: new f.ArrayField(new f.StringField(), {required: true}),
          ranged: new f.ArrayField(new f.StringField(), {required: true}),
        }),
        spell: new f.SchemaField({
          melee: new f.ArrayField(new f.StringField(), {required: true}),
          ranged: new f.ArrayField(new f.StringField(), {required: true}),
        }),
        checks: new f.SchemaField({
          mig: new f.ArrayField(new f.StringField(), {required: true}),
          agi: new f.ArrayField(new f.StringField(), {required: true}),
          int: new f.ArrayField(new f.StringField(), {required: true}),
          cha: new f.ArrayField(new f.StringField(), {required: true}),
          att: new f.ArrayField(new f.StringField(), {required: true}),
          spe: new f.ArrayField(new f.StringField(), {required: true}),
        }),
        saves: new f.SchemaField({
          mig: new f.ArrayField(new f.StringField(), {required: true}),
          agi: new f.ArrayField(new f.StringField(), {required: true}),
          int: new f.ArrayField(new f.StringField(), {required: true}),
          cha: new f.ArrayField(new f.StringField(), {required: true}),
        }),
        skills: new f.ArrayField(new f.StringField(), {required: true}),
        tradeSkills: new f.ArrayField(new f.StringField(), {required: true}),
        ...fields
      }),
    };
    super(fields, options);
  }
}

class SenseFields extends foundry.data.fields.SchemaField {
  constructor(fields={}, options={}) {
    const f = foundry.data.fields;
    const init0 = { required: true, nullable: false, integer: true, initial: 0 };

    const sense = () => ({
      range: new f.NumberField(init0),
      bonus: new f.NumberField(init0),
      overridenRange: new f.NumberField(init0),
      override: new f.BooleanField({required: true, initial: false}),
      orOption: new f.SchemaField({
        range: new f.NumberField(init0),
        bonus: new f.NumberField(init0),
      })
    });

    fields = {
      darkvision: new f.SchemaField({
        ...sense(),
        label: new f.StringField({initial: "dc20rpg.senses.darkvision"}),
      }),
      tremorsense: new f.SchemaField({
        ...sense(),
        label: new f.StringField({initial: "dc20rpg.senses.tremorsense"}),
      }),
      blindsight: new f.SchemaField({
        ...sense(),
        label: new f.StringField({initial: "dc20rpg.senses.blindsight"}),
      }),
      truesight: new f.SchemaField({
        ...sense(),
        label: new f.StringField({initial: "dc20rpg.senses.truesight"}),
      }),
    };
    super(fields, options);
  }
}

class SizeFields extends foundry.data.fields.SchemaField {
  constructor(fromAncestry=false, fields={}, options={}) {
    const f = foundry.data.fields;
    fields = {
      fromAncestry: new f.BooleanField({required: true, initial: fromAncestry}),
      size: new f.StringField({required: true, initial: "medium"}),
      ...fields
    };
    super(fields, options);
  }
}

class SkillFields {
  constructor(type) {
    const f = foundry.data.fields;
    const skillStore = game.settings.get("dc20rpg", "skillStore");

    switch(type) {
      case "skill": return new f.ObjectField({required: true, initial: skillStore.skills})
      case "trade": return new f.ObjectField({required: true, initial: skillStore.trades})
      case "language": return new f.ObjectField({required: true, initial: skillStore.languages})
    }
  }
}

class DC20BaseActorData extends foundry.abstract.TypeDataModel {
  static defineSchema() {
    const f = foundry.data.fields;

    return {
      attributes: new AttributeFields(),
      skills: new SkillFields("skill"),
      languages: new SkillFields("language"),
      expertise: new f.SchemaField({
        automated: new f.ArrayField(new f.StringField(), {required: true}),
        manual: new f.ArrayField(new f.StringField(), {required: true})
      }),
      help: new f.SchemaField({
        active: new f.ObjectField({required: true}),
        maxDice: new f.NumberField({required: true, initial: 8})
      }),
      defences: new DefenceFields(),
      damageReduction: new DamageReductionFields(), 
      healingReduction: new f.SchemaField({
        flat: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
        flatHalf: new f.BooleanField({required: true, initial: false}),
      }),
      statusResistances: new ConditionsFields(),
      customCondition: new f.StringField({initial: ""}),
      size: new SizeFields(),
      jump: new JumpFields(),
      movement: new MovementFields(),
      senses: new SenseFields(),
      scaling: new f.ObjectField({required: true}),
      currency: new f.SchemaField({
        cp: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
        sp: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
        gp: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
        pp: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
      }),
      movePoints: new f.NumberField({ required: true, nullable: false, integer: false, initial: 0 }),
      moveCost: new f.NumberField({ required: true, nullable: false, integer: false, initial: 1 }),
      death: new f.SchemaField({
        active: new f.BooleanField({required: true, initial: false}),
        treshold: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
        bonus: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
      }),
      globalFormulaModifiers: new GFModFields(),
      globalModifier: new f.SchemaField({
        range: new f.SchemaField({
          melee: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
          normal: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
          max: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
        }),
        allow: new f.SchemaField({ 
          overheal: new f.BooleanField({required: true, initial: false}),
        }),
        prevent: new f.SchemaField({ 
          goUnderAP: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
          hpRegeneration: new f.BooleanField({required: true, initial: false}),
        }),
        ignore: new f.SchemaField({
          difficultTerrain: new f.BooleanField({required: true, initial: false}),
          closeQuarters: new f.BooleanField({required: true, initial: false}),
          longRange: new f.BooleanField({required: true, initial: false}),
          flanking: new f.BooleanField({required: true, initial: false})
        }),
        provide: new f.SchemaField({
          halfCover: new f.BooleanField({required: true, initial: false}),
          tqCover: new f.BooleanField({required: true, initial: false}),
        }),
      }),
      events: new f.ArrayField(new f.StringField(), {required: true}),
      conditionals: new f.ArrayField(new f.ObjectField(), {required: true}),
      keywords: new f.ObjectField({required: true}),
      rollLevel: new RollLevelFields(),
      mcp: new f.ArrayField(new f.StringField(), {required: true}),
      sustain: new f.ArrayField(new f.ObjectField(), {required: true}),
      journal: new f.StringField({required: true, initial: ""})
    }
  }

  static migrateData(source) {
    if (source.vision) {
      source.senses = source.vision;
      delete source.vision;
    }
    if (source.conditions) {
      source.statusResistances = source.conditions;
      delete source.conditions;
    }
    return super.migrateData(source);
  }

  static mergeSchema(a, b) {
    Object.assign(a, b);
    return a;
  }
}

class DC20CharacterData extends DC20BaseActorData {
  static defineSchema() {
    const f = foundry.data.fields;

    return this.mergeSchema(super.defineSchema(), {
      attributes: new AttributeFields(-2, true),
      attributePoints: new PointFields(12),
      resources: new ResourceFields(true),
      skillPoints: new f.SchemaField({
        skill: new PointFields(0, {converted: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 })}),
        trade: new PointFields(0,{converted: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 })}),
        language: new PointFields(0,{converted: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 })}),
      }),
      known: new f.SchemaField({
        cantrips: new f.SchemaField({
          current: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
          max: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
        }),
        spells: new f.SchemaField({
          current: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
          max: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
        }),
        maneuvers: new f.SchemaField({
          current: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
          max: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
        }),
        techniques: new f.SchemaField({
          current: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
          max: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
        }),
      }),
      tradeSkills: new SkillFields("trade"),
      details: new f.SchemaField({
        ancestry: new f.SchemaField({id: new f.StringField({required: true})}, {required: false}),
        background: new f.SchemaField({id: new f.StringField({required: true})}, {required: false}),
        class: new f.SchemaField({
          id: new f.StringField({required: true}),
          maxHpBonus: new f.NumberField({ required: false, integer: true, initial: 0, nullable: false }),
          bonusStamina: new f.NumberField({ required: false, integer: true, initial: 0, nullable: false }),
          bonusMana: new f.NumberField({ required: false, integer: true, initial: 0, nullable: false }),
        }, {required: false}),
        subclass: new f.SchemaField({id: new f.StringField({required: false})}, {required: false}),
        level: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
        combatMastery: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
        martial: new f.BooleanField({required: true, initial: false}),
        martialExpansionProvided: new f.BooleanField({required: true, initial: false}),
        spellcaster: new f.BooleanField({required: true, initial: false}),
        primeAttrKey: new f.StringField({required: false}),
        advancementInfo: new f.SchemaField({
          multiclassTalents: new f.ObjectField({required: true}),
        })
      }),
      size: new SizeFields(true),
      movement: new MovementFields(false),
      saveDC: new f.SchemaField({
        value: new f.SchemaField({
          spell: new f.NumberField({ required: true, nullable: false, integer: true, initial: 8 }),
          martial: new f.NumberField({ required: true, nullable: false, integer: true, initial: 8 }),
        }),
        bonus: new f.SchemaField({
          spell: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
          martial: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
        }),
      }),
      attackMod: new f.SchemaField({
        value: new f.SchemaField({
          spell: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
          martial: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
        }),
        bonus: new f.SchemaField({
          spell: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
          martial: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
        }),
      }),
      combatTraining: new CombatTraining(),
      rest: new RestFields()
    });
  } 

  static migrateData(source) {
    return super.migrateData(source);
  }
}

class DC20NpcData extends DC20BaseActorData {
  static defineSchema() {
    const f = foundry.data.fields;

    return this.mergeSchema(super.defineSchema(), {
      defences: new DefenceFields("flat"),
      jump: new JumpFields("flat"),
      resources: new ResourceFields(false),
      details: new f.SchemaField({
        level: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
        combatMastery: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
        creatureType: new f.StringField({required: false}),
        role: new f.StringField({required: false}),
        aligment: new f.StringField({required: false}),
      }),
      saveDC: new f.SchemaField({
        flat: new f.BooleanField({required: true, initial: false}),
        value: new f.SchemaField({
          spell: new f.NumberField({ required: true, nullable: false, integer: true, initial: 8 }),
          martial: new f.NumberField({ required: true, nullable: false, integer: true, initial: 8 }),
        }),
        bonus: new f.SchemaField({
          spell: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
          martial: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
        }),
      }),
      attackMod: new f.SchemaField({
        flat: new f.BooleanField({required: true, initial: false}),
        value: new f.SchemaField({
          spell: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
          martial: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
        }),
        bonus: new f.SchemaField({
          spell: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
          martial: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
        }),
      }),
      combatTraining: new CombatTraining(true)
    });
  } 
}

class DC20CompanionData extends DC20NpcData {
  static defineSchema() {
    const f = foundry.data.fields;

    return this.mergeSchema(super.defineSchema(), {
      attributePoints: new PointFields(8),
      traits: new f.ObjectField({required: true}),
      companionOwnerId: new f.StringField({required: true, initial: ""}),
      shareWithCompanionOwner: new f.SchemaField({
        attackMod: new f.BooleanField({required: true, initial: true}),
        saveDC: new f.BooleanField({required: true, initial: true}),
        ap: new f.BooleanField({required: true, initial: true}),
        health: new f.BooleanField({required: true, initial: false}),
        combatMastery: new f.BooleanField({required: true, initial: true}),
        prime: new f.BooleanField({required: true, initial: true}),
        combatTraining: new f.BooleanField({required: true, initial: true}),
        speed: new f.BooleanField({required: true, initial: false}),
        skills: new f.BooleanField({required: true, initial: false}),
        defences: new f.SchemaField({
          area: new f.BooleanField({required: true, initial: false}),
          precision: new f.BooleanField({required: true, initial: false}),
        }),
        damageReduction: new f.SchemaField({
          pdr: new f.BooleanField({required: true, initial: false}),
          edr: new f.BooleanField({required: true, initial: false}),
          mdr: new f.BooleanField({required: true, initial: false}),
        }),
        attributes: new f.SchemaField({
          mig: new f.BooleanField({required: true, initial: false}),
          agi: new f.BooleanField({required: true, initial: false}),
          cha: new f.BooleanField({required: true, initial: false}),
          int: new f.BooleanField({required: true, initial: false}),
        }),
        saves: new f.SchemaField({
          mig: new f.BooleanField({required: true, initial: false}),
          agi: new f.BooleanField({required: true, initial: false}),
          cha: new f.BooleanField({required: true, initial: false}),
          int: new f.BooleanField({required: true, initial: false}),
        }),
        mcp: new f.BooleanField({required: true, initial: false}),
        initiative: new f.BooleanField({required: true, initial: false}),
      }),
    })
  }
}

class AttackFormulaFields extends foundry.data.fields.SchemaField {
  constructor(fields={}, options={}) {
    const f = foundry.data.fields;

    fields = {
      rangeType: new f.StringField({required: true, initial: "melee"}),
      checkType: new f.StringField({required: true, initial: "attack"}), // TODO rename to checkKey for consistency, see DC20SpellData - we need to change that too
      targetDefence: new f.StringField({required: true, initial: "precision"}),
      rollBonus: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
      combatMastery: new f.BooleanField({required: true, initial: true}),
      critThreshold: new f.NumberField({ required: true, nullable: false, integer: true, initial: 20 }),
      halfDmgOnMiss: new f.BooleanField({required: true, initial: false}),
      skipBonusDamage: new f.SchemaField({
        heavy: new f.BooleanField({required: true, initial: false}),
        brutal: new f.BooleanField({required: true, initial: false}),
        crit: new f.BooleanField({required: true, initial: false}),
        conditionals: new f.BooleanField({required: true, initial: false}),
      }),
      formulaMod: new f.StringField({required: true, initial: ""}),
      ...fields
    };
    super(fields, options);
  }
}

class CheckFields extends foundry.data.fields.SchemaField {
  constructor(fields={}, options={}) {
    const f = foundry.data.fields;

    fields = {
      canCrit: new f.BooleanField({required: true, initial: false}),
      checkKey: new f.StringField({required: true, initial: "att"}),
      multiCheck: new f.SchemaField({
        active: new f.BooleanField({required: true, initial: false}),
        options: new f.ObjectField({required: true})
      }),
      contestedKey: new f.StringField({required: true, initial: "phy"}), // Left for backward compatibility
      againstDC: new f.BooleanField({required: true, initial: true}),
      checkDC: new f.NumberField({ required: true, nullable: false, integer: true, initial: 10 }),
      respectSizeRules: new f.BooleanField({required: true, initial: false}), // Left for backward compatibility
      failEffect: new f.StringField({required: true, initial: ""}), // Left for backward compatibility
      ...fields
    };
    super(fields, options);
  }
}

class ConditionalFields extends foundry.data.fields.SchemaField {
  constructor(fields={}, options={}) {
    const f = foundry.data.fields;

    fields = {
      hasConditional: new f.BooleanField({required: true, initial: false}),
      name: new f.StringField({required: true, initial: ""}),
      condition: new f.StringField({required: true, initial: ""}),
      useFor: new f.StringField({required: true, initial: ""}),
      linkWithToggle: new f.BooleanField({required: true, initial: false}),
      bonus: new f.StringField({ required: true, initial: "0" }),
      flags: new f.SchemaField({
        ignorePdr: new f.BooleanField({required: true, initial: false}),
        ignoreEdr: new f.BooleanField({required: true, initial: false}),
        ignoreMdr: new f.BooleanField({required: true, initial: false}),
        ignoreResistance: new f.ObjectField({required: true}),
        ignoreImmune: new f.ObjectField({required: true}),
      }),
      effect: new f.ObjectField({required: true, nullable: true, initial: null}),
      addsNewRollRequest: new f.BooleanField({required: true, initial: false}),
      rollRequest: new f.SchemaField({
        category: new f.StringField({required: true, initial: ""}),
        saveKey: new f.StringField({required: true, initial: ""}),
        contestedKey: new f.StringField({required: true, initial: ""}),
        dcCalculation: new f.StringField({required: true, initial: ""}),
        dc: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
        addMasteryToDC: new f.BooleanField({required: true, initial: true}),
        respectSizeRules: new f.BooleanField({required: true, initial: false}),
      }),
      addsNewFormula: new f.BooleanField({required: true, initial: false}),
      formula: new f.SchemaField({
        formula: new f.StringField({required: true, initial: ""}),
        type: new f.StringField({required: true, initial: ""}),
        category: new f.StringField({required: true, initial: "damage"}),
        dontMerge: new f.BooleanField({required: true, initial: false}),
        overrideDefence: new f.StringField({required: true, initial: ""}),
      }),
    };
    super(fields, options);
  }
}

class EffectsConfigFields extends foundry.data.fields.SchemaField {
  constructor(fields={}, options={}) {
    const f = foundry.data.fields;

    fields = {
      linkWithToggle: new f.BooleanField({required: true, initial: false}),
      toggleItem: new f.BooleanField({required: true, initial: true}),
      active: new f.BooleanField({required: true, initial: false}),
      addToChat: new f.BooleanField({required: true, initial: false}),
      addToTemplates: new f.StringField({required: true, initial: ""}),
      ...fields
    };
    super(fields, options);
  }
}

class PropertyFields extends foundry.data.fields.SchemaField {
  constructor(itemType="", fields={}, options={}) {
    const f = foundry.data.fields;
    const init0 = { required: true, nullable: false, integer: true, initial: 0 };

    const attunement = () => ({
      attunement: new f.SchemaField({
        active: new f.BooleanField({required: true, initial: false}),
        slotCost: new f.NumberField(init0),
        label: new f.StringField({initial: "dc20rpg.properties.attunement"})
      })
    });

    switch(itemType) {
      case "weapon": 
        fields = {
          ...attunement(),
          ..._weaponProps(), 
          ...fields
        };
        break;
      
      case "equipment": 
        fields = {
          ...attunement(),
          ..._equipmentProps(), 
          ...fields
        };
        break;
      
      default: 
        fields = {
          ...attunement(),
          ...fields,
        };
    }
    super(fields, options);
  }

}

function _weaponProps() {
  const f = foundry.data.fields;
  return {
    ammo: new f.SchemaField({
      active: new f.BooleanField({required: true, initial: false}),
      label: new f.StringField({initial: "dc20rpg.properties.ammo"})
    }),
    concealable: new f.SchemaField({
      active: new f.BooleanField({required: true, initial: false}),
      label: new f.StringField({initial: "dc20rpg.properties.concealable"})
    }),
    guard: new f.SchemaField({
      active: new f.BooleanField({required: true, initial: false}),
      label: new f.StringField({initial: "dc20rpg.properties.guard"})
    }),
    heavy: new f.SchemaField({
      active: new f.BooleanField({required: true, initial: false}),
      label: new f.StringField({initial: "dc20rpg.properties.heavy"})
    }),
    impact: new f.SchemaField({
      active: new f.BooleanField({required: true, initial: false}),
      label: new f.StringField({initial: "dc20rpg.properties.impact"})
    }),
    longRanged: new f.SchemaField({
      active: new f.BooleanField({required: true, initial: false}),
      label: new f.StringField({initial: "dc20rpg.properties.longRanged"})
    }),
    multiFaceted: new f.SchemaField({
      active: new f.BooleanField({required: true, initial: false}),
      selected: new f.StringField({required: true, initial: "first"}),
      labelKey: new f.StringField({required: true, initial: ""}),
      weaponStyle: new f.SchemaField({
        first: new f.StringField({required: true, initial: ""}),
        second: new f.StringField({required: true, initial: ""}),
      }),
      damageType: new f.SchemaField({
        first: new f.StringField({required: true, initial: ""}),
        second: new f.StringField({required: true, initial: ""}),
      }),
      label: new f.StringField({initial: "dc20rpg.properties.multiFaceted"})
    }),
    reach: new f.SchemaField({
      active: new f.BooleanField({required: true, initial: false}),
      value: new f.NumberField({ required: true, nullable: false, integer: true, initial: 1 }),
      label: new f.StringField({initial: "dc20rpg.properties.reach"})
    }),
    reload: new f.SchemaField({
      active: new f.BooleanField({required: true, initial: false}),
      loaded: new f.BooleanField({required: true, initial: true}),
      value: new f.NumberField({ required: true, nullable: false, integer: true, initial: 1 }),
      label: new f.StringField({initial: "dc20rpg.properties.reload"})
    }),
    silent: new f.SchemaField({
      active: new f.BooleanField({required: true, initial: false}),
      label: new f.StringField({initial: "dc20rpg.properties.silent"})
    }),
    toss: new f.SchemaField({
      active: new f.BooleanField({required: true, initial: false}),
      label: new f.StringField({initial: "dc20rpg.properties.toss"})
    }),
    thrown: new f.SchemaField({
      active: new f.BooleanField({required: true, initial: false}),
      label: new f.StringField({initial: "dc20rpg.properties.thrown"})
    }),
    twoHanded: new f.SchemaField({
      active: new f.BooleanField({required: true, initial: false}),
      label: new f.StringField({initial: "dc20rpg.properties.twoHanded"})
    }),
    unwieldy: new f.SchemaField({
      active: new f.BooleanField({required: true, initial: false}),
      label: new f.StringField({initial: "dc20rpg.properties.unwieldy"})
    }),
    versatile: new f.SchemaField({
      active: new f.BooleanField({required: true, initial: false}),
      label: new f.StringField({initial: "dc20rpg.properties.versatile"})
    }),
    capture: new f.SchemaField({
      active: new f.BooleanField({required: true, initial: false}),
      label: new f.StringField({initial: "dc20rpg.properties.capture"})
    }),
    returning: new f.SchemaField({
      active: new f.BooleanField({required: true, initial: false}),
      label: new f.StringField({initial: "dc20rpg.properties.returning"})
    }),
  }
}

function _equipmentProps() {
  const f = foundry.data.fields;
  return {
    adIncrease: new f.SchemaField({
      active: new f.BooleanField({required: true, initial: false}),
      value: new f.NumberField({ required: true, nullable: false, integer: true, initial: 1 }),
      label: new f.StringField({initial: "dc20rpg.properties.adIncrease"})
    }),
    pdIncrease: new f.SchemaField({
      active: new f.BooleanField({required: true, initial: false}),
      value: new f.NumberField({ required: true, nullable: false, integer: true, initial: 1 }),
      label: new f.StringField({initial: "dc20rpg.properties.pdIncrease"})
    }),
    edr: new f.SchemaField({
      active: new f.BooleanField({required: true, initial: false}),
      label: new f.StringField({initial: "dc20rpg.properties.edr"})
    }),
    pdr: new f.SchemaField({
      active: new f.BooleanField({required: true, initial: false}),
      label: new f.StringField({initial: "dc20rpg.properties.pdr"})
    }),
    bulky: new f.SchemaField({
      active: new f.BooleanField({required: true, initial: false}),
      label: new f.StringField({initial: "dc20rpg.properties.bulky"})
    }),
    rigid: new f.SchemaField({
      active: new f.BooleanField({required: true, initial: false}),
      label: new f.StringField({initial: "dc20rpg.properties.rigid"})
    }),
    grasp: new f.SchemaField({
      active: new f.BooleanField({required: true, initial: false}),
      label: new f.StringField({initial: "dc20rpg.properties.grasp"})
    }),
    toss: new f.SchemaField({
      active: new f.BooleanField({required: true, initial: false}),
      label: new f.StringField({initial: "dc20rpg.properties.toss"})
    }),
    mounted: new f.SchemaField({
      active: new f.BooleanField({required: true, initial: false}),
      label: new f.StringField({initial: "dc20rpg.properties.mounted"})
    }),
  }
}

class SaveFields extends foundry.data.fields.SchemaField {
  constructor(fields={}, options={}) {
    const f = foundry.data.fields;

    fields = {
      type: new f.StringField({required: true, initial: "phy"}),
      dc: new f.NumberField({ required: true, nullable: true, integer: true, initial: 0 }),
      calculationKey: new f.StringField({required: true, initial: "spell"}),
      addMastery: new f.BooleanField({required: true, initial: false}),
      failEffect: new f.StringField({required: true, initial: ""}), // Left for backward compatibility
      ...fields
    };
    super(fields, options);
  }
}

class UseCostFields extends foundry.data.fields.SchemaField {
  constructor(fields={}, options={}) {
    const f = foundry.data.fields;
    const initNull = { required: true, nullable: true, integer: true, initial: null };

    fields = {
      resources: new f.SchemaField({
        actionPoint: new f.NumberField(initNull),
        stamina: new f.NumberField(initNull),
        mana: new f.NumberField(initNull),
        health: new f.NumberField(initNull),
        grit: new f.NumberField(initNull),
        restPoints: new f.NumberField(initNull),
        custom: new f.ObjectField({required: true}),
      }),
      charges: new f.SchemaField({
        current: new f.NumberField(initNull),
        max: new f.NumberField(initNull),
        maxChargesFormula: new f.StringField({required: true, nullable: true, initial: null}),
        overriden: new f.BooleanField({required: true, initial: false}),
        rechargeFormula: new f.StringField({required: true, nullable: false, initial: ""}),
        rechargeDice: new f.StringField({required: true, nullable: false, initial: ""}),
        requiredTotalMinimum: new f.NumberField(initNull),
        reset: new f.StringField({required: true, nullable: false, initial: ""}),
        showAsResource: new f.BooleanField({required: true, initial: false}),
        subtract: new f.NumberField({ required: true, nullable: false, integer: true, initial: 1 }),
      }),
      otherItem: new f.SchemaField({
        itemId: new f.StringField({required: true, initial: ""}),
        amountConsumed: new f.NumberField({ required: true, nullable: true, integer: true, initial: 0 }),
        consumeCharge: new f.BooleanField({required: true, initial: true}),
      })
    };
    super(fields, options);
  }
}

class UsesWeaponFields extends foundry.data.fields.SchemaField {
  constructor(fields={}, options={}) {
    const f = foundry.data.fields;

    fields = {
      weaponAttack: new f.BooleanField({required: true, initial: false}),
      weaponId: new f.StringField({required: true, initial: ""}),
      ...fields
    };
    super(fields, options);
  }
}

class DC20BaseItemData extends foundry.abstract.TypeDataModel {
  static defineSchema() {
    const f = foundry.data.fields;

    return {
      itemKey: new f.StringField({required: true, initial: ""}),
      description: new f.StringField({required: true, initial: ""}),
      tableName: new f.StringField({required: true, initial: ""}),
      source: new f.StringField({required: true, initial: ""}),
      choicePointCost: new f.NumberField({ required: true, nullable: false, integer: true, initial: 1 }),
      requirements: new f.SchemaField({
        level: new f.NumberField({ required: true, nullable: true, integer: true, initial: 1 }),
        items: new f.StringField({required: true, initial: ""}),
      }),
      hideFromCompendiumBrowser: new f.BooleanField({required: true, initial: false}),
      quickRoll: new f.BooleanField({required: true, initial: false}),
      macros: new f.ObjectField({required: true})
    }
  }

  static mergeSchema(a, b) {
    Object.assign(a, b);
    return a;
  }

  static migrateData(source) {
    if (source.effectsConfig?.toggleable) {
      const effectConfig = source.effectsConfig;
      source.toggle = {
        toggleable: true,
        toggledOn: false,
        toggleOnRoll: false
      };
      source.effectsConfig.linkWithToggle = effectConfig.toggleable;
      delete source.effectsConfig.toggleable;
      if (source.conditional?.connectedToEffects) {
        source.conditional.linkWithToggle = source.conditional.connectedToEffects;
        delete source.conditional.connectedToEffects;
      }
    }
    super.migrateData(source);
  }
}

class DC20UsableItemData extends DC20BaseItemData {
  static defineSchema() {
    const f = foundry.data.fields;
  
    return this.mergeSchema(super.defineSchema(), {
      isReaction: new f.BooleanField({required: true, initial: false}),
      help: new f.SchemaField({
        ignoreMHP: new f.BooleanField({required: true, initial: false}),
        subtract: new f.BooleanField({required: true, initial: false}),
        doNotExpire: new f.BooleanField({required: true, initial: false}),
      }),
      toggle: new f.SchemaField({
        toggleable: new f.BooleanField({required: true, initial: false}),
        toggledOn: new f.BooleanField({required: true, initial: false}),
        toggleOnRoll: new f.BooleanField({required: true, initial: false}),
      }),
      actionType: new f.StringField({required: true, initial: ""}),
      attackFormula: new AttackFormulaFields(),
      check: new CheckFields(),
      save: new SaveFields(),
      costs: new UseCostFields(),
      againstEffect: new f.SchemaField({
        id: new f.StringField({required: true, initial: ""}),
        supressFromChatMessage: new f.BooleanField({required: true, initial: false}),
        untilYourNextTurnStart: new f.BooleanField({required: true, initial: false}),
        untilYourNextTurnEnd: new f.BooleanField({required: true, initial: false}),
        untilTargetNextTurnStart: new f.BooleanField({required: true, initial: false}),
        untilTargetNextTurnEnd: new f.BooleanField({required: true, initial: false}),
        untilFirstTimeTriggered: new f.BooleanField({required: true, initial: false}),
      }), // Left for backward compatibility
      againstStatuses: new f.ObjectField({required: true}),
      rollRequests: new f.ObjectField({required: true}),
      formulas: new f.ObjectField({required: true}),
      enhancements: new f.ObjectField({required: true}),
      copyEnhancements: new f.SchemaField({
        copy: new f.BooleanField({required: true, initial: false}),
        copyFor: new f.StringField({required: true, initial: ""}),
        linkWithToggle: new f.BooleanField({required: true, initial: false}),
        hideFromRollMenu: new f.BooleanField({required: true, initial: false}),
      }),
      range: new f.SchemaField({
        melee: new f.NumberField({ required: true, nullable: true, integer: true, initial: 1 }),
        normal: new f.NumberField({ required: true, nullable: true, integer: true, initial: null }),
        max: new f.NumberField({ required: true, nullable: true, integer: true, initial: null }),
        unit: new f.StringField({required: true, initial: ""}),
      }),
      duration: new f.SchemaField({
        value: new f.NumberField({ required: true, nullable: true, integer: true, initial: null }),
        type: new f.StringField({required: true, initial: ""}),
        timeUnit: new f.StringField({required: true, initial: ""}),
      }),
      target: new f.SchemaField({
        count: new f.NumberField({ required: true, nullable: true, integer: true, initial: null }),
        type: new f.StringField({required: true, initial: ""}),
        areas: new f.ObjectField({required: true, initial: {
          default: {
            area: "",
            distance: null,
            width: null,
            unit: "",
            difficult: false,
          }
        }})
      }),
      conditional: new ConditionalFields(), // Left for backward compatibility
      conditionals: new f.ObjectField({required: true}),
      hasAdvancement: new f.BooleanField({required: false, initial: false}),
      provideMartialExpansion: new f.BooleanField({required: false, initial: false}),
      advancements: new f.ObjectField({required: true, initial: {
        default: {
          name: "Item Advancement",
          mustChoose: false,
          pointAmount: 1,
          level: 0,
          applied: false,
          talent: false,
          repeatable: false,
          repeatAt: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
          allowToAddItems: false,
          additionalAdvancement: true,
          compendium: "",
          preFilters: "",
          tip: "",
          items: {}
        }
      }})
    })
  }
}

class DC20ItemItemData extends DC20BaseItemData {
  static defineSchema() {
    const f = foundry.data.fields;

    return this.mergeSchema(super.defineSchema(), {
      quantity: new f.NumberField({ required: true, nullable: false, integer: true, initial: 1 }),
      weight: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
      price: new f.SchemaField({
        value: new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
        currency: new f.StringField({required: true, initial: "gp"})
      }),
      rarity: new f.StringField({required: true, initial: ""}),
      statuses: new f.SchemaField({
        attuned: new f.BooleanField({required: true, initial: false}),
        equipped: new f.BooleanField({required: true, initial: false}),
        identified: new f.BooleanField({required: true, initial: true}),
      }),
      properties: new PropertyFields(),
      effectsConfig: new EffectsConfigFields({mustEquip: new f.BooleanField({required: true, initial: true})})
    })
  }
}

class DC20ItemUsableMergeData extends DC20BaseItemData {
  static defineSchema() {
    const itemData = DC20ItemItemData.defineSchema();
    const usableData = DC20UsableItemData.defineSchema();

    return {
      ...itemData,
      ...usableData,
    }
  }
}

class DC20UniqueItemData extends DC20BaseItemData {
  static defineSchema() {
    const f = foundry.data.fields;
  
    return this.mergeSchema(super.defineSchema(), {
      scaling: new f.ObjectField({required: true}),
      advancements: new f.ObjectField({required: true}),
    })
  }

  static migrateData(source) {
    if (source.advancements) {
      const entries = Object.entries(source.advancements);
      for (const [key, advancement] of entries) {
        if (advancement.repeatAt === undefined) {
          source.advancements[key].repeatAt = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];
        }
      }
    }
    return super.migrateData(source);
  }
}

class DC20WeaponData extends DC20ItemUsableMergeData {
  static defineSchema() {
    const f = foundry.data.fields;
  
    return this.mergeSchema(super.defineSchema(), {
      weaponStyle: new f.StringField({required: true, initial: ""}),
      weaponType: new f.StringField({required: true, initial: ""}),
      weaponStyleActive: new f.BooleanField({required: true, initial: false}),
      properties: new PropertyFields("weapon"),
    })
  }
}

class DC20EquipmentData extends DC20ItemUsableMergeData {
  static defineSchema() {
    const f = foundry.data.fields;
  
    return this.mergeSchema(super.defineSchema(), {
      equipmentType: new f.StringField({required: true, initial: ""}),
      properties: new PropertyFields("equipment"),
    })
  }
}

class DC20ConsumableData extends DC20ItemUsableMergeData {
  static defineSchema() {
    const f = foundry.data.fields;
  
    return this.mergeSchema(super.defineSchema(), {
      consumableType: new f.StringField({required: true, initial: ""}),
      consume: new f.BooleanField({required: true, initial: true}),
      deleteOnZero: new f.BooleanField({required: true, initial: true}),
      showAsResource: new f.BooleanField({required: true, initial: false}),
    })
  }
}

class DC20LootData extends DC20ItemItemData {
  static defineSchema() {
    return super.defineSchema();
  }
}

class DC20FeatureData extends DC20UsableItemData {
  static defineSchema() {
    const f = foundry.data.fields;
  
    return this.mergeSchema(super.defineSchema(), {
      featureType: new f.StringField({required: true, initial: ""}),
      featureOrigin: new f.StringField({required: true, initial: ""}),
      featureSourceItem: new f.StringField({required: true, initial: ""}),
      staminaFeature: new f.BooleanField({required: true, initial: false}),
      flavorFeature: new f.BooleanField({required: true, initial: false}),
      isResource: new f.BooleanField({required: true, initial: false}),
      resource: new f.SchemaField({
        name: new f.StringField({required: true, initial: ""}),
        resourceKey: new f.StringField({required: true, initial: "key"}),
        reset: new f.StringField({required: true, initial: ""}),
        useStandardTable: new f.BooleanField({required: true, initial: true}),
        customMaxFormula: new f.StringField({required: true, initial: ""}),
        values: new f.ArrayField(
          new f.NumberField({ required: true, nullable: false, integer: true, initial: 0 }), {
            required: true,
            initial: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
          }),
      }),
      usesWeapon: new UsesWeaponFields(),
      effectsConfig: new EffectsConfigFields()
    })
  }
}

class DC20BasicActionData extends DC20UsableItemData {
  static defineSchema() {
    const f = foundry.data.fields;

    return this.mergeSchema(super.defineSchema(), {
      category: new f.StringField({required: true, initial: ""}),
      hideFromCompendiumBrowser: new f.BooleanField({required: true, initial: true}),
      effectsConfig: new EffectsConfigFields()
    })
  }
}

class DC20TechniqueData extends DC20UsableItemData {
  static defineSchema() {
    const f = foundry.data.fields;
  
    return this.mergeSchema(super.defineSchema(), {
      techniqueType: new f.StringField({required: true, initial: ""}),
      techniqueOrigin: new f.StringField({required: true, initial: ""}),
      knownLimit: new f.BooleanField({required: true, initial: true}),
      usesWeapon: new UsesWeaponFields(),
      effectsConfig: new EffectsConfigFields()
    })
  }
}

class DC20SpellData extends DC20UsableItemData {
  static defineSchema() {
    const f = foundry.data.fields;
  
    return this.mergeSchema(super.defineSchema(), {
      spellType: new f.StringField({required: true, initial: ""}),
      spellOrigin: new f.StringField({required: true, initial: ""}),
      magicSchool: new f.StringField({required: true, initial: ""}),
      knownLimit: new f.BooleanField({required: true, initial: true}),
      attackFormula: new AttackFormulaFields({checkType: new f.StringField({required: true, initial: "spell"})}),
      usesWeapon: new UsesWeaponFields(),
      effectsConfig: new EffectsConfigFields(),
      spellLists: new f.SchemaField({
        arcane: new f.SchemaField({
          active: new f.BooleanField({required: true, initial: false}),
          label: new f.StringField({initial: "dc20rpg.spellList.arcane"})
        }),
        divine: new f.SchemaField({
          active: new f.BooleanField({required: true, initial: false}),
          label: new f.StringField({initial: "dc20rpg.spellList.divine"})
        }),
        primal: new f.SchemaField({
          active: new f.BooleanField({required: true, initial: false}),
          label: new f.StringField({initial: "dc20rpg.spellList.primal"})
        }),
      }),
      components: new f.SchemaField({
        verbal: new f.SchemaField({
          active: new f.BooleanField({required: true, initial: false}),
          char: new f.StringField({required: true, initial: "V"}),
          label: new f.StringField({initial: "dc20rpg.spellComponent.verbal"})
        }),
        somatic: new f.SchemaField({
          active: new f.BooleanField({required: true, initial: false}),
          char: new f.StringField({required: true, initial: "S"}),
          label: new f.StringField({initial: "dc20rpg.spellComponent.somatic"})
        }),
        material: new f.SchemaField({
          active: new f.BooleanField({required: true, initial: false}),
          char: new f.StringField({required: true, initial: "M"}),
          description: new f.StringField({required: true, initial: ""}),
          cost: new f.StringField({required: true, initial: ""}),
          consumed: new f.BooleanField({required: true, initial: false}),
          label: new f.StringField({initial: "dc20rpg.spellComponent.material"})
        })
      }),
      tags: new f.SchemaField({
        fire: new f.SchemaField({
          active: new f.BooleanField({required: true, initial: true}),
          label: new f.StringField({initial: "dc20rpg.spellTags.fire"})
        }),
      }),
    })
  }
}

class DC20ClassData extends DC20UniqueItemData {
  static defineSchema() {
    const f = foundry.data.fields;
  
    return this.mergeSchema(super.defineSchema(), {
      level: new f.NumberField({ required: true, nullable: false, integer: true, initial: 1 }),
      combatTraining: new CombatTraining(),
      bannerImg: new f.StringField({required: false, initial: ""}),
      martial: new f.BooleanField({required: true, initial: false}),
      spellcaster: new f.BooleanField({required: true, initial: false}),
      martialExpansion: new f.BooleanField({required: true, initial: false}),
      talentMasteries: new f.ArrayField(
        new f.StringField({required: true, initial: ""}), {
          required: true,
          initial: ["","","","","","","","","","","","","","","","","","","",""]
      }),
      scaling: new f.ObjectField({required: true, initial: {
        maxHpBonus: {
          label: "dc20rpg.scaling.maxHpBonus",
          values: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
        },
        bonusStamina: {
          label: "dc20rpg.scaling.bonusStamina",
          values: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
        },
        bonusMana: {
          label: "dc20rpg.scaling.bonusMana",
          values: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
        },
        skillPoints: {
          label: "dc20rpg.scaling.skillPoints",
          values: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
        },
        tradePoints: {
          label: "dc20rpg.scaling.tradePoints",
          values: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
        },
        attributePoints: {
          label: "dc20rpg.scaling.attributePoints",
          values: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
        },
        maneuversKnown: {
          label: "dc20rpg.scaling.maneuversKnown",
          values: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
        },
        techniquesKnown: {
          label: "dc20rpg.scaling.techniquesKnown",
          values: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
        },
        cantripsKnown: {
          label: "dc20rpg.scaling.cantripsKnown",
          values: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
        },
        spellsKnown: {
          label: "dc20rpg.scaling.spellsKnown",
          values: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
        }
      }}),
      startingEquipment: new f.SchemaField({
        weapons: new f.StringField({required: true, initial: ""}),
        armor: new f.StringField({required: true, initial: ""}),
        other: new f.StringField({required: true, initial: ""})
      }),
    })
  }
}

class DC20SubclassData extends DC20UniqueItemData {
  static defineSchema() {
    const f = foundry.data.fields;
  
    return this.mergeSchema(super.defineSchema(), {
      forClass: new f.SchemaField({
        classSpecialId: new f.StringField({required: true, initial: ""}),
        name: new f.StringField({required: true, initial: ""}),
      })
    })
  }
}

class DC20AncestryData extends DC20UniqueItemData {
  static defineSchema() {
    const f = foundry.data.fields;
  
    return this.mergeSchema(super.defineSchema(), {
      movement: new f.SchemaField({
        speed: new f.NumberField({ required: true, nullable: false, integer: true, initial: 5 })
      })
    })
  }

  static migrateData(source) {
    if (source.size) {
      delete source.size;
    }
    return super.migrateData(source);
  }
}

class DC20BackgroundData extends DC20UniqueItemData {
  static defineSchema() {
    const f = foundry.data.fields;
  
    return this.mergeSchema(super.defineSchema(), {
      skillPoints: new f.NumberField({ required: true, nullable: false, integer: true, initial: 5 }),
      tradePoints: new f.NumberField({ required: true, nullable: false, integer: true, initial: 3 }),
      langPoints: new f.NumberField({ required: true, nullable: false, integer: true, initial: 2 }),
    })
  }
}

class CharacterCreationWizard extends Dialog {

  constructor(dialogData = {}, options = {}) {
    super(dialogData, options);
    this.actorData = {
      name: "",
      img: "icons/svg/mystery-man.svg",
      attributes: {
        mig: {
          value: -2,
          mastery: false,
          label: "Might"
        },
        agi: {
          value: -2,
          mastery: false,
          label: "Agility"
        },
        cha: {
          value: -2,
          mastery: false,
          label: "Charisma"
        },
        int: {
          value: -2,
          mastery: false,
          label: "Inteligence"
        }
      },
      attrPoints: {
        pointsLeft: 12,
        manual: false,
      },
      class: {
        _id: "",
        name: "Class",
        img: "icons/svg/mystery-man.svg"
      },
      ancestry: {
        _id: "",
        name: "Ancestry",
        img: "icons/svg/angel.svg"
      },
      background: {
        _id: "",
        name: "Background",
        img: "icons/svg/village.svg"
      },
      inventory: {
        weapons: {
          text: "",
          items: {},
          packName: "weapons"
        },
        armor: {
          text: "",
          items: {},
          packName: "armor"
        },
        other: {
          text: "",
          items: {},
          packName: "other"
        }
      },
    };
    this.step = 0;
    this.fromCompendium = {};
    this._collectFutureData();
  }

  static get defaultOptions() {
    return foundry.utils.mergeObject(super.defaultOptions, {
      template: "systems/dc20rpg/templates/dialogs/character-progress/character-creation-wizard.hbs",
      classes: ["dc20rpg", "dialog"],
      width: 1200,
      height: 800,
      resizable: true,
      draggable: true,
    });
  }

  async getData() {
    switch(this.step) {
      case 0: return this._basic();
      case 1: return await this._uniqueItems("ancestry");
      case 2: return await this._uniqueItems("background")
      case 3: return await this._uniqueItems("class");
      case 4: return this._equipment();
    }
  }

  _basic() {
    const disableNext = this.actorData.name === "" || !(this.actorData.attrPoints.manual || this.actorData.attrPoints.pointsLeft === 0);
    return {
      disableNext: disableNext,
      actorData: this.actorData,
      currentStep: this.step,
    }
  }

  _equipment() {
    const startingEquipment = this.actorData["class"]?.system?.startingEquipment;
    if (startingEquipment) {
      this.actorData.inventory.weapons.text = startingEquipment.weapons;
      this.actorData.inventory.armor.text = startingEquipment.armor;
      this.actorData.inventory.other.text = startingEquipment.other;
    }

    return {
      inventory: this.actorData.inventory,
      actorData: this.actorData,
      currentStep: this.step,
      createActorRequestSend: this.createActorRequestSend
    }
  }

  async _uniqueItems(itemType) {
    // Check, maybe we already collected that item type
    let collectedItems = [];
    if (this.fromCompendium[itemType] !== undefined) {
      collectedItems = this.fromCompendium[itemType];
    }
    else {
      collectedItems = await this._collectItemsFor(itemType);
      this.fromCompendium[itemType] = collectedItems;
    }
    const selectedItem = this.actorData[itemType];

    let shouldDisable = false;
    if (selectedItem._id === "") {
      shouldDisable = true;
      selectedItem.descriptionHTML = "<p>Select Item</p>";
    }
    else {
      selectedItem.descriptionHTML = await TextEditor.enrichHTML(selectedItem.system.description, {secrets:true});
    }

    return {
      disableNext: shouldDisable,
      actorData: this.actorData,
      currentStep: this.step,
      itemType: itemType,
      collectedItems: collectedItems,
      selectedItem: selectedItem
    }
  }

  async _collectFutureData() {
    const classes = [];
    const ancestries = [];
    const backgrounds = [];

    const hideItems = game.dc20rpg.compendiumBrowser.hideItems;
    for (const pack of game.packs) {
      if (pack.documentName === "Item") {
        const packageType = pack.metadata.packageType;
        const items = await pack.getDocuments();
        items.filter(item => ["ancestry", "background", "class"].includes(item.type))
          .forEach(item => {
            if (packageType === "system" && hideItems.has(item.id)) return;
            if (item.type === "ancestry") ancestries.push(item);
            if (item.type === "background") backgrounds.push(item);
            if (item.type === "class") classes.push(item);
          });
      }
    }
    this._sort(ancestries);
    this._sort(backgrounds);
    this._sort(classes);
    this.fromCompendium["ancestry"] = ancestries;
    this.fromCompendium["background"] = backgrounds;
    this.fromCompendium["class"] = classes;
  }

  _sort(array) {
    array.sort(function(a, b) {
      const textA = a.name.toUpperCase();
      const textB = b.name.toUpperCase();
      return (textA < textB) ? -1 : (textA > textB) ? 1 : 0;
    });
  }

  async _collectItemsFor(itemType) {
    const collected = [];
    for (const pack of game.packs) {
      if (pack.documentName === "Item") {
        const items = await pack.getDocuments();
        const found = items.filter(item => item.type === itemType);
        collected.push(...found);
      }
    }
    return collected;
  }

  activateListeners(html) {
    super.activateListeners(html);
    html.find(".image-picker").click(() => this._onImagePicker());
    html.find(".input-text").change(ev => setValueForPath(this, datasetOf(ev).path, valueOf(ev)));
    html.find(".input").change(ev => setValueForPath(this, datasetOf(ev).path, parseInt(valueOf(ev))));
    html.find(".input-numeric").change(ev => this._onNumericValueChange(datasetOf(ev).path, valueOf(ev)));
    html.find(".manual-switch").click(ev => this._onManualSwitch());
    html.find(".add-attr").click(ev => this._onAttrChange(datasetOf(ev).key, true));
    html.find(".sub-attr").click(ev => this._onAttrChange(datasetOf(ev).key, false));

    html.find(".select-row").click(ev => this._onSelectRow(datasetOf(ev).index, datasetOf(ev).type));
    html.find('.open-compendium').click(ev => createItemBrowser("inventory", false, this));
    html.find(".remove-item").click(ev => this._onItemRemoval(datasetOf(ev).itemKey, datasetOf(ev).storageKey));

    html.find(".next").click(ev => this._onNext(ev));
    html.find(".back").click(ev => this._onBack(ev));
    html.find(".create-actor").click(ev => this._onActorCreate(ev));
    html.find('.mix-ancestry').click(async () => {
      const ancestryData = await createMixAncestryDialog();
      if (!ancestryData) return;
      ancestryData._id = generateKey();
      ancestryData.merged = true;
      this.fromCompendium["ancestry"].unshift(ancestryData);
      this.render();
    });
    html.find('.content-link').hover(async ev => itemTooltip(await this._itemFromUuid(datasetOf(ev).uuid), ev, html, {position: this._getTooltipPosition(html)}), ev => hideTooltip(ev, html));

    // Drag and drop events
    html[0].addEventListener('dragover', ev => ev.preventDefault());
    html[0].addEventListener('drop', async ev => await this._onDrop(ev));
  }

  _onImagePicker() {
    new FilePicker({
      type: "image",
      displayMode: "tiles",
      callback: (path) => {
        if (!path) return;
        this.actorData.img = path;
        this.render();
      }
    }).render();
  }

  _onManualSwitch() {
    const willBeManual = !this.actorData.attrPoints.manual;
    this.actorData.attrPoints.manual = willBeManual;

    if (!willBeManual) {
      this.actorData.attributes.mig.value = -2; 
      this.actorData.attributes.agi.value = -2;
      this.actorData.attributes.cha.value = -2;
      this.actorData.attributes.int.value = -2;
      this.actorData.attrPoints.pointsLeft = 12;
    }
    this.render();
  }

  _onAttrChange(key, add) {
    const current = this.actorData.attributes[key].value;
    if (add && (current < 3) && (this.actorData.attrPoints.pointsLeft > 0)) {
      this.actorData.attributes[key].value = current + 1;
      this.actorData.attrPoints.pointsLeft--;
    }
    else if (!add && (current > -2)){
      this.actorData.attributes[key].value = current - 1;
      this.actorData.attrPoints.pointsLeft++;
    }
    this.render();
  }

  _onSelectRow(index, itemType) {
    const items = this.fromCompendium[itemType];
    this.actorData[itemType] = items[index];
    this.render();
  }

  _onNumericValueChange(pathToValue, value) {
    const numericValue = parseInt(value);
    setValueForPath(this, pathToValue, numericValue);
    this.render();
  }

  _onBack(event) {
    event.preventDefault();
    this.step--;
    this.render();
  }

  _onNext(event) {
    event.preventDefault();
    this.step++;
    this.render();
  }

  async _onActorCreate(event) {
    event.preventDefault();

    // We dont want advancement window to appear after every unique item added
    await game.settings.set("dc20rpg", "suppressAdvancements", true);
    // Create actor
    const actor = await this._createActor();
    if (!actor) {
      await game.settings.set("dc20rpg", "suppressAdvancements", false);
      return;
    }

    // Update actor current HP
    const maxHP = actor.system.resources.health.max;
    await actor.update({["system.resources.health.current"]: maxHP});

    // Add items to actor
    await createItemOnActor(actor, this.actorData.ancestry);
    await createItemOnActor(actor, this.actorData.background);
    await createItemOnActor(actor, this.actorData.class);

    for (const pack of Object.values(this.actorData.inventory)) {
      for (const item of Object.values(pack.items)) {
        await createItemOnActor(actor, item);
      }
    }

    this.close();
    await game.settings.set("dc20rpg", "suppressAdvancements", false);

    await actor.sheet.render(true, { focus: false });
    // Sometimes we need to force advancement window to appear
    if (actor.system.details.class.id !== "") runAdvancements(actor, 1);
  }

  async _createActor() {
    const actorData = this._prepareActorData();
    if (game.user.isGM) return await Actor.create(actorData);

    const activeGM = game.users.activeGM;
    if (!activeGM) {
      ui.notifications.error("There is no active GM. Actor cannot be created.");
      return;
    }

    game.socket.emit('system.dc20rpg', { 
      actorData: actorData,
      gmUserId: activeGM.id,
      type: "createActor"
    });
    this.createActorRequestSend = true;
    this.render();

    const actorId = await responseListener("actorCreated", {emmiterId: game.user.id});
    return game.actors.get(actorId);
  }

  _prepareActorData() {
    const attributes = this.actorData.attributes;
    const maxPoints = 8 + attributes.mig.value + attributes.agi.value + attributes.cha.value + attributes.int.value;

    return {
      forceCreate: true,
      name: this.actorData.name,
      type: "character",
      img: this.actorData.img,
      ownership: {
        [game.user.id]: CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER,
      },
      system: {
        attributes: {
          mig: {
            current: attributes.mig.value
          },
          agi: {
            current: attributes.agi.value
          },
          cha: {
            current: attributes.cha.value
          },
          int: {
            current: attributes.int.value
          },
        },
        attributePoints: {
          max: maxPoints
        }
      }
    }
  }

  async _onDrop(event) {
    event.preventDefault();
    const droppedData  = event.dataTransfer.getData('text/plain');
    if (!droppedData) return;
    
    const droppedObject = JSON.parse(droppedData);
    if (droppedObject.type !== "Item") return;

    const item = await Item.fromDropData(droppedObject);
    const itemKey = generateKey();
    if (item.type === "weapon") this.actorData.inventory.weapons.items[itemKey] = item.toObject();
    else if (item.type === "equipment") this.actorData.inventory.armor.items[itemKey] = item.toObject();
    else if (["consumable", "loot"].includes(item.type)) this.actorData.inventory.other.items[itemKey] = item.toObject();
    this.render();
  }

  _onItemRemoval(itemKey, storagekey) {
    delete this.actorData.inventory[storagekey].items[itemKey];
    this.render();
  }

  async _itemFromUuid(uuid) {
    const item = await fromUuid(uuid);
    return item;
  }

  async _render(...args) {
    let scrollPosition = 0;

    let selector = this.element.find('.item-selector');
    if (selector.length > 0) {
      scrollPosition = selector[0].scrollTop;
    }
    
    await super._render(...args);
    
    // Refresh selector
    selector = this.element.find('.item-selector');
    if (selector.length > 0) {
      selector[0].scrollTop = scrollPosition;
    }
  }

  setPosition(position) {
    super.setPosition(position);

    this.element.css({
      "min-height": "600px",
      "min-width": "800px",
    });
    this.element.find("#character-creation-wizard").css({
      height: this.element.height() -30,
    });
  }

  _getTooltipPosition(html) {
    let position = null;
    const left = html.find(".left-column");
    if (left[0]) {
      position = {
        width: left.width() - 25,
        height: left.height() - 20,
      };
    }
    return position;
  }
}

function characterCreationWizardDialog() {
  const dialog = new CharacterCreationWizard({title: `Character Creation Wizard`});
  dialog.render(true);
}

function characterWizardButton(html) {
  const button = `${
  `<button class="character-creation-wizard" style="margin: 5px; width: -webkit-fill-available;">
    <i class="fa-solid fa-hat-wizard"></i>
    Open Character Creation Wizard
  </button>`
  }`;
  
  const createActorButton = html.find(".header-search");
  createActorButton.before(button);
  html.find(".character-creation-wizard").click(() => characterCreationWizardDialog());
}

class DC20RpgTokenDocument extends TokenDocument {

  /**@override*/
  prepareData() {
    this._prepareSystemSpecificVisionModes();
    this._setTokenSize();
    super.prepareData();
    // Refresh existing token if exist
    if (this.object) this.object.refresh();
  }

  _prepareSystemSpecificVisionModes() {
    if (!this.sight.enabled) return; // Only when using vision
    const senses = this.actor.system.senses;
    const sight = this.sight;
    const detection = this.detectionModes;

    // Darkvision
    if (senses.darkvision.value > 0) {
      const defaults = CONFIG.Canvas.visionModes.darkvision.vision.defaults;
      if (sight.visionMode === "basic") sight.visionMode = "darkvision";
      if (senses.darkvision.value > sight.range) sight.range = senses.darkvision.value;
      if (sight.saturation === 0) sight.saturation = defaults.saturation;
    }

    // Tremorsense
    if (senses.tremorsense.value > 0) {
      detection.push({
        id: "feelTremor",
        enabled: true,
        range: senses.tremorsense.value
      });
    }

    // Blindsight
    if (senses.blindsight.value > 0) {
      detection.push({
        id: "seeInvisibility",
        enabled: true,
        range: senses.blindsight.value
      });
    }

    // Truesight
    if (senses.truesight.value > 0) {
      detection.push({
        id: "seeAll",
        enabled: true,
        range: senses.truesight.value
      });
    }
  }

  _setTokenSize() {
    const size = this.actor.system.size;
    if (this.flags?.dc20rpg?.notOverrideSize) return;

    switch(size.size) {
      case "tiny":
        this.width = 0.5;
        this.height = 0.5;
        break;

      case "small": case "medium": case "mediumLarge":
        this.width = 1;
        this.height = 1;
        break;

      case "large":
        this.width = 2;
        this.height = 2;
        break;

      case "huge":
        this.width = 3;
        this.height = 3;
        break;

      case "gargantuan":
        this.width = 4;
        this.height = 4;
        break;

      case "colossal":
        this.width = 5;
        this.height = 5;
        break;

      case "titanic":
        this.width = 7;
        this.height = 7;
        break;
    }
  }

  hasStatusEffect(statusId) {
    return this.actor?.hasStatus(statusId) ?? false;
  }

  _onUpdate(changed, options, userId) {
    super._onUpdate(changed, options, userId);
    if (userId === game.user.id && this.actor) {
      if (changed.hasOwnProperty("x") || changed.hasOwnProperty("y")) {
        runEventsFor("move", this.actor);
        
        // Wait for movement to finish before triggering measured template check
        let counter = 0;  // Max amount of loops
        const timeoutID = setInterval(() => {
          if (counter > 100 || (
                (!changed.hasOwnProperty("x") || this.object.x === changed.x) && 
                (!changed.hasOwnProperty("y") || this.object.y === changed.y)
              )) {
            this.updateLinkedTemplates();
            checkMeasuredTemplateWithEffects();
            clearInterval(timeoutID);
          }
          else counter++;
        }, 100);
      }
    }
  }

  movementData = {};
  async _preUpdate(changed, options, user) {
    const freeMove = game.keyboard.downKeys.has("KeyF");
    const teleport = options.teleport;
    if ((changed.hasOwnProperty("x") || changed.hasOwnProperty("y")) && !freeMove && !teleport) {
      const startPosition = {x: this.x, y: this.y};
      if (!changed.hasOwnProperty("x")) changed.x = startPosition.x;
      if (!changed.hasOwnProperty("y")) changed.y = startPosition.y;

      const ignoreDT = game.settings.get("dc20rpg", "disableDifficultTerrain") || this.actor.system.globalModifier.ignore.difficultTerrain;
      const occupiedSpaces = this.object.getOccupiedGridSpaces();
      this.movementData = {
        moveCost: this.actor.system.moveCost,
        ignoreDT: ignoreDT
      };
      const costFunction = canvas.grid.isGridless 
                              ? (from, to, distance) => this.costFunctionGridless(from, to, distance, this.movementData, this.width) 
                              : (from, to, distance) => this.costFunctionGrid(from, to, distance, this.movementData, occupiedSpaces);
      const pathCost = canvas.grid.measurePath([startPosition, changed], {cost: costFunction}).cost;
      let subtracted = await subtractMovePoints(this, pathCost, options);
      // Spend extra AP to move
      if (subtracted !== true && game.settings.get("dc20rpg","askToSpendMoreAP")) {
        subtracted = await spendMoreApOnMovement(this.actor, subtracted);
      }
      // Snap to closest available position
      if (subtracted !== true && game.settings.get("dc20rpg","snapMovement")) {
        [subtracted, changed] = snapTokenToTheClosetPosition(this, subtracted, startPosition, changed, this.costFunctionGridless, this.costFunctionGrid);
      }
      // Do not move the actor
      if (subtracted !== true) {
        ui.notifications.warn("Not enough movement! If you want to make a free move hold 'F' key.");
        return false;
      }
    }
    super._preUpdate(changed, options, user);
  }

  costFunctionGrid(from, to, distance, movementData, occupiedSpaces) {
    const moveCost = movementData.moveCost;
    if (movementData.ignoreDT) return moveCost;

    // In the first iteration we want to prepare absolute spaces occupied by the token
    if (!movementData.absoluteSpaces) {
      movementData.absoluteSpaces = occupiedSpaces.map(space => [space[0] - from.j, space[1] - from.i]);
    }

    const absolute = movementData.absoluteSpaces;
    let lastDifficultTerrainSpaces = movementData.lastDifficultTerrainSpaces || 0;
    let currentDifficultTerrainSpaces = 0;
    for (let i = 0; i < absolute.length; i++) {
      if (DC20RpgMeasuredTemplate.isDifficultTerrain(absolute[i][1] + from.i, absolute[i][0] + from.j)) {
        currentDifficultTerrainSpaces++;
      }
    }
    movementData.lastDifficultTerrainSpaces = currentDifficultTerrainSpaces;

    // When we are reducing number of difficult terrain spaces in might mean that we are leaving difficult terrain
    if (currentDifficultTerrainSpaces > 0 && currentDifficultTerrainSpaces >= lastDifficultTerrainSpaces) return 1 + moveCost;
    return moveCost;
  }

  costFunctionGridless(from, to, distance, movementData, tokenWidth) {
    const moveCost = movementData.moveCost;
    let finalCost = 0;
    let traveled = 0;
    const gridSize = canvas.grid.size;
    const z = gridSize * tokenWidth;

    const travelPoints = getPointsOnLine(from.j, from.i, to.j, to.i, canvas.grid.size);
    for (let i = 0; i < travelPoints.length-1; i++) {
      if (movementData.ignoreDT) {
        finalCost += moveCost;
        traveled +=1;
      }
      else {
        const x = travelPoints[i].x + z/4;
        const y = travelPoints[i].y + z/4;
        if (DC20RpgMeasuredTemplate.isDifficultTerrain(x, y)) finalCost += 1;                   // Top Left
        else if (DC20RpgMeasuredTemplate.isDifficultTerrain(x + z/2, y)) finalCost += 1;        // Top Right
        else if (DC20RpgMeasuredTemplate.isDifficultTerrain(x + z/2, y + z/2)) finalCost += 1;  // Bottom Right
        else if (DC20RpgMeasuredTemplate.isDifficultTerrain(x, y + z/2)) finalCost += 1;        // Bottom Left
        else if (DC20RpgMeasuredTemplate.isDifficultTerrain(x + z/4, y + z/4)) finalCost += 1;  // Center
        finalCost += moveCost;
        traveled +=1;
      }
    }
    
    const distanceLeft = distance - traveled;
    if (distanceLeft >= 0.1) {
      if (movementData.ignoreDT) {
        finalCost += distanceLeft * moveCost;
      }
      else {
        const x = travelPoints[travelPoints.length-1].x;
        const y = travelPoints[travelPoints.length-1].y;
        let multiplier = 1;
        if (DC20RpgMeasuredTemplate.isDifficultTerrain(x, y)) multiplier = 2;                   // Top Left
        else if (DC20RpgMeasuredTemplate.isDifficultTerrain(x + z/2, y)) multiplier = 2;        // Top Right
        else if (DC20RpgMeasuredTemplate.isDifficultTerrain(x + z/2, y + z/2)) multiplier = 2;  // Bottom Right
        else if (DC20RpgMeasuredTemplate.isDifficultTerrain(x, y + z/2)) multiplier = 2;        // Bottom Left
        else if (DC20RpgMeasuredTemplate.isDifficultTerrain(x + z/4, y + z/4)) multiplier = 2;  // Center
        finalCost += distanceLeft * (multiplier + moveCost - 1);
      }
    }
    return finalCost;
  }

  async updateLinkedTemplates() {
    const linkedTemplates = this.flags.dc20rpg?.linkedTemplates;
    if (!linkedTemplates) return;
    
    const idsToRemove = new Set();
    for (const templateId of linkedTemplates) {
      const mt = canvas.templates.placeables.find(template => template.id === templateId);
      if (!mt) idsToRemove.add(templateId);
      else {
        await mt.document.update({
          skipUpdateCheck: true,
          x: this.object.center.x,
          y: this.object.center.y
        });
      }

      if (idsToRemove.size > 0) {
        const templatesLeft = new Set(linkedTemplates).difference(idsToRemove);
        this.update({["flags.dc20rpg.linkedTemplates"]: Array.from(templatesLeft)});
      } 
    }
  }
}

class ActorBrowser extends Dialog {

  constructor(dialogData = {}, options = {}) {
    super(dialogData, options);
    this.collectedActors = [];
    this.filteredActors = [];
    this.filters = getDefaultActorFilters();
    this._collectActors();
  }

  static get defaultOptions() {
    return foundry.utils.mergeObject(super.defaultOptions, {
      template: "systems/dc20rpg/templates/dialogs/compendium-browser/actor-browser.hbs",
      classes: ["dc20rpg", "dialog"],
      dragDrop: [
        {dragSelector: ".actor-row[data-uuid]", dropSelector: null},
      ],
      width: 850,
      height: 650,
      resizable: true,
      draggable: true,
    });
  }

  async getData() {
    const filters = Object.values(this.filters);
    const filteredActors = filterDocuments(this.collectedActors, Object.values(this.filters));
    return {
      collectedActors: filteredActors,
      filters: filters,
      collectingData: this.collectingData
    }
  }

  async _collectActors() {
    this.collectingData = true;
    this.collectedActors = await collectActors();
    this.collectingData = false;
    this.render(true);
  }

  activateListeners(html) {
    super.activateListeners(html);
    activateDefaultListeners(this, html);
    html.find(".show-actor").click(ev => this._showActor(datasetOf(ev).uuid));
    html.find(".import-actor").click(ev => this._onImportActor(ev));

    // Drag and drop events
    html[0].addEventListener('dragover', ev => ev.preventDefault());
  }

  _showActor(uuid) {
    const actor = fromUuidSync(uuid);
    if (actor) actor.sheet.render(true);
  }

  async _onImportActor(ev) {
    ev.stopPropagation();
    const actor = fromUuidSync(datasetOf(ev).uuid);
    if (!actor) return;
    const createdActor = await Actor.create(actor);
    createdActor.sheet.render(true);
  }

  async _render(...args) {
    const selector = this.element.find('.item-selector');
    let scrollPosition = 0;

    if (selector.length > 0) scrollPosition = selector[0].scrollTop;
    await super._render(...args);
    if (selector.length > 0) {
      this.element.find('.item-selector')[0].scrollTop = scrollPosition;
    }
  }

  _onDragStart(event) {
    const dataset = event.currentTarget.dataset;
    dataset.type = "Actor";
    event.dataTransfer.setData("text/plain", JSON.stringify(dataset));
  }

  setPosition(position) {
    super.setPosition(position);

    this.element.css({
      "min-height": "400px",
      "min-width": "600px",
    });
    this.element.find("#compendium-browser").css({
      height: this.element.height() -30,
    });
  }
}

let actorBrowserInstance = null;
function createActorBrowser() {
  if (actorBrowserInstance) actorBrowserInstance.close();
  const dialog = new ActorBrowser({title: `Actor Browser`});
  dialog.render(true);
  actorBrowserInstance = dialog;
}

function compendiumBrowserButton(html) {
  const itemButton =
  `<button class="open-item-browser" style="margin: 5px; width: -webkit-fill-available;">
    <i class="fa-solid fa-boxes-stacked"></i>
    Open Item Browser
  </button>`;
  const actorButton = 
  `<button class="open-actor-browser" style="margin: 5px; width: -webkit-fill-available;">
    <i class="fa-solid fa-user"></i>
    Open Actor Browser
  </button>`;
  
  const createActorButton = html.find(".header-search");
  createActorButton.before(itemButton);
  if (game.user.isGM) createActorButton.before(actorButton);
  html.find(".open-item-browser").click(() => createItemBrowser("weapon", false));
  html.find(".open-actor-browser").click(() => createActorBrowser());

  const compendiumFooter = html.find(".compendium-footer");
  for (const footer of compendiumFooter) {
    const comp = footer.children[0];
    if (comp && comp.textContent === " dc20rpg") {
      comp.innerHTML = "<i class='fa-solid fa-dice'></i> dc20";
    }
  }
  compendiumFooter[0].children[0].textContent;
}

class DC20RpgMacroConfig extends MacroConfig {

  /** @override */
  async _updateObject(event, formData) {
    event.preventDefault();
    if (this.object.flags?.dc20rpg?.temporaryMacro) {
      const data = this.object.flags.dc20rpg;
      if (data.item) {
        if (data.enhKey) await data.item.update({[`system.enhancements.${data.enhKey}.modifications.macros.${data.macroKey}`]: formData.command});
        else await data.item.update({[`system.macros.${data.key}.command`]: formData.command});
      }
      if (data.effect) await data.effect.update({[`flags.dc20rpg.macro`]: formData.command});
    }
    else {
      await super._updateObject(event, formData);
    }
  }
}

class DC20RpgTokenConfig extends TokenConfig {

  activateListeners(html) {
    super.activateListeners(html);

    const notOverrideSize = this.token?.flags?.dc20rpg?.notOverrideSize || false;
    const notOverrideChecked = notOverrideSize ? "fa-solid fa-square-check" : "fa-regular fa-square";
    const notOverrideSizeRow = `
    <div class="form-group slim" title="${game.i18n.localize("dc20rpg.sheet.tokenConfig.notOverrideSizeTitle")}">
      <label>${game.i18n.localize("dc20rpg.sheet.tokenConfig.notOverrideSize")}</label>
      <a class="override-save-checkbox fa-lg ${notOverrideChecked}" style="font-size:1.75em;display:flex;align-items:center;justify-content:end;"></a>
    </div>
    `;
    const formGroup = html.find('[data-tab="appearance"]').find(".form-group").first();
    formGroup.after(notOverrideSizeRow);

    html.find('.override-save-checkbox').click(() => this.token.update({["flags.dc20rpg.notOverrideSize"]: !notOverrideSize}));
  }
}

function expandEnrichHTML(oldFunction) {
  return (content, options={}) => {
    content = _parseInlineRolls(content);
    return oldFunction.call(TextEditor, content, options);
  }
}

function registerGlobalInlineRollListener() {
  document.body.addEventListener('click', ev => {
    if (!ev.target.classList.contains('roll-inline')) return;

    ev.preventDefault();
    const data = ev.target.dataset;
    const tokens = getSelectedTokens();
    if (tokens.length < 1) {
      if (data.rollType === "roll") _handleRoll(data);
      else ui.notifications.warn("You need to select at least one Token");
      return;
    }

    tokens.forEach(token => {
      switch(data.rollType) {
        case "save": _handleSave(data, token.actor); break;
        case "check": _handleCheck(data, token.actor); break;
        case "damage": _handleDamage(data, token); break;
        case "heal": _handleHealing(data, token); break;
        case "roll":  _handleRoll(data, token); break;
      }
    });
  });
}

function _parseInlineRolls(content) {
  if (!content) return content;

  const inlineRollRegex = /@(\w+)\[(\w+)\](?:{([^}]+)})?/g;
  const parsedHTML = content.replace(inlineRollRegex, (match, rollType, subtype, label) => {
    let icon = "fa-dice-d20";
    switch(rollType) {
      case "save": icon = "fa-shield"; break;
      case "check": icon = "fa-user-check"; break;
      case "damage": icon = "fa-droplet"; break;
      case "heal": icon = "fa-heart"; break;
    }

    let value = "";
    if (label && label.startsWith("&lt;")) {
      const decodedLabel = label.replace(/&lt;/g, "<").replace(/&gt;/g, ">");
      value = decodedLabel.match(/<([^>]*)>/)?.[1];
      label = decodedLabel.replace(/<[^>]*>/g, "").trim();
    }
    return `<a class="content-link roll-inline" data-roll-type="${rollType}" data-subtype="${subtype}" data-value="${value}"><i class="fa-solid ${icon}"></i> ${label || subtype}</a>`;
  });
  return parsedHTML;
}

async function _handleRoll(data, token) {
  const rollData = token ? token.actor.getRollData() : {};
  const formula = data.value;
  const roll = new Roll(formula, rollData);
  await roll.evaluate();
  roll.toMessage();
}

function _handleSave(data, actor) {
  const saveDetails = prepareSaveDetailsFor(data.subtype);
  promptRollToOtherPlayer(actor, saveDetails);
}

function _handleCheck(data, actor) {
  const checkDetails = prepareCheckDetailsFor(data.subtype);
  promptRollToOtherPlayer(actor, checkDetails);
}

function _handleDamage(data, token) {
  let dmg = {
    value: parseInt(data.value || 1),
    source: "Inline Roll",
    type: data.subtype
  };
  const target = tokenToTarget(token);
  dmg = calculateForTarget(target, {clear: {...dmg}, modified: {...dmg}}, {isDamage: true});
  applyDamage(token.actor, dmg.modified);
}

function _handleHealing(data, token) {
  let heal = {
    source: "Inline Roll",
    value: parseInt(data.value || 1),
    type: data.subtype
  };
  const target = tokenToTarget(token);
  heal = calculateForTarget(target, {clear: {...heal}, modified: {...heal}}, {isHealing: true});
  applyHealing(token.actor, heal.modified);
}

class ActorRequestDialog extends Dialog {

  constructor(title, selectOptions, request, onlyPC, dialogData = {}, options = {}) {
    super(dialogData, options);
    this.selected = "";
    this._collectActors(onlyPC);

    this.header = title;
    this.selectOptions = selectOptions;
    this.request = request;
  }

  static get defaultOptions() {
    return foundry.utils.mergeObject(super.defaultOptions, {
      template: "systems/dc20rpg/templates/dialogs/actor-request-dialog.hbs",
      classes: ["dc20rpg", "dialog"],
      width: 400
    });
  }

  _collectActors(onlyPC) {
    const activePlayersIds = getActivePlayers().map(user => user.id);

    // Go over actors and collect the ones belonging to active users
    const selectedActors = {};
    game.actors.forEach(actor => {
      if (Object.keys(actor.ownership).some(userId => activePlayersIds.includes(userId))) {
        if (onlyPC) if (actor.type !== "character") return;
        selectedActors[actor.id] = {
          selected: false,
          actor: actor
        };
      }
    });
    this.selectedActors = selectedActors;
  }

  getData() {
    const hasActors = Object.keys(this.selectedActors).length > 0;
    return {
      selectedActors: this.selectedActors,
      hasActors: hasActors,
      rollOptions: this.selectOptions,
      selected: this.selected,
      title: this.header
    };
  }

   /** @override */
  activateListeners(html) {
    super.activateListeners(html);
    html.find(".selectable").change(ev => this._onSelection(valueOf(ev)));
    html.find('.activable').click(ev => this._onActivable(datasetOf(ev).path));
    html.find('.confirm-request').click(ev => this._onConfirmRequest(ev));
  }

  _onSelection(value) {
    this.selected = value;
    this.render();
  }

  _onActivable(path) {
    let value = getValueFromPath(this, path);
    setValueForPath(this, path, !value);
    this.render();
 }

 _onConfirmRequest(event) {
    event.preventDefault();
    if (this.selected) this.request(this.selected, this.selectedActors);
    this.close();
  }
}

function createActorRequestDialog(title, selectOptions, request, onlyPC) {
  const dialog = new ActorRequestDialog(title, selectOptions, request, onlyPC, {title: "Actor Request"});
  dialog.render(true);
}

function rollRequest(selected, selectedActors) {
  let rollDetails = prepareCheckDetailsFor(selected);
  if (["agi", "mig", "cha", "int", "phy", "men"].includes(selected)) {
    rollDetails = prepareSaveDetailsFor(selected);
  }
  Object.values(selectedActors).forEach(actor => {
    if (actor.selected) promptRollToOtherPlayer(actor.actor, rollDetails, false);
  });
}

function restRequest(selected, selectedActors) {
  Object.values(selectedActors).forEach(actor => {
    if (actor.selected) openRestDialogForOtherPlayers(actor.actor, selected);
  });
}

class DmgCalculatorDialog extends Dialog {

  constructor(dialogData = {}, options = {}) {
    super(dialogData, options);
    this.calculationType = "";
    this.fall = {
      spaces: 1,
      acrCheckSucceeded: false,
      fallingAttack: false,
    };
    this.collision = {
      spaces: 1,
      shareDamage: false,
    };
    this.dmg = 0;
    this.token = getSelectedTokens()[0];
  }

  static get defaultOptions() {
    return foundry.utils.mergeObject(super.defaultOptions, {
      template: "systems/dc20rpg/templates/dialogs/dmg-calculator-dialog.hbs",
      classes: ["dc20rpg", "dialog", "flex-dialog"],
      width: 450
    });
  }

  getData() {
    this._calculateDamage();
    return {
      calculationTypes: {"fall": "Falling Damage", "collision": "Collision Damage"},
      calculationType: this.calculationType,
      token: this.token,
      fall: this.fall,
      collision: this.collision,
      dmg: this.dmg,
      dmgType: getLabelFromKey(this.dmgType, CONFIG.DC20RPG.DROPDOWN_DATA.damageTypes)
    };
  }

  _calculateDamage() {
    this.dmg = 0;
    const actor = this.token?.actor;

    if (this.calculationType === "fall") {
      const fall = this.fall;
      let agi = 0;
      if (actor) agi = Math.max(actor.system.attributes.agi.value, 0);
      if (fall.spaces <= agi) {
        this.dmgType = "true";
        return;
      }

      let fallDmg = fall.spaces;
      if (fall.fallingAttack) fallDmg = Math.ceil(fallDmg/2);
      if (fall.acrCheckSucceeded) fallDmg = fallDmg - agi;
      this.dmg = fallDmg;
      this.dmgType = "true";
    }

    if (this.calculationType === "collision") {
      const collision = this.collision;
      let collisionDamage = collision.spaces;
      if (actor) {
        const bludge = actor.system.damageReduction.damageTypes.bludgeoning;

        let multiplier = 1;
        if (bludge.vulnerability) multiplier = multiplier * 2;
        if (bludge.resistance) multiplier = multiplier * 0.5;
        if (collision.shareDamage) multiplier = multiplier * 0.5;
        if (bludge.immune) multiplier = 0;

        const valueX = bludge.vulnerable - bludge.resist;
        collisionDamage = (collisionDamage + valueX) * multiplier;
      }
      else if (collision.shareDamage) collisionDamage = Math.ceil(collisionDamage/2);
      this.dmg = collisionDamage;
      this.dmgType = "bludgeoning";
    }
  }

   /** @override */
  activateListeners(html) {
    super.activateListeners(html);
    activateDefaultListeners(this, html);
    html.find('.ask-for-acr-check').click(ev => this._onAcrobaticsCheck());
    html.find('.apply-damage').click(ev => this._onDmgApply(ev));
    html.find('.close-dialog').click(ev => {ev.preventDefault(); this.close();});
  }

  _onDmgApply(ev) {
    ev.preventDefault();
    if (!this.calculationType) {this.close(); return}
    const actor = this.token?.actor;
    if (!actor) {this.close(); return}

    const source = this.calculationType === "fall" ? "Falling Damage" : "Collision Damage";
    applyDamage(actor, {value: this.dmg, source: source, type: this.dmgType});
    this.close();
  }

  async _onAcrobaticsCheck() {
    const actor = this.token?.actor;
    if (!actor) return;
    
    const acr = CONFIG.DC20RPG.ROLL_KEYS.checks.acr;
    if (!acr) {
      ui.notifications.warn("Acrobatics is not a skill in your world, you cannot roll it.");
      return;
    }

    const against = 10 + this.fall.spaces;
    const details = {
      checkKey: "acr",
      label: getLabelFromKey("acr", CONFIG.DC20RPG.ROLL_KEYS.checks),
      roll: "d20+@skills.acr.modifier",
      type: "skillCheck",
      against: against
    };
    const roll = await promptRollToOtherPlayer(actor, details, true);
    if (roll && (roll.total >= against || roll.crit)) this.fall.acrCheckSucceeded = true;
    else this.fall.acrCheckSucceeded = false;
    this.render();
  }
}

function createDmgCalculatorDialog() {
  const dialog = new DmgCalculatorDialog({title: "Damage Calculator"});
  dialog.render(true);
}

async function createGmToolsMenu() {
  const gmToolsMenu = document.createElement('div');
  gmToolsMenu.id = "gm-tools";
  gmToolsMenu.appendChild(_restDialog());
  gmToolsMenu.appendChild(_rollReaquestButton());
  gmToolsMenu.appendChild(_dmgCalculator());

  const uiRightSidebar = document.querySelector('#ui-right').querySelector('#sidebar');
  if (uiRightSidebar) {
    uiRightSidebar.appendChild(gmToolsMenu);
  }
}

function _restDialog() {
  const restDialog = document.createElement('a');
  restDialog.innerHTML = '<i class="fa-solid fa-bed"></i>';
  restDialog.id = 'rest-request-button';
  restDialog.classList.add("gm-tool");
  restDialog.title = game.i18n.localize("dc20rpg.ui.sidebar.restRequest");

  restDialog.addEventListener('click', ev => {
    ev.preventDefault();
    createActorRequestDialog("Start Resting for", CONFIG.DC20RPG.DROPDOWN_DATA.restTypes, restRequest, true);
  });
  return restDialog;
}

function _rollReaquestButton() {
  const rollRequestButton = document.createElement('a');
  rollRequestButton.innerHTML = '<i class="fa-solid fa-dice"></i>';
  rollRequestButton.id = 'roll-request-button';
  rollRequestButton.classList.add("gm-tool");
  rollRequestButton.title = game.i18n.localize("dc20rpg.ui.sidebar.rollRequest");

  rollRequestButton.addEventListener('click', ev => {
    ev.preventDefault();
    createActorRequestDialog("Roll Request", CONFIG.DC20RPG.ROLL_KEYS.contests, rollRequest, false);
  });
  return rollRequestButton;
}

function _dmgCalculator() {
  const dmgCalculator = document.createElement('a');
  dmgCalculator.innerHTML = '<i class="fa-solid fa-calculator"></i>';
  dmgCalculator.id = 'dmg-calculator';
  dmgCalculator.classList.add("gm-tool");
  dmgCalculator.title = game.i18n.localize("dc20rpg.ui.sidebar.dmgCalculator");

  dmgCalculator.addEventListener('click', ev => {
    ev.preventDefault();
    createDmgCalculatorDialog();
  });
  return dmgCalculator;
}

/* -------------------------------------------- */
/*  Init Hook                                   */
/* -------------------------------------------- */
Hooks.once('init', async function() {
  registerGameSettings(game.settings); // Register game settings
  prepareColorPalette(); // Prepare Color Palette
  
  CONFIG.DC20RPG = DC20RPG;
  initDC20Config();
  prepareDC20tools();
  CONFIG.DC20Events = {};
  CONFIG.statusEffects = registerDC20Statues();
  CONFIG.specialStatusEffects.BLIND = "blinded";
  game.dc20rpg.compendiumBrowser = {
    hideItems: new Set(),
    hideActors: new Set()
  }; 

  // Define custom Document classes
  CONFIG.Actor.documentClass = DC20RpgActor;
  CONFIG.Item.documentClass = DC20RpgItem;
  CONFIG.Combatant.documentClass  = DC20RpgCombatant;
  CONFIG.Combat.documentClass = DC20RpgCombat;
  CONFIG.ui.combat = DC20RpgCombatTracker;
  CONFIG.ChatMessage.documentClass = DC20ChatMessage;
  CONFIG.ActiveEffect.documentClass = DC20RpgActiveEffect;
  CONFIG.ActiveEffect.legacyTransferral = false;
  CONFIG.Token.documentClass = DC20RpgTokenDocument;
  CONFIG.Token.hudClass = DC20RpgTokenHUD;
  CONFIG.Token.objectClass = DC20RpgToken;
  CONFIG.MeasuredTemplate.objectClass = DC20RpgMeasuredTemplate;
  CONFIG.MeasuredTemplate.documentClass = DC20MeasuredTemplateDocument;
  CONFIG.MeasuredTemplate.TEMPLATE_REFRESH_TIMEOUT = 200;

  // Register data models
  CONFIG.Actor.dataModels.character = DC20CharacterData;
  CONFIG.Actor.dataModels.npc = DC20NpcData;
  CONFIG.Actor.dataModels.companion = DC20CompanionData;
  CONFIG.Item.dataModels.basicAction = DC20BasicActionData;
  CONFIG.Item.dataModels.weapon = DC20WeaponData;
  CONFIG.Item.dataModels.equipment = DC20EquipmentData;
  CONFIG.Item.dataModels.consumable = DC20ConsumableData;
  CONFIG.Item.dataModels.loot = DC20LootData;
  CONFIG.Item.dataModels.feature = DC20FeatureData;
  CONFIG.Item.dataModels.technique = DC20TechniqueData;
  CONFIG.Item.dataModels.spell = DC20SpellData;
  CONFIG.Item.dataModels.class = DC20ClassData;
  CONFIG.Item.dataModels.subclass = DC20SubclassData;
  CONFIG.Item.dataModels.ancestry = DC20AncestryData;
  CONFIG.Item.dataModels.background = DC20BackgroundData;

  // Register sheet application classes
  Actors.unregisterSheet("core", ActorSheet);
  Actors.registerSheet("dc20rpg", DC20RpgActorSheet, { makeDefault: true });
  Items.unregisterSheet("core", ItemSheet);
  Items.registerSheet("dc20rpg", DC20RpgItemSheet, { makeDefault: true });
  DocumentSheetConfig.unregisterSheet(ActiveEffect, "dc20rpg", ActiveEffectConfig);
  DocumentSheetConfig.registerSheet(ActiveEffect, "dc20rpg", DC20RpgActiveEffectConfig, { makeDefault: true });
  DocumentSheetConfig.unregisterSheet(Macro, "dc20rpg", MacroConfig);
  DocumentSheetConfig.registerSheet(Macro, "dc20rpg", DC20RpgMacroConfig, { makeDefault: true });
  DocumentSheetConfig.unregisterSheet(TokenDocument, "dc20rpg", TokenConfig);
  DocumentSheetConfig.registerSheet(TokenDocument, "dc20rpg", DC20RpgTokenConfig, { makeDefault: true });

  // Register Handlebars helpers and creators
  registerHandlebarsHelpers();
  registerHandlebarsCreators();

  // Register extended enrichHTML method
  TextEditor.enrichHTML = expandEnrichHTML(TextEditor.enrichHTML);
  registerGlobalInlineRollListener();

  // Preload Handlebars templates
  return preloadHandlebarsTemplates();
});

/* -------------------------------------------- */
/*  Ready Hook                                  */
/* -------------------------------------------- */
Hooks.once("ready", async function() {
  await runMigrationCheck();
  // await testMigration("0.9.0", "0.9.5", new Set(["dc20-core-rulebook"]));
  // await testMigration("0.9.0", "0.9.5");

  /* -------------------------------------------- */
  /*  Hotbar Macros                               */
  /* -------------------------------------------- */
  // Wait to register hotbar drop hook on ready so that modules could register earlier if they want to
  Hooks.on("hotbarDrop", (bar, data, slot) => {
    if (data.type === "Item") {
      createItemHotbarDropMacro(data, slot);
      return false;
    }
    if (data.type === "Macro") {
      let macro = game.macros.find(macro => (macro.uuid === data.uuid));
      if(macro) game.user.assignHotbarMacro(macro, slot);
    }
    return true; 
  });

  registerSystemSockets();
  createTokenEffectsTracker();
  registerUniqueSystemItems();

  if(game.user.isGM) await createGmToolsMenu();

  // Override error notification to ignore "Item does not exist" error.
  ui.notifications.error = (message, options) => {
    if (message.includes("does not exist!")) return;
    return ui.notifications.notify(message, "error", options);
  };

  // Hide tooltip when releasing button
  window.addEventListener('keyup', (event) => {
    if (event.key === 'Alt') {
      const tooltip = document.getElementById("tooltip-container");
      if (tooltip && tooltip.style.visibility === "visible") {
        tooltip.style.opacity = 0;
        tooltip.style.visibility = "hidden";
      }
    }
  });
});

Hooks.on("renderActorDirectory", (app, html, data) => characterWizardButton(html));
Hooks.on("renderCompendiumDirectory", (app, html, data) => compendiumBrowserButton(html));
Hooks.on("renderDialog", (app, html, data) => {
  // We want to remove "basicAction" from "Create Item Dialog"
  if (html.find('[name="type"]').length > 0) {
    const typeSelect = html.find('[name="type"]');
    const typesToRemove = ["basicAction"];

    typesToRemove.forEach(type => {
      typeSelect.find(`option[value="${type}"]`).remove();
    });
  }
});
Hooks.on("createScene", async (scene, options, userId) => {
  if (userId !== game.userId) return;

  if (scene.grid.distance !== 1) {
    const confirmed = await getSimplePopup("confirm", {header: "Incorrect grid distance", information: [`Looks like the '${scene.name}' scene is using a different than default grid distance. This may cause problems with distance calculations. Would you like to replace the distance with the default for this system?`]});
    if (confirmed) scene.update({
      ["grid.units"]: "Space",
      ["grid.distance"]: 1
    });
  }
});
