import { TypeScriptUtils } from '../../utils';
import { AbilityAccessors } from '../Ability';
import { CharacterDerivers } from '../Character';
import { ClassAccessors } from '../Class';
import { AbilityStatEnum, AttackTypeRangeEnum } from '../Core';
import { DataOriginTypeEnum } from '../DataOrigin';
import { DiceAccessors, DiceDerivers, DiceUtils } from '../Dice';
import { HelperUtils } from '../Helper';
import { ModifierAccessors, ModifierDerivers, ModifierValidators } from '../Modifier';
import { RuleDataAccessors, RuleDataUtils } from '../RuleData';
import { AdjustmentTypeEnum, ValueUtils, ValueValidators } from '../Value';
import { getAbilityModifierStatId, getActionTypeId, getAttackRangeId, getAttackSubtypeId, getDamageTypeId, getDataOrigin, getDataOriginType, getDefinitionName, getDefinitionRange, getDice, getDisplayAsAttack, getEntityTypeId, getFixedSaveDc, getFixedToHit, getId, getIsMartialArts, getIsProficienct, getMappingEntityTypeId, getMappingId, getSaveStatId, getSpellRangeType, requiresAttackRoll, } from './accessors';
import { ACTION_CUSTOMIZATION_ADJUSTMENT_TYPES, ActionTypeEnum, AttackSubtypeEnum } from './constants';
/**
 *
 * @param action
 */
export function deriveUniqueKey(action) {
    return `${getId(action)}-${getEntityTypeId(action)}`;
}
/**
 *
 * @param action
 */
export function deriveRequiresAttackRoll(action) {
    return getAttackRangeId(action) !== null;
}
/**
 *
 * @param action
 */
export function deriveRequiresSavingThrow(action) {
    return getSaveStatId(action) !== null;
}
/**
 *
 * @param action
 */
export function deriveIsDefaultDisplayAsAttack(action) {
    const displayAsAttack = getDisplayAsAttack(action);
    if (displayAsAttack !== null) {
        return displayAsAttack;
    }
    return deriveRequiresAttackRoll(action);
}
/**
 *
 * @param action
 */
export function deriveAttackSubtypeName(action) {
    switch (getAttackSubtypeId(action)) {
        case AttackSubtypeEnum.UNARMED:
            return 'Unarmed Strike';
        case AttackSubtypeEnum.NATURAL:
            return 'Natural Attack';
    }
    return null;
}
/**
 *
 * @param action
 * @param martialArtsLevelScale
 */
export function deriveIsMartialArtsAvailable(action, martialArtsLevelScale) {
    const isMartialArtsEnabled = getIsMartialArts(action);
    const attackSubtype = getAttackSubtypeId(action);
    if (martialArtsLevelScale !== null && (attackSubtype === AttackSubtypeEnum.UNARMED || isMartialArtsEnabled)) {
        return true;
    }
    return false;
}
/**
 *
 * @param action
 * @param ruleData
 */
export function deriveFallbackAbilityIds(action, ruleData) {
    const actionType = getActionTypeId(action);
    const attackTypeRange = getAttackRangeId(action);
    const availableAbilities = [];
    if (actionType === ActionTypeEnum.WEAPON) {
        if (attackTypeRange === AttackTypeRangeEnum.RANGED) {
            availableAbilities.push(AbilityStatEnum.DEXTERITY);
        }
        else {
            availableAbilities.push(AbilityStatEnum.STRENGTH);
        }
    }
    else {
        const dataOrigin = getDataOrigin(action);
        const dataOriginType = getDataOriginType(action);
        switch (dataOriginType) {
            case DataOriginTypeEnum.CLASS_FEATURE:
                const primaryAbilities = ClassAccessors.getPrimaryAbilities(dataOrigin.parent);
                if (primaryAbilities.length) {
                    availableAbilities.push(...primaryAbilities);
                }
                break;
        }
    }
    return availableAbilities;
}
/**
 *
 * @param action
 * @param abilityLookup
 * @param ruleData
 */
export function deriveBestFallbackAbility(action, abilityLookup, ruleData) {
    const fallbackAbilityIds = deriveFallbackAbilityIds(action, ruleData);
    const fallbackAbilities = fallbackAbilityIds.map((id) => abilityLookup[id]);
    return HelperUtils.getLast(fallbackAbilities, 'modifier');
}
/**
 *
 * @param action
 * @param martialArtsLevelScale
 * @param ruleData
 * @param includeFallbacks
 */
export function deriveAvailableAbilities(action, martialArtsLevelScale, ruleData, includeFallbacks = true) {
    let attackStat = null;
    if (requiresAttackRoll(action)) {
        attackStat = getAbilityModifierStatId(action);
    }
    const availableAbilities = [];
    if (attackStat) {
        availableAbilities.push(attackStat);
    }
    else if (includeFallbacks) {
        availableAbilities.push(...deriveFallbackAbilityIds(action, ruleData));
    }
    if (deriveIsMartialArtsAvailable(action, martialArtsLevelScale) &&
        !availableAbilities.includes(AbilityStatEnum.DEXTERITY)) {
        availableAbilities.push(AbilityStatEnum.DEXTERITY);
    }
    return availableAbilities;
}
/**
 *
 * @param action
 * @param abilityLookup
 * @param proficiencyBonus
 * @param valueLookup
 * @param ruleData
 */
export function deriveAttackSaveValue(action, abilityLookup, proficiencyBonus, valueLookup, ruleData) {
    const id = getMappingId(action);
    const entityId = getMappingEntityTypeId(action);
    if (getSaveStatId(action) === null) {
        return null;
    }
    const fixedSaveDc = getFixedSaveDc(action);
    if (fixedSaveDc) {
        return fixedSaveDc;
    }
    const saveDcOverride = ValueUtils.getKeyValue(valueLookup, AdjustmentTypeEnum.SAVE_DC_OVERRIDE, id, entityId);
    if (saveDcOverride !== null) {
        return saveDcOverride;
    }
    let attackSaveAbility = null;
    const abilityModifierStatId = getAbilityModifierStatId(action);
    if (abilityModifierStatId) {
        attackSaveAbility = abilityLookup[abilityModifierStatId];
    }
    else {
        attackSaveAbility = deriveBestFallbackAbility(action, abilityLookup, ruleData);
    }
    if (!attackSaveAbility) {
        return null;
    }
    const abilityModifier = AbilityAccessors.getModifier(attackSaveAbility);
    const attackSaveAbilityModifier = abilityModifier ? abilityModifier : 0;
    let saveDc = CharacterDerivers.deriveAttackSaveValue(proficiencyBonus, attackSaveAbilityModifier);
    saveDc += ValueUtils.getKeyValue(valueLookup, AdjustmentTypeEnum.SAVE_DC_BONUS, id, entityId, 0);
    return saveDc;
}
/**
 *
 * @param bestAbilityInfo
 * @param action
 * @param modifiers
 * @param abilityLookup
 * @param valueLookup
 */
export function deriveToHit(bestAbilityInfo, action, modifiers, abilityLookup, valueLookup) {
    const actionType = getActionTypeId(action);
    let toHit = null;
    if (bestAbilityInfo && getAttackRangeId(action) !== null) {
        const id = getMappingId(action);
        const entityId = getMappingEntityTypeId(action);
        const toHitOverride = ValueUtils.getKeyValue(valueLookup, AdjustmentTypeEnum.TO_HIT_OVERRIDE, id, entityId);
        if (toHitOverride !== null) {
            return toHitOverride;
        }
        let bonusToHitModifiers = null;
        let bonusToHitModifierTotal = 0;
        if (actionType === ActionTypeEnum.SPELL) {
            bonusToHitModifiers = modifiers.filter((modifier) => ModifierValidators.isValidBonusActionSpellToHitModifier(modifier, action));
        }
        else if (actionType === ActionTypeEnum.WEAPON) {
            bonusToHitModifiers = modifiers.filter((modifier) => ModifierValidators.isValidBonusActionWeaponToHitModifier(modifier, action));
        }
        if (bonusToHitModifiers !== null) {
            bonusToHitModifierTotal = ModifierDerivers.sumModifiers(bonusToHitModifiers, abilityLookup);
        }
        const toHitCustomBonus = ValueUtils.getKeyValue(valueLookup, AdjustmentTypeEnum.TO_HIT_BONUS, id, entityId, 0);
        toHit = bestAbilityInfo.toHit + bonusToHitModifierTotal + toHitCustomBonus;
    }
    const fixedToHit = getFixedToHit(action);
    if (fixedToHit !== null) {
        toHit = fixedToHit;
    }
    return toHit;
}
/**
 *
 * @param action
 * @param baseDamage
 * @param fixedDamageBonuses
 * @param valueLookup
 * @param ruleData
 */
export function deriveDamage(action, baseDamage, fixedDamageBonuses, valueLookup, ruleData) {
    const id = getMappingId(action);
    const entityId = getMappingEntityTypeId(action);
    const damageTypeId = getDamageTypeId(action);
    let type = null;
    let value = null;
    let dataOrigin = null;
    let isMartialArts = false;
    const damageCustomBonus = ValueUtils.getKeyValue(valueLookup, AdjustmentTypeEnum.FIXED_VALUE_BONUS, id, entityId, 0);
    const fixedDamage = fixedDamageBonuses + damageCustomBonus;
    if (baseDamage !== null) {
        if (typeof baseDamage === 'number') {
            value = Math.max(0, baseDamage + fixedDamage);
        }
        else if (baseDamage.isMartialArts !== undefined) {
            if (baseDamage.dataOrigin) {
                dataOrigin = baseDamage.dataOrigin;
            }
            else if (baseDamage.isMartialArts) {
                isMartialArts = true;
            }
            const fixedValue = DiceAccessors.getFixedValue(baseDamage);
            value = Object.assign(Object.assign({}, baseDamage), { fixedValue: (fixedValue ? fixedValue : 0) + fixedDamage });
        }
    }
    if (damageTypeId) {
        type = RuleDataUtils.getDamageType(damageTypeId, ruleData);
    }
    return {
        type,
        value,
        dataOrigin,
        isMartialArts,
    };
}
/**
 *
 * @param action
 * @param valueLookup
 */
export function deriveName(action, valueLookup) {
    const id = getMappingId(action);
    const entityId = getMappingEntityTypeId(action);
    let derivedName = getDefinitionName(action);
    const nameOverride = ValueUtils.getKeyValue(valueLookup, AdjustmentTypeEnum.NAME_OVERRIDE, id, entityId);
    if (nameOverride) {
        derivedName = nameOverride;
    }
    if (derivedName === null) {
        derivedName = '';
    }
    return derivedName;
}
/**
 *
 * @param action
 * @param modifiers
 */
export function deriveLabel(action, modifiers) {
    const masteryModifiers = modifiers.filter((modifier) => ModifierValidators.isValidWeaponMasteryModifier(modifier));
    const masteryModifier = masteryModifiers.find((modifier) => modifier.componentId === action.componentId);
    if (masteryModifier) {
        return ModifierAccessors.getFriendlySubtypeName(masteryModifier);
    }
    return null;
}
/**
 *
 * @param action
 * @param proficiencyBonus
 */
export function deriveProficiencyBonus(action, proficiencyBonus) {
    let bonus = 0;
    if (getIsProficienct(action)) {
        bonus = proficiencyBonus;
    }
    return bonus;
}
/**
 *
 * @param action
 */
export function deriveProficiency(action) {
    return getIsProficienct(action);
}
/**
 *
 * @param action
 * @param valueLookup
 */
export function deriveDisplayAsAttack(action, valueLookup) {
    const id = getMappingId(action);
    const entityId = getMappingEntityTypeId(action);
    const displayAsAttackOverride = ValueUtils.getKeyValue(valueLookup, AdjustmentTypeEnum.DISPLAY_AS_ATTACK, id, entityId);
    if (displayAsAttackOverride !== null) {
        return displayAsAttackOverride;
    }
    return deriveIsDefaultDisplayAsAttack(action);
}
/**
 *
 * @param action
 * @param modifiers
 * @param modifierData
 * @param ruleData
 */
export function deriveReach(action, modifiers, modifierData, ruleData) {
    let baseReach = null;
    if (getActionTypeId(action) === ActionTypeEnum.SPELL) {
        return null;
    }
    // Reach is null if it isn't a melee attack
    if (getAttackRangeId(action) !== AttackTypeRangeEnum.MELEE) {
        return null;
    }
    // Figure out base reach based on initial data
    const range = getDefinitionRange(action);
    if (range) {
        baseReach = range.range;
    }
    if (baseReach === null) {
        baseReach = RuleDataAccessors.getBaseWeaponReach(ruleData);
    }
    const bonusReachModifiers = modifiers.filter((modifier) => ModifierValidators.isBonusMeleeReachModifier(modifier));
    const bonusReachModifierTotal = ModifierDerivers.deriveTotalValue(bonusReachModifiers, modifierData);
    return baseReach + bonusReachModifierTotal;
}
/**
 *
 * @param action
 * @param martialArtsLevelScale
 * @param modifiers
 */
export function deriveHighestDamageDie(action, martialArtsLevelScale, modifiers) {
    const actionType = getActionTypeId(action);
    const baseDice = getDice(action);
    const attackSubtype = getAttackSubtypeId(action);
    const isMartialArtsAvailable = deriveIsMartialArtsAvailable(action, martialArtsLevelScale);
    const allDamageDice = [];
    if (baseDice) {
        allDamageDice.push(Object.assign(Object.assign({}, baseDice), { isMartialArts: false, dataOrigin: null }));
    }
    if (actionType === ActionTypeEnum.WEAPON && attackSubtype === AttackSubtypeEnum.UNARMED) {
        const setDamageDieModifiers = modifiers.filter((modifier) => ModifierValidators.isSetUnarmedDamageDieModifier(modifier));
        const setDamageDice = setDamageDieModifiers
            .map((modifier) => {
            const modifierDice = ModifierAccessors.getDice(modifier);
            if (modifierDice === null) {
                return null;
            }
            return Object.assign(Object.assign({}, modifierDice), { isMartialArts: false, dataOrigin: ModifierAccessors.getDataOrigin(modifier) });
        })
            .filter(TypeScriptUtils.isNotNullOrUndefined);
        allDamageDice.push(...setDamageDice);
    }
    if (martialArtsLevelScale !== null && isMartialArtsAvailable) {
        const martialArtsDie = DiceDerivers.deriveMartialArtsDamageDie(martialArtsLevelScale);
        allDamageDice.push(Object.assign(Object.assign({}, martialArtsDie), { isMartialArts: true, dataOrigin: null }));
    }
    return DiceUtils.getHighestDie(allDamageDice);
}
/**
 *
 * @param bestAbilityInfo
 * @param action
 * @param modifiers
 * @param abilityLookup
 */
export function deriveFixedDamageBonuses(bestAbilityInfo, action, modifiers, abilityLookup) {
    const actionType = getActionTypeId(action);
    let damageModifiers = [];
    let damageModifierTotal = 0;
    if (actionType === ActionTypeEnum.SPELL) {
        damageModifiers = modifiers.filter((modifier) => ModifierValidators.isValidDamageActionSpellModifier(modifier, action));
    }
    else if (actionType === ActionTypeEnum.WEAPON) {
        damageModifiers = modifiers.filter((modifier) => ModifierValidators.isValidDamageActionWeaponModifier(modifier, action));
    }
    if (damageModifiers.length) {
        damageModifierTotal = ModifierDerivers.sumModifiers(damageModifiers, abilityLookup);
    }
    return damageModifierTotal + (bestAbilityInfo ? bestAbilityInfo.damageBonus : 0);
}
/**
 *
 * @param action
 * @param modifiers
 * @param abilityLookup
 */
export function deriveRange(action, modifiers, abilityLookup) {
    const actionType = getActionTypeId(action);
    const definitionRange = getDefinitionRange(action);
    if (definitionRange === null) {
        return definitionRange;
    }
    let range = Object.assign(Object.assign({}, definitionRange), { origin: null });
    if (actionType === ActionTypeEnum.SPELL) {
        const attackTypeRange = getAttackRangeId(action);
        let newRange = definitionRange.range;
        if (attackTypeRange !== null && newRange) {
            const spellAttackRangeMultiplierModifiers = modifiers.filter((modifier) => ModifierValidators.isBonusSpellAttackRangeMultiplierModifier(modifier));
            const spellAttackRangeMultiplierTotal = Math.max(1, ModifierDerivers.sumModifiers(spellAttackRangeMultiplierModifiers, abilityLookup, 1));
            newRange *= spellAttackRangeMultiplierTotal;
        }
        range = Object.assign(Object.assign({}, definitionRange), { range: newRange, origin: getSpellRangeType(action) });
    }
    return range;
}
/**
 *
 * @param action
 * @param valueLookup
 */
export function deriveIsOffhand(action, valueLookup) {
    let isOffhand = false;
    if (getActionTypeId(action) === ActionTypeEnum.WEAPON) {
        const id = getMappingId(action);
        const entityId = getMappingEntityTypeId(action);
        isOffhand = ValueUtils.getKeyValue(valueLookup, AdjustmentTypeEnum.IS_OFFHAND, id, entityId, false);
    }
    return isOffhand;
}
/**
 *
 * @param action
 * @param valueLookup
 */
export function deriveIsSilvered(action, valueLookup) {
    let isSilvered = false;
    if (getActionTypeId(action) === ActionTypeEnum.WEAPON) {
        const id = getMappingId(action);
        const entityId = getMappingEntityTypeId(action);
        isSilvered = ValueUtils.getKeyValue(valueLookup, AdjustmentTypeEnum.IS_SILVER, id, entityId, false);
    }
    return isSilvered;
}
/**
 *
 * @param action
 * @param valueLookup
 */
export function deriveIsCustomized(action, valueLookup) {
    return ValueValidators.validateHasCustomization(ACTION_CUSTOMIZATION_ADJUSTMENT_TYPES, valueLookup, getMappingId(action), getMappingEntityTypeId(action));
}
