text: const SkillFlag = require('./SkillFlag');
  const SkillType = require('./SkillType');
  const SkillErrors = require('./SkillErrors');
  const Damage = require('./Damage');
  const Broadcast = require('./Broadcast');
  
  /**
   * @property {function (Effect)} configureEffect modify the skill's effect before adding to player
   * @property {null|number}      cooldownLength When a number > 0 apply a cooldown effect to disallow usage
   *                                       until the cooldown has ended
   * @property {string}           effect Id of the passive effect for this skill
   * @property {Array<SkillFlag>} flags
   * @property {function ()}      info Function to run to display extra info about this skill
   * @property {function ()}      run  Function to run when skill is executed/activated
   * @property {GameState}        state
   * @property {SkillType}        type
   * @param {Array<string>}       aliases
   * @property {string}    gender            используется для корректного построения фраз
   * @property {string}    damageVerb        используется для умений и эффектов наносящих урон противнику
   */
  class Skill {
    /**
     * @param {string} id
     * @param {object} config
     * @param {GameState} state
     */
    constructor(id, config, state) {
      const {
        configureEffect = (_) => _,
        cooldown = null,
        effect = null,
        flags = [],
        info = (_) => {},
        initiatesCombat = false,
        name,
        gender,
        damageVerb,
        aliases = [],
        requiresTarget = true,
        resource = null, /* format [{ attribute: 'someattribute', cost: 10}] */
        run = (_) => {},
        targetSelf = false,
        type = SkillType.SKILL,
        options = {},
      } = config;
  
      this.configureEffect = configureEffect;
  
      this.cooldownGroup = null;
      if (cooldown && typeof cooldown === 'object') {
        this.cooldownGroup = cooldown.group;
        this.cooldownLength = cooldown.length;
      } else {
        this.cooldownLength = cooldown;
      }
  
      this.effect = effect;
      this.flags = flags;
      this.id = id;
      this.info = info.bind(this);
      this.initiatesCombat = initiatesCombat;
      this.name = name;
      this.gender = gender;
      this.damageVerb = damageVerb;
      this.options = options;
      this.requiresTarget = requiresTarget;
      this.resource = resource;
      this.run = run.bind(this);
      this.state = state;
      this.targetSelf = targetSelf;
      this.type = type;
      this.aliases = aliases;
    }
  
    /**
     * perform an active skill
     * @param {string} args
     * @param {Player} player
     * @param {Character} target
     */
    execute(args, player, target) {
      if (this.flags.includes(SkillFlag.PASSIVE)) {
        throw new SkillErrors.PassiveError();
      }
  
      const cdEffect = this.onCooldown(player);
      if (this.cooldownLength && cdEffect) {
        throw new SkillErrors.CooldownError(cdEffect);
      }
  
      if (this.resource) {
        if (!this.hasEnoughResources(player)) {
          throw new SkillErrors.NotEnoughResourcesError();
        }
      }
  
      if (target !== player && this.initiatesCombat) {
        player.initiateCombat(target);
      }
  
      // allow skills to not incur the cooldown if they return false in run
      if (this.run(args, player, target) !== false) {
        this.cooldown(player);
        if (this.resource) {
          this.payResourceCosts(player);
        }
      }
  
      return true;
    }
  
    /**
     * @param {Player} player
     * @return {boolean} If the player has paid the resource cost(s).
     */
    payResourceCosts(player) {
      const hasMultipleResourceCosts = Array.isArray(this.resource);
      if (hasMultipleResourceCosts) {
        for (const resourceCost of this.resource) {
          this.payResourceCost(player, resourceCost);
        }
        return true;
      }
  
      return this.payResourceCost(player, this.resource);
    }
  
    // Helper to pay a single resource cost.
    payResourceCost(player, resource) {
      // Resource cost is calculated as the player damaging themself so effects
      // could potentially reduce resource costs
      const damage = new Damage(resource.attribute, resource.cost, player, this, {
        hidden: true,
      });
  
      damage.commit(player);
    }
  
    activate(player) {
      if (!this.flags.includes(SkillFlag.PASSIVE)) {
        return;
      }
  
      if (!this.effect) {
        throw new Error('Passive skill has no attached effect');
      }
  
      let effect = this.state.EffectFactory.create(this.effect, { description: this.info(player) });
      effect = this.configureEffect(effect);
      effect.skill = this;
      player.addEffect(effect);
      this.run(player);
    }
  
    /**
     * @param {Character} character
     * @return {boolean|Effect} If on cooldown returns the cooldown effect
     */
    onCooldown(character) {
      for (const effect of character.effects.entries()) {
        if (effect.id === 'cooldown' && effect.state.cooldownId === this.getCooldownId()) {
          return effect;
        }
      }
  
      return false;
    }
  
    /**
     * Put this skill on cooldown
     * @param {number} duration Cooldown duration
     * @param {Character} character
     */
    cooldown(character) {
      if (!this.cooldownLength) {
        return;
      }
  
      character.addEffect(this.createCooldownEffect());
    }
  
    getCooldownId() {
      return this.cooldownGroup ? `skillgroup:${this.cooldownGroup}` : `skill:${this.id}`;
    }
  
    /**
     * Create an instance of the cooldown effect for use by cooldown()
     *
     * @private
     * @return {Effect}
     */
    createCooldownEffect() {
      if (!this.state.EffectFactory.has('cooldown')) {
        this.state.EffectFactory.add('cooldown', this.getDefaultCooldownConfig());
      }
  
      const effect = this.state.EffectFactory.create(
        'cooldown',
        { name: `Задержка: ${this.name}`, duration: this.cooldownLength * 1000 },
        { cooldownId: this.getCooldownId() },
      );
      effect.skill = this;
  
      return effect;
    }
  
    getDefaultCooldownConfig() {
      return {
        config: {
          name: 'Cooldown',
          description: 'Cannot use ability while on cooldown.',
          unique: false,
          type: 'cooldown',
        },
        state: {
          cooldownId: null,
        },
        listeners: {
          effectDeactivated() {
            Broadcast.sayAt(this.target, `Вы снова можете использовать <bold>'${this.skill.name}'</bold> again.`);
          },
        },
      };
    }
  
    /**
     * @param {Character} character
     * @return {boolean}
     */
    hasEnoughResources(character) {
      if (Array.isArray(this.resource)) {
        return this.resource.every((resource) => this.hasEnoughResource(character, resource));
      }
      return this.hasEnoughResource(character, this.resource);
    }
  
    /**
     * @param {Character} character
     * @param {{ attribute: string, cost: number}} resource
     * @return {boolean}
     */
    hasEnoughResource(character, resource) {
      return !resource.cost || (
        character.hasAttribute(resource.attribute)
        && character.getAttribute(resource.attribute) >= resource.cost
      );
    }
  }
  
  module.exports = Skill;
  