chargen.System.Feature = {
  initFeature : function(pgroup, pname, p) {
    let base = p;
    if (!p) {
      let options = p;
      p = function(){};
      if (!chargen.Features[pgroup]) chargen.Features[pgroup] = {};
      chargen.Features[pgroup][pname] = base = p;
      p = p.prototype;
      if (options) {
        p.options = options;
      }
    } else {
      if (!p.prototype) {
        chargen.Features[pgroup][pname] = base = function() {};
        base.prototype = p;
      } else {
        p = p.prototype;
      }
      if (p._initialised) return;
    }
    let owner = null;
//    if (p.autoattach == undefined || p.autoattach) {
      if (chargen.Races[pgroup])
        owner = chargen.Races[pgroup].prototype;
      else if (chargen.Classes[pgroup])
        owner = chargen.Classes[pgroup].prototype;
      if ( owner && owner.features && !inArray(base, owner.features) &&
           (!owner.dynamic_features || (!inArray(base, owner.dynamic_features) && !inArray(pgroup+'.'+pname, owner.dynamic_features)) ) )
        owner.features.push(base)
//    }
//    delete p.autoattach;
    p.id = pname;
    p._type = 'feature';
    p._Types = 'Features';
//    if (!p.book)  p.book   = 'phb'; // Features don't need book... thus far
    if (!p.group) p.group = pgroup;
    if (!p.reset) p.reset = this.reset;
    p.text = text.Features[pgroup][pname];
    p.rules = chargen.Rule.parseAll(p.text, p);
    p._initialised = true;
  },

  /* Reset feature stat to initial, unadjusted state */
  reset: function() {
    this.text = cloneObject(text.Features[this.group][this.id]);
    this.rules = chargen.Rule.parseAll(this.text, this);
  },

  /* A common feature/power selecting feature implementation. Used for rogue, warlock, swordmage, etc. */
  featureSelector: function(choices, features, powers) {
    this.options = choices;
    this._map_features = features;
    this._map_powers = powers;
  }

};
chargen.System.Feature.featureSelector.prototype = {
  updatePowers	: function(chr) {
    if (!chr.features[this.id]) return;
    let choice = chr.features[this.id].selection.choice || false;
    if (choice) {
      choice = this.options.indexOf(choice);
      if (this._map_features) {
        let feature = this._map_features[choice];
        if (typeof feature == 'string') feature = [feature];
        for each (let f in feature) chr.setFeature(f, new chargen.Features[this.group][f]());
      }
      if (this._map_powers) {
        let power = this._map_powers[choice];
        if (typeof power == 'string') power = [power];
        for each (let p in power) chr.setPower(p, new chargen.Powers[this.group][p]());
      }
    }
  }
}

let (feature = chargen.Features) {
  feature.Racial = {};
  feature.Heroic = {};
  feature.Paragon = {};
  feature.Epic = {};



  feature.Racial.LowLightVision = function(){};
  feature.Racial.LowLightVision.prototype.updateMisc = function(chr) { if (chr.vision < 1) chr.vision = 1; };

  feature.Racial.DangerSense = function(){};
  feature.Racial.DangerSense.prototype.updateDefenses = function(chr) { chr.stats.initiative.addBonus(2, this, 'racial'); };

  feature.Racial.Oversized = function(){};
  feature.Racial.Oversized.prototype.updateProfs = function(chr) {
    for each (let slot in chr.equipments)
      if (slot.selection && slot.selection.type == 'weapon')
        slot.selection.adjustSize(+1);
  }

  feature.Dragonborn = {};
  feature.Dragonborn.DraconicHeritage = {
    updateDefenses : function(chr) {
      chr.stats.hshp.addBonus(chr.conMod, this);
    }
  }

  feature.Dwarf = {};
  feature.Dwarf.DwarvenWeaponProficiency = {
    updateProfs : function(chr){
      if (!chr.proficiencies[chargen.Equipments.BaseWeapon.ThrowingHammer.prototype.category])
        chr.proficiencies.ThrowingHammer = chargen.LIST.MANDATORY;
      if (!chr.proficiencies[chargen.Equipments.BaseWeapon.Warhammer.prototype.category])
        chr.proficiencies.Warhammer = chargen.LIST.MANDATORY;
    }
  }

  feature.Dwarf.EncumberedSpeed = function(){};
  feature.Dwarf.EncumberedSpeed.prototype.updateFFPStats2 = function(chr){
    let armour = chr.equipments.body.selection;
    if (armour && armour.type == 'armour' && armour.speed < 0) chr.stats.speed.removeBonus(armour, 'armour');
  };



  feature.Eladrin = {};
  feature.Eladrin.EladrinWeaponProficiency = function(){};
  feature.Eladrin.EladrinWeaponProficiency.prototype.updateProfs = function(chr){
    if (!chr.proficiencies[chargen.Equipments.BaseWeapon.Longsword.prototype.category])
      chr.proficiencies.Longsword = chargen.LIST.MANDATORY;
  };

  feature.Eladrin.EladrinEducation = function(){};
  let (p = feature.Eladrin.EladrinEducation.prototype) {
    p.options = {};
    for each (let [skill,] in Iterator(chargen.Skills)) p.options[skill] = 'skill_'+skill;
    p.updateSkills = function(chr) {
      if (this.choice) chr.trainedSkills[this.choice] = chargen.LIST.MANDATORY;
    };
  }

  feature.Eladrin.EladrinWill = function(){};
  feature.Eladrin.EladrinWill.prototype.updateDefenses = function(chr) {
    chr.stats.will.addBonus(1, this, 'racial');
  };



  feature.HalfElf = {};

  feature.HalfElf.DualHeritage = function(){};
  feature.HalfElf.DualHeritage.prototype.updateValidations = function(chr) {
    for each (let slot in chr.feats)
      if (slot.bonus_options)
        slot.bonus_options = slot.bonus_options.concat(
          chargen.Races.Human.prototype.feats.concat(chargen.Races.Elf.prototype.feats));
  }

  feature.HalfElf.Dilettante = function(){};
  feature.HalfElf.Dilettante.prototype.updatePowers = function(chr) {
    // Add half-elf bonus power slot
    let slot = new chargen.System.Power.FrequencyPowerSlot('encounter');
    slot.__defineGetter__('options',  function() {
      if (!chargen.Features.HalfElf.Dilettante.Powers) {
        let powers = [];
        for each (let cls in chargen.Classes)
          powers = powers.concat(cls.prototype.atwill);
        chargen.Features.HalfElf.Dilettante.Powers = powers;
      }
      let removeList = this.char.classes.heroic.atwill;
      let result = [i for each (i in chargen.Features.HalfElf.Dilettante.Powers) if (removeList.indexOf(i) < 0)];
      return result;
    } );
    chr.setPowerSlot('HalfElf_BonusPower', slot, text.Features.HalfElf.Dilettante);
  };



  feature.Human = {};
  feature.Human.SingleAbilityBonus2 = function(){};
  feature.Human.SingleAbilityBonus2.prototype.updateAbilities = function(chr) { if (this.choice) chr.abilities[this.choice] += 2; };
  feature.Human.SingleAbilityBonus2.prototype.options = { str:'str', con:'con', dex:'dex', int:'int', wis:'wis', cha:'cha' };

  feature.Human.BonusAtWillPower = function(){};
  feature.Human.BonusAtWillPower.prototype.updatePowers = function(chr) {
    let power = new chargen.System.Power.atwillClassPowerSlot();
    power.level = 1;
    chr.setPowerSlot('Human_BonusPower', power, text.Features.Human.BonusAtWillPower);
  };

  feature.Human.BonusFeat = function(){};
  feature.Human.BonusFeat.prototype.updateFeats = function(chr) {
    let feat = new chargen.System.Feat.FeatSlot();
    chr.setFeatSlot('Human_BonusFeat', feat, text.Features.Human.BonusFeat);
  };

  feature.Human.BonusSkill = function(){};
  feature.Human.BonusSkill.prototype.updateSkills = function(chr) {
    chr.classes.heroic.skill_slot += 1;
  };
  feature.Human.HumanDefenseBonuses = function(){};
  feature.Human.HumanDefenseBonuses.prototype.updateDefenses = function(chr) {
    chr.stats.fort.addBonus(1, this, 'racial');
    chr.stats.ref.addBonus(1, this, 'racial');
    chr.stats.will.addBonus(1, this, 'racial');
  };


  feature.Doppelganger = { MentalDefense : function(){} };
  feature.Doppelganger.MentalDefense.prototype.updateDefenses = function(chr) { chr.stats.will.addBonus(1, this, 'racial'); };

  feature.Githyanki = { GithyankiWillpower : function(){} };
  feature.Githyanki.GithyankiWillpower.prototype.updateDefenses = function(chr) { chr.stats.will.addBonus(1, this, 'racial'); };

  feature.Goblin = { GoblinReflexes : function(){} };
  feature.Goblin.GoblinReflexes.prototype.updateDefenses = function(chr) { chr.stats.ref.addBonus(1, this, 'racial'); };

  feature.Hobgoblin = { BattleReady : function(){} };
  feature.Hobgoblin.BattleReady.prototype.updateDefenses = function(chr) { chr.stats.initiative.addBonus(2, this, 'racial'); };

  feature.Shadarkai = {};
  feature.Shadarkai.Winterkin = function(){};
  feature.Shadarkai.Winterkin.prototype.updateDefenses = function(chr) { chr.stats.fort.addBonus(1, this, 'racial'); };


  feature.Genasi = {
    ElementalManifestation : {
      options : [ 'Earthsoul', 'Firesoul', 'Stormsoul', 'Watersoul', 'Windsoul' ],
      updateFeatures : function(chr) {
        if (!chr.temp.features.ElementalManifestation) return;
        let choice = chr.temp.features.ElementalManifestation.selection.choice || false;
        if (choice) chr.setFeature(choice, new chargen.Features.Genasi[choice]());
      }
    },
    Earthsoul : {
      updatePowers	: function(chr) { chr.setPower('Earthshock', new chargen.Powers.Genasi.Earthshock()); },
      updateDefenses	: function(chr) { chr.stats.fort.addBonus(1, this, 'racial'); }
    },
    Firesoul : {
      updatePowers	: function(chr) { chr.setPower('Firepulse', new chargen.Powers.Genasi.Firepulse()); },
      updateDefenses	: function(chr) { chr.stats.ref.addBonus(1, this, 'racial'); }
    },
    Stormsoul : {
      updatePowers	: function(chr) { chr.setPower('PromiseOfStorm', new chargen.Powers.Genasi.PromiseOfStorm()); },
      updateDefenses	: function(chr) { chr.stats.fort.addBonus(1, this, 'racial'); }
    },
    Watersoul : {
      updatePowers	: function(chr) { chr.setPower('Swiftcurrent', new chargen.Powers.Genasi.Swiftcurrent()); }
    },
    Windsoul : {
      updatePowers	: function(chr) { chr.setPower('Windwalker', new chargen.Powers.Genasi.Windwalker()); }
    }
  };




  feature.Cleric = {};
  feature.Cleric.HealersLore = function(){};
  feature.Cleric.HealersLore.prototype.resetPower = function(power) {
    chargen.System.Feat.effectNumberInc(power, this, 'heal', 'wisM', 'healing');
  }



  feature.Fighter = {};
  feature.Fighter.FighterWeaponTalent = {
    options : { 1:'hand_1_handed', 2:'hand_2_handed' },
    onSetPower : function(param) {
//      if (param.eq && this.choice && param.eq.hand == this.choice)
      if (this.choice) {
        let choice = this.choice;
        chargen.System.Commons.addBonusForEq(param.power, 'attack', 1, this, 'feature',
          function(eq){ return eq && eq.hand == choice } );
      }
    }
  }

  feature.Ranger = {};
  feature.Ranger.RangerSkill = {
    options : { dungeoneering: 'skill_dungeoneering', nature: 'skill_nature' },
    updateSkills : function(chr) { if (this.choice) chr.setSelected('trainedSkills', this.choice, chargen.LIST.MANDATORY); }
  };
  feature.Ranger.FightingStyle = new chargen.System.Feature.featureSelector( ['Archer', 'TwoBlade'], [ 'ArcherFightingStyle', 'TwoBladeFightingStyle'] );
  feature.Ranger.ArcherFightingStyle = {
    updateFeats : function(chr) { chr.setFeat('ArcherFightingStyle', new chargen.Feats.Heroic.DefensiveMobility()); }
  };
  feature.Ranger.TwoBladeFightingStyle = {
    updateFeats	: function(chr) { chr.setFeat('TwoBladeFightingStyle', new chargen.Feats.Heroic.Toughness()); },
    updateValidations	: function(chr) {
      if (!this.bonus_options) {
        this.bonus_options = chargen.Features.Ranger.TwoBladeFightingStyle.prototype.bonus_options =
          [w for each (w in chargen.System.Equipment.eqBySlot.melee) if (w.prototype.hand == 1 && !w.prototype.is('offhand'))];
      }
      let offhand = chr.equipments.offhand;
      offhand.bonus_options = offhand.bonus_options.concat(this.bonus_options);
    }
  };



  feature.Rogue = {};
  feature.Rogue.RogueWeaponTalent = {
    updateProfs : function(chr) {
      for each (let eq in chr.equipments)
        if (eq.selection && eq.selection.is && eq.selection.is('Shuriken'))
          eq.selection.adjustSize(+1);
    },
    onSetPower : function(param) {
      chargen.System.Commons.addBonusForEq(param.power, 'attack', 1, this, 'feature', 'Dagger');
    }
  };
  feature.Rogue.RogueTactics = new chargen.System.Feature.featureSelector(['Artful', 'Brutal'], ['ArtfulDodger', 'BrutalScoundrel']);
  feature.Rogue.BrutalScoundrel = {
    updateFFPStats : function(chr) {
      if (chr.features.SneakAttack) {
        let sa = chr.features.SneakAttack.selection;
        sa.rules.desc.Op('replace', this.text.find, this.text.replace);
        chargen.Rule.getFirstNumber(sa.rules.feature).addBonus('strMod', this, 'system_feature')
      }
    }
  };



  feature.Warlock = {};
  feature.Warlock.WarlockPact = new chargen.System.Feature.featureSelector(
          ['Fey', 'Infernal', 'Star'], [ ['FeyPact', 'MistyStep'], ['InfernalPact', 'DarkOnesBlessing'], ['StarPact', 'FateOfTheVoid'] ], [ 'EldritchBlast', 'EldritchBlast', 'EldritchBlast' ]
          );
  feature.Warlock.FeyPact = { updateFeats: function(chr){ chr.setPower('EyeBite', new chargen.Powers.Warlock.EyeBite()); delete chr.powers.atwill1; delete chr.powers.atwill2; } };
  feature.Warlock.InfernalPact = { updateFeats: function(chr){ chr.setPower('HellishRebuke', new chargen.Powers.Warlock.HellishRebuke()); delete chr.powers.atwill1; delete chr.powers.atwill2; } };
  feature.Warlock.StarPact = { updateFeats: function(chr){ chr.setPower('DireRadiance', new chargen.Powers.Warlock.DireRadiance()); delete chr.powers.atwill1; delete chr.powers.atwill2; } };




  feature.Warlord = {};
  feature.Warlord.CommandingPresence = new chargen.System.Feature.featureSelector( ['Inspiring', 'Tactical'], [ 'InspiringPresence', 'TacticalPresence'] );



  feature.Wizard = {};
  feature.Wizard.ArcaneImplementMastery = new chargen.System.Feature.featureSelector(
    ['Orb', 'Staff', 'Wand'], [ 'OrbOfImposition', 'StaffOfDefense', 'WandOfAccuracy'] );
  feature.Wizard.Spellbook = {
    updatePowers : function(chr) {
      for each (let [name, slot] in Iterator(chr.powers)) {
        let part = '', type = '';
        if (startsWith(name, 'daily')) {
          type = 'daily'; part = name.slice(5);
        } else if (startsWith(name, 'utility')) {
          type = 'utility'; part = name.slice(7);
        } else {
          continue;
        }
        let book = new chargen.System.Power[type+'ClassPowerSlot']();
        book.validate = function(item) { return item.group == 'Wizard' };
        book.level = slot.level;
        chr.setPowerSlot(name+'_spellbook', book, text.Features.Wizard.Spellbook);
      }
    },
    updateDefenses : function(chr) {
      for each (let [name, slot] in Iterator(chr.powers))
        if (slot && slot.selection && slot.selection.group != 'Wizard')
          if (startsWith(name, 'daily'))
            delete chr.powers[name+'_spellbook'];
          else if (startsWith(name, 'utility'))
            delete chr.powers[name+'_spellbook'];
    }
  };
  feature.Wizard.Cantrips = {
    updatePowers : function(chr) {
      chr.setPower('GhostSound', new chargen.Powers.Wizard.GhostSound());
      chr.setPower('Light', new chargen.Powers.Wizard.Light());
      chr.setPower('MageHand', new chargen.Powers.Wizard.MageHand());
      chr.setPower('Prestidigitation', new chargen.Powers.Wizard.Prestidigitation());
    }
  };
  feature.Wizard.StaffOfDefense = {
    updateFFPStats : function(chr) {
      for each (let eq in chr.equipments) if (eq.selection && eq.selection.is && eq.selection.is('staff')) {
        chr.stats.ac.addBonus(1, this, 'system_feature');
        break;
      }
    }
  }




  feature.Swordmage = {
    SwordmageWarding: {
      updateDefenses: function(chr) { chr.stats.ac.addBonus( chr.counts.hands - chr.counts.melee_hands < 1 ? 1 : 3, this, 'class'); }
    },
    SwordmageAegis: new chargen.System.Feature.featureSelector(['Assult', 'Shielding'], undefined, ['AegisOfAssult', 'AegisOfShielding'])
  }

  feature.Artificer = {};

}