type singleWithId = {
  id: int,
  options: list(Hero.Activatable.option),
  level: Maybe.t(int),
  customCost: Maybe.t(int),
};

let isActive = (x: Hero.Activatable.t) => ListH.Extra.notNull(x.active);

let isActiveM = Maybe.maybe(false, isActive);

module Convert = {
  open Maybe;

  let heroEntryToSingles = (x: Hero.Activatable.t) =>
    x.active
    |> ListH.map((s: Hero.Activatable.single) =>
         {
           id: x.id,
           options: s.options,
           level: s.level,
           customCost: s.customCost,
         }
       );

  let singleToSingleWithId =
      (x: Hero.Activatable.t, s: Hero.Activatable.single) => {
    id: x.id,
    options: s.options,
    level: s.level,
    customCost: s.customCost,
  };
  /* /**
    * Converts the object generated by the list item to an object that can be
    * inserted into an array of ActiveObjects.
    * @param obj The entry for which you want to convert the object.
    * @param activate The object generated by the list item.
    */
   export const convertUIStateToActiveObject =
     (activate: Record<ActivatableActivationOptions>): Record<ActiveObject> => {
       const id = AAOA.id (activate)
       const selectOptionId1 = AAOA.selectOptionId1 (activate)
       const selectOptionId2 = AAOA.selectOptionId2 (activate)
       const selectOptionId3 = AAOA.selectOptionId3 (activate)
       const input = AAOA.input (activate)
       const level = AAOA.level (activate)
       const customCost = AAOA.customCost (activate)

       return id === AdvantageId.HatredOf
         ? ActiveObject ({
             sid: selectOptionId1,
             sid2: input,
             cost: customCost,
           })
         : id === DisadvantageId.PersonalityFlaw
         ? ActiveObject ({
             sid: selectOptionId1,
             sid2: or (fmap (elemF (List<number | string> (7, 8)))
                           (selectOptionId1))
               ? input
               : Nothing,
             cost: customCost,
           })
         : id === SpecialAbilityId.SkillSpecialization
         ? ActiveObject ({
             sid: selectOptionId1,
             sid2: alt<number | string> (input)
                                       (selectOptionId2),
             cost: customCost,
           })
         : isJust (input) && isJust (selectOptionId1)
         ? ActiveObject ({
             sid: selectOptionId1,
             sid2: input,
             sid3: selectOptionId2,
             tier: level,
             cost: customCost,
           })
         : ActiveObject ({
             sid: alt<number | string> (input)
                                       (selectOptionId1),
             sid2: then (guard (isJust (input) || isJust (selectOptionId1)))
                       (selectOptionId2),
             sid3: selectOptionId3,
             tier: level,
             cost: customCost,
           })
     } */
  /* /**
    * Generates a list of ActiveObjects based on the given instance.
    */
   export const convertActivatableToArray =
     (x: Record<ActivatableDependent>) =>
       pipe_ (
               x,
               ADA.active,
               imap (index => toActiveObjectWithId (index) (ADA.id (x)))
             ) */
  /* /**
    * Get all active items in an array.
    * @param state A state slice.
    */
   export const getActiveFromState =
     foldr (pipe (convertActivatableToArray, append)) (empty)

   export interface ActiveObjectAny extends ActiveObject {
     [key: string]: any
   } */

  let activatableOptionToSelectOptionId =
      (id: Hero.Activatable.option): maybe(Ids.selectOptionId) =>
    switch (id) {
    | `Generic(_) as id
    | `Skill(_) as id
    | `CombatTechnique(_) as id
    | `Spell(_) as id
    | `Cantrip(_) as id
    | `LiturgicalChant(_) as id
    | `Blessing(_) as id => Just(id)
    | `CustomInput(_) => Nothing
    };
};

module Accessors = {
  open Static;

  let name = x =>
    switch (x) {
    | Advantage(y) => y.name
    | Disadvantage(y) => y.name
    | SpecialAbility(y) => y.name
    };

  let selectOptions = x =>
    switch (x) {
    | Advantage(y) => y.selectOptions
    | Disadvantage(y) => y.selectOptions
    | SpecialAbility(y) => y.selectOptions
    };

  let input = x =>
    switch (x) {
    | Advantage(y) => y.input
    | Disadvantage(y) => y.input
    | SpecialAbility(y) => y.input
    };

  let apValue = x =>
    switch (x) {
    | Advantage(y) => y.apValue
    | Disadvantage(y) => y.apValue
    | SpecialAbility(y) => y.apValue
    };
};

module SelectOptions = {
  open Static.SelectOption;
  open Maybe;
  open Maybe.Functor;
  open Maybe.Monad;

  let getSelectOption = (x, id) =>
    id
    |> Convert.activatableOptionToSelectOptionId
    >>= Function.flip(SelectOptionMap.lookup, Accessors.selectOptions(x));

  /**
   * Get a selection option's name with the given id from given wiki entry.
   * Returns `Nothing` if not found.
   */
  let getSelectOptionName = (x, id) =>
    id |> getSelectOption(x) <&> (y => y.name);

  /**
   * Get a selection option's cost with the given id from given wiki entry.
   * Returns `Nothing` if not found.
   */
  let getSelectOptionCost = (x, id) =>
    id |> getSelectOption(x) >>= (y => y.cost);

  /**
   * Get all first select option IDs from the given entry.
   */
  let getActiveSelections = (x: Hero.Activatable.t) =>
    x.active
    |> mapMaybe((y: Hero.Activatable.single) => y.options |> listToMaybe);
  // type SecondarySelections = OrderedMap<number | string, List<string | number>>
  //
  // /**
  //  * Get all `ActiveObject.sid2` values from the given instance, sorted by
  //  * `ActiveObject.sid` in Map.
  //  * @param entry
  //  */
  // export const getActiveSecondarySelections =
  //   fmap (pipe (
  //               ADA.active,
  //               foldl ((map: SecondarySelections) => (selection: Record<ActiveObject>) =>
  //                       fromMaybe (map)
  //                                 (liftM2<string | number, string | number, SecondarySelections>
  //                                   (id => id2 => alter<List<string | number>>
  //                                     (pipe (
  //                                       fmap (consF (id2)),
  //                                       altF (Just (List (id2)))
  //                                     ))
  //                                     (id)
  //                                     (map))
  //                                   (AOA.sid (selection))
  //                                   (AOA.sid2 (selection))))
  //                     (OrderedMap.empty)
  //             ))
  //
  // /**
  //  * Get all `DependencyObject.sid` values from the given instance.
  //  * @param obj The entry.
  //  */
  // export const getRequiredSelections:
  //   (m: Maybe<Record<ActivatableDependent>>) => Maybe<List<string | number | List<number>>> =
  //     fmap (pipe (
  //       ADA.dependencies,
  //       mapMaybe<ActivatableDependency, string | number | List<number>> (pipe (
  //         ensure (isRecord),
  //         bindF (DependencyObject.A.sid)
  //       ))
  //     ))
};

let getOption = (index, heroEntry) => ListH.(heroEntry.options <!!> index);
let getOption1 = heroEntry => heroEntry.options |> Maybe.listToMaybe;
let getOption2 = getOption(1);
let getOption3 = getOption(2);

let getCustomInput = (option: Hero.Activatable.option) =>
  switch (option) {
  | `CustomInput(x) => Maybe.Just(x)
  | `Generic(_)
  | `Skill(_)
  | `CombatTechnique(_)
  | `Spell(_)
  | `LiturgicalChant(_)
  | `Cantrip(_)
  | `Blessing(_) => Nothing
  };

let getGenericId = (option: Hero.Activatable.option) =>
  switch (option) {
  | `Generic(x) => Maybe.Just(x)
  | `Skill(_)
  | `CombatTechnique(_)
  | `Spell(_)
  | `LiturgicalChant(_)
  | `Cantrip(_)
  | `Blessing(_)
  | `CustomInput(_) => Nothing
  };

let lookupMap = (k, mp, f) => Maybe.Functor.(f <$> IntMap.lookup(k, mp));

let getSkillFromOption =
    (staticData: Static.t, option: Hero.Activatable.option) =>
  switch (option) {
  | `Skill(id) => IntMap.lookup(id, staticData.skills)
  | `Generic(_)
  | `CombatTechnique(_)
  | `Spell(_)
  | `LiturgicalChant(_)
  | `Cantrip(_)
  | `Blessing(_)
  | `CustomInput(_) => Nothing
  };

module Names = {
  open Maybe;
  open Maybe.Functor;
  open Maybe.Monad;
  open Static;
  open Function;

  let getDefaultNameAddition = (staticEntry, heroEntry) => {
    let input = Accessors.input(staticEntry);
    let selectOptions = Accessors.selectOptions(staticEntry);

    let sid = heroEntry |> getOption1;
    let sid2 = heroEntry |> getOption2;

    switch (input, sid, sid2) {
    // Text input
    | (Just(_), Just(`CustomInput(str)), Nothing) => Just(str)
    // Select option and text input
    | (
        Just(_),
        Just(
          `Generic(_) as id | `Skill(_) as id | `CombatTechnique(_) as id |
          `Spell(_) as id |
          `LiturgicalChant(_) as id |
          `Cantrip(_) as id |
          `Blessing(_) as id,
        ),
        Just(`CustomInput(str)),
      )
        when SelectOption.SelectOptionMap.size(selectOptions) > 0 =>
      Just(
        (
          id
          |> SelectOptions.getSelectOptionName(staticEntry)
          |> fromMaybe("")
        )
        ++ ": "
        ++ str,
      )
    // Plain select option
    | (
        Nothing,
        Just(
          `Generic(_) as id | `Skill(_) as id | `CombatTechnique(_) as id |
          `Spell(_) as id |
          `LiturgicalChant(_) as id |
          `Cantrip(_) as id |
          `Blessing(_) as id,
        ),
        Nothing,
      ) =>
      SelectOptions.getSelectOptionName(staticEntry, id)
    | _ => Nothing
    };
  };

  /**
   * A lot of entries have customization options: Text input, select option or
   * both. This function creates a string that can be appended to the `name`
   * property of the respective record to create the full active name.
   */
  let getEntrySpecificNameAddition = (staticData, staticEntry, heroEntry) =>
    switch (staticEntry) {
    | Advantage(entry) =>
      switch (Ids.AdvantageId.fromInt(entry.id)) {
      | Aptitude
      | ExceptionalSkill =>
        heroEntry
        |> getOption1
        >>= (
          sid =>
            switch (sid) {
            | `Skill(id) => lookupMap(id, staticData.skills, x => x.name)
            | `Spell(id) => lookupMap(id, staticData.spells, x => x.name)
            | `LiturgicalChant(id) =>
              lookupMap(id, staticData.liturgicalChants, x => x.name)
            | `Generic(_)
            | `CombatTechnique(_)
            | `Cantrip(_)
            | `Blessing(_)
            | `CustomInput(_) => Nothing
            }
        )
      | ExceptionalCombatTechnique
      | WeaponAptitude =>
        heroEntry
        |> getOption1
        >>= (
          sid =>
            switch (sid) {
            | `CombatTechnique(id) =>
              lookupMap(id, staticData.combatTechniques, x => x.name)
            | `Generic(_)
            | `Skill(_)
            | `Spell(_)
            | `LiturgicalChant(_)
            | `Cantrip(_)
            | `Blessing(_)
            | `CustomInput(_) => Nothing
            }
        )
      | HatredFor =>
        heroEntry
        |> getOption1
        >>= SelectOptions.getSelectOption(staticEntry)
        |> liftM2(
             (type_, frequency: Static.SelectOption.t) =>
               type_ ++ " (" ++ frequency.name ++ ")",
             getOption2(heroEntry) >>= getCustomInput,
           )
      | _ => getDefaultNameAddition(staticEntry, heroEntry)
      }
    | Disadvantage(entry) =>
      switch (Ids.DisadvantageId.fromInt(entry.id)) {
      | Incompetent =>
        heroEntry
        |> getOption1
        >>= getSkillFromOption(staticData)
        <&> (x => x.name)
      | PersonalityFlaw =>
        heroEntry
        |> getOption1
        >>= SelectOptions.getSelectOption(staticEntry)
        <&> (
          option1 =>
            (
              switch (option1.id) {
              // Get the input if Prejudice or Unworldly is selected
              | `Generic(7 | 8) => heroEntry |> getOption2 >>= getCustomInput
              // Otherwise ignore any additional options
              | `Generic(_)
              | `Skill(_)
              | `CombatTechnique(_)
              | `Spell(_)
              | `LiturgicalChant(_)
              | `Cantrip(_)
              | `Blessing(_) => Nothing
              }
            )
            |> maybe(option1.name, specialInput =>
                 option1.name ++ ": " ++ specialInput
               )
        )
      | _ => getDefaultNameAddition(staticEntry, heroEntry)
      }
    | SpecialAbility(entry) =>
      switch (Ids.SpecialAbilityId.fromInt(entry.id)) {
      | AdaptionZauber
      | FavoriteSpellwork =>
        heroEntry
        |> getOption1
        >>= (
          sid =>
            switch (sid) {
            | `Spell(id) => lookupMap(id, staticData.spells, x => x.name)
            | `Generic(_)
            | `Skill(_)
            | `CombatTechnique(_)
            | `LiturgicalChant(_)
            | `Cantrip(_)
            | `Blessing(_)
            | `CustomInput(_) => Nothing
            }
        )
      | TraditionSavant
      | Forschungsgebiet
      | Expertenwissen
      | Wissensdurst
      | Recherchegespuer =>
        heroEntry
        |> getOption1
        >>= getSkillFromOption(staticData)
        <&> (x => x.name)
      | Lieblingsliturgie =>
        heroEntry
        |> getOption1
        >>= (
          sid =>
            switch (sid) {
            | `LiturgicalChant(id) =>
              lookupMap(id, staticData.liturgicalChants, x => x.name)
            | `Generic(_)
            | `Skill(_)
            | `CombatTechnique(_)
            | `Spell(_)
            | `Cantrip(_)
            | `Blessing(_)
            | `CustomInput(_) => Nothing
            }
        )
      | SkillSpecialization =>
        heroEntry
        |> getOption1
        >>= getSkillFromOption(staticData)
        >>= (
          skill =>
            heroEntry
            |> getOption2
            >>= (
              option2 =>
                (
                  switch (option2) {
                  // If input string use input
                  | `CustomInput(x) => Just(x)
                  // Otherwise lookup application name
                  | `Generic(id) =>
                    skill.applications
                    |> IntMap.Foldable.find((a: Skill.application) =>
                         a.id === id
                       )
                    <&> (a => a.name)
                  | `Skill(_)
                  | `CombatTechnique(_)
                  | `Spell(_)
                  | `LiturgicalChant(_)
                  | `Cantrip(_)
                  | `Blessing(_) => Nothing
                  }
                )
                // Merge skill name and application name
                <&> (appName => skill.name ++ ": " ++ appName)
            )
        )
      | Exorzist =>
        switch (heroEntry.level) {
        | Just(1) =>
          heroEntry
          |> getOption1
          >>= SelectOptions.getSelectOptionName(staticEntry)
        | _ => Nothing
        }
      | SpellEnhancement as entryId
      | ChantEnhancement as entryId =>
        heroEntry
        |> getOption1
        >>= SelectOptions.getSelectOption(staticEntry)
        >>= (
          enhancement =>
            enhancement.target
            >>= (
              id =>
                (
                  switch (entryId) {
                  | SpellEnhancement =>
                    IntMap.lookup(id, staticData.spells) <&> (x => x.name)
                  | _ =>
                    IntMap.lookup(id, staticData.liturgicalChants)
                    <&> (x => x.name)
                  }
                )
                <&> (targetName => targetName ++ ": " ++ enhancement.name)
            )
        )
      | TraditionArcaneBard =>
        heroEntry
        |> getOption1
        >>= getGenericId
        >>= flip(IntMap.lookup, staticData.arcaneBardTraditions)
      | TraditionArcaneDancer =>
        heroEntry
        |> getOption1
        >>= getGenericId
        >>= flip(IntMap.lookup, staticData.arcaneDancerTraditions)
      | LanguageSpecializations =>
        liftM2(
          SelectOptions.getSelectOption,
          IntMap.lookup(
            Ids.SpecialAbilityId.toInt(Language),
            staticData.specialAbilities,
          )
          <&> (specialAbility => SpecialAbility(specialAbility)),
          getOption1(heroEntry),
        )
        |> join
        >>= (
          language =>
            heroEntry
            |> getOption2
            >>= (
              option2 =>
                (
                  switch (option2) {
                  | `CustomInput(str) => Just(str)
                  | `Generic(specializationId) =>
                    language.specializations
                    >>= (
                      specializations =>
                        ListH.(specializations <!!> specializationId - 1)
                    )
                  | `Skill(_)
                  | `CombatTechnique(_)
                  | `Spell(_)
                  | `LiturgicalChant(_)
                  | `Cantrip(_)
                  | `Blessing(_) => Nothing
                  }
                )
                <&> (specialization => language.name ++ ": " ++ specialization)
            )
        )
      | Fachwissen =>
        heroEntry
        |> getOption1
        >>= getSkillFromOption(staticData)
        >>= (
          skill => {
            let applications =
              skill.applications
              |> IntMap.filter((app: Skill.application) =>
                   app.prerequisite |> isNothing
                 );

            [heroEntry |> getOption2, heroEntry |> getOption3]
            |> mapMaybe(option =>
                 option
                 >>= getGenericId
                 >>= (
                   opt =>
                     applications
                     |> IntMap.Foldable.find((app: Skill.application) =>
                          app.id === opt
                        )
                     <&> (app => app.name)
                 )
               )
            |> ensure(apps => apps |> ListH.Foldable.length |> (===)(2))
            <&> (
              apps =>
                apps
                |> AdvancedFiltering.sortStrings(staticData)
                |> Intl.ListFormat.format(Conjunction, staticData)
                |> (appsStr => skill.name ++ ": " ++ appsStr)
            );
          }
        )
      | _ => getDefaultNameAddition(staticEntry, heroEntry)
      }
    };

  let getDisAdvLevelStr = level =>
    level |> Integers.intToRoman |> fromMaybe(Int.show(level));

  let getSpecialAbilityLevelStr = level =>
    (level > 1 ? "I" ++ Chars.nobr ++ "–" ++ Chars.nobr : "")
    ++ getDisAdvLevelStr(level);

  /**
   * Gets the level string that hast to be appended (with a non-breaking space!)
   * to the name. This string is aware of differences between dis/advantages and
   *  special abilties as well as it handles the Native Tongue level for
   * languages.
   */
  let getLevelName = (staticData, staticEntry, singleHeroEntry) =>
    switch (staticEntry, singleHeroEntry.level) {
    | (Advantage(_), Just(level))
    | (Disadvantage(_), Just(level)) => Just(getDisAdvLevelStr(level))
    | (SpecialAbility(staticEntry), Just(level)) =>
      switch (Id.specialAbilityFromInt(staticEntry.id)) {
      // Language level 4 = Native Tongue and thus needs a special name
      | Language when level === 4 =>
        Just(staticData.messages.specialabilities_nativetonguelevel)
      | _ => Just(getSpecialAbilityLevelStr(level))
      }
    | _ => Nothing
    };

  /**
   * Some entries cannot use the default `name` property from wiki entries. The
   * value returned by may not use the default `name` property. For all entries
   * that do not need to handle a specific display format, the default `name`
   * property is used.
   */
  let getEntrySpecificNameReplacements =
      (~addLevelToName, staticEntry, nameAddition, levelName) => {
    let name = Accessors.name(staticEntry);

    let flatLevelName =
      addLevelToName ? maybe("", (++)(Chars.nbsp), levelName) : "";

    let mapNameAddition = f => maybe(name, f, nameAddition);

    let mapDefaultWithParens = () =>
      mapNameAddition(add => name ++ " (" ++ add ++ ")");

    let mapDefaultWithoutParens = () =>
      mapNameAddition(add => name ++ " " ++ add);

    let addSndInParens = snd =>
      ListH.Extra.replaceStr(")", ": " ++ snd ++ ")");

    switch (staticEntry) {
    | Advantage(entry) =>
      switch (Ids.AdvantageId.fromInt(entry.id)) {
      | ImmunityToPoison
      | ImmunityToDisease
      | HatredFor => mapDefaultWithoutParens()
      | _ => mapDefaultWithParens() ++ flatLevelName
      }
    | Disadvantage(entry) =>
      switch (Ids.DisadvantageId.fromInt(entry.id)) {
      | AfraidOf => mapDefaultWithoutParens() ++ flatLevelName
      | Principles
      | Obligations =>
        maybe(
          name ++ flatLevelName,
          nameAddition => name ++ flatLevelName ++ " (" ++ nameAddition ++ ")",
          nameAddition,
        )
      | _ => mapDefaultWithParens() ++ flatLevelName
      }
    | SpecialAbility(entry) =>
      switch (Ids.SpecialAbilityId.fromInt(entry.id)) {
      | GebieterDesAspekts => mapDefaultWithoutParens()
      | TraditionArcaneBard
      | TraditionArcaneDancer
      | TraditionSavant => mapNameAddition(flip(addSndInParens, name))
      | _ => mapDefaultWithParens() ++ flatLevelName
      }
    };
  };

  type combinedName = {
    name: string,
    baseName: string,
    addName: maybe(string),
    levelName: maybe(string),
  };

  /**
   * Returns name, splitted and combined, of advantage/disadvantage/special
   * ability as a Maybe (in case the wiki entry does not exist).
   */
  let getName = (~addLevelToName, staticData, staticEntry, heroEntry) => {
    let addName =
      getEntrySpecificNameAddition(staticData, staticEntry, heroEntry);

    let levelName = getLevelName(staticData, staticEntry, heroEntry);

    let fullName =
      getEntrySpecificNameReplacements(
        ~addLevelToName,
        staticEntry,
        addName,
        levelName,
      );

    {
      name: fullName,
      baseName: Accessors.name(staticEntry),
      addName,
      levelName,
    };
  };
  /*
   /**
    * `compressList :: L10n -> [ActiveActivatable] -> String`
    *
    * Takes a list of active Activatables and merges them together. Used to display
    * lists of Activatables on character sheet.
    */
   export const compressList =
     (staticData: StaticDataRecord) =>
     (xs: List<Record<ActiveActivatable>>): string => {
       const grouped_xs =
         elems (groupByKey<Record<ActiveActivatable>, string> (AAA_.id) (xs))

       return pipe (
                     map (
                       ifElse<List<Record<ActiveActivatable>>>
                         (xs_group => flength (xs_group) === 1)
                         (pipe (listToMaybe, maybe ("") (AAA_.name)))
                         (xs_group => pipe (
                                             map ((x: Record<ActiveActivatable>) => {
                                               const levelPart =
                                                 pipe (
                                                       AAA_.level,
                                                       fmap (pipe (toRoman, appendStr (" "))),
                                                       fromMaybe ("")
                                                     )
                                                     (x)

                                               const selectOptionPart =
                                                 fromMaybe ("") (AAA_.addName (x))

                                               return selectOptionPart + levelPart
                                             }),
                                             sortStrings (staticData),
                                             intercalate (", "),
                                             x => ` (${x})`,
                                             x => maybe ("")
                                                       ((r: Record<ActiveActivatable>) =>
                                                         AAA_.baseName (r) + x)
                                                       (listToMaybe (xs_group))
                                           )
                                           (xs_group))
                     ),
                     sortStrings (staticData),
                     intercalate (", ")
                   )
                   (grouped_xs)
     }
   */
};

module AdventurePoints = {
  open Maybe;
  open Maybe.Functor;
  open Maybe.Monad;
  open Static;
  open Function;

  let ensureFlat =
    fun
    | Static.Advantage.Flat(x) => Just(x)
    | Static.Advantage.PerLevel(_) => Nothing;

  let ensurePerLevel =
    fun
    | Static.Advantage.Flat(_) => Nothing
    | Static.Advantage.PerLevel(x) => Just(x);

  let getDefaultEntryCost = (staticEntry, singleHeroEntry) => {
    open ListH;

    let sid1 = singleHeroEntry |> getOption1;
    let level = fromMaybe(1, singleHeroEntry.level);
    let apValue =
      staticEntry |> Accessors.apValue |> fromMaybe(Static.Advantage.Flat(0));

    let optionApValue =
      sid1 >>= SelectOptions.getSelectOptionCost(staticEntry);

    switch (optionApValue) {
    | Just(x) => Just(x)
    | Nothing =>
      switch (apValue) {
      | Flat(x) => Just(x * level)
      | PerLevel(xs) =>
        switch (staticEntry) {
        | Advantage(_)
        | Disadvantage(_) => xs <!!> level - 1
        | SpecialAbility(_) =>
          xs
          |> ListH.take(Int.max(1, level))
          |> ListH.Foldable.sum
          |> (x => Just(x))
        }
      }
    };
  };

  /**
   * Returns the value(s) how the spent AP value would change after removing the
   * respective entry.
   *
   * @param isEntryToAdd If `entry` has not been added to the list of active
   * entries yet, this must be `true`, otherwise `false`.
   */
  let getEntrySpecificCost =
      (
        ~isEntryToAdd,
        staticData,
        hero: Hero.t,
        staticEntry,
        heroEntry: Hero.Activatable.t,
        singleHeroEntry,
      ) => {
    open ListH;

    let sid1 = singleHeroEntry |> getOption1;
    let level = singleHeroEntry.level;
    let apValue =
      staticEntry |> Accessors.apValue |> fromMaybe(Static.Advantage.Flat(0));

    switch (staticEntry) {
    | Advantage(entry) =>
      switch (Ids.AdvantageId.fromInt(entry.id)) {
      | Aptitude
      | ExceptionalSkill =>
        singleHeroEntry
        |> getOption1
        >>= (
          sid =>
            switch (sid, apValue) {
            | (`Skill(id), PerLevel(apValues)) =>
              IntMap.lookup(id, staticData.skills)
              >>= (static => apValues <!!> IC.icToIx(static.ic))
            | (`Spell(id), PerLevel(apValues)) =>
              IntMap.lookup(id, staticData.spells)
              >>= (static => apValues <!!> IC.icToIx(static.ic))
            | (`LiturgicalChant(id), PerLevel(apValues)) =>
              IntMap.lookup(id, staticData.liturgicalChants)
              >>= (static => apValues <!!> IC.icToIx(static.ic))
            | _ => Nothing
            }
        )
      | ExceptionalCombatTechnique
      | WeaponAptitude =>
        singleHeroEntry
        |> getOption1
        >>= (
          sid =>
            switch (sid, apValue) {
            | (`CombatTechnique(id), PerLevel(apValues)) =>
              IntMap.lookup(id, staticData.combatTechniques)
              >>= (static => apValues <!!> IC.icToIx(static.ic))
            | _ => Nothing
            }
        )
      | _ => getDefaultEntryCost(staticEntry, singleHeroEntry)
      }
    | Disadvantage(entry) =>
      switch (Ids.DisadvantageId.fromInt(entry.id)) {
      | PersonalityFlaw =>
        switch (sid1) {
        | Just(`Generic(selected_option)) =>
          let matchOption = (target_option, current) =>
            switch (current) {
            | Just(`Generic(x)) => x === target_option
            | _ => false
            };

          let isPersonalityFlawNotPaid = (target_option, paid_entries_max) =>
            target_option === selected_option
            && ListH.countBy(
                 (e: Hero.Activatable.single) =>
                   e.options
                   |> listToMaybe
                   |> matchOption(target_option)
                   // Entries with custom cost are ignored for the rule
                   && isNothing(e.customCost),
                 heroEntry.active,
               )
            > (isEntryToAdd ? paid_entries_max - 1 : paid_entries_max);

          // 7 = "Prejudice" => more than one entry possible
          // more than one entry of Prejudice does not contribute to AP spent
          //
          // 8 = "Unworldly" => more than one entry possible
          // more than two entries of Unworldly do not contribute to AP spent
          //
          // In both cases, removing the entry would not change AP so it has to
          // return 0.
          if (isPersonalityFlawNotPaid(7, 1)
              || isPersonalityFlawNotPaid(8, 2)) {
            Just(0);
          } else {
            SelectOptions.getSelectOptionCost(
              staticEntry,
              `Generic(selected_option),
            );
          };
        | _ => Nothing
        }
      | Principles
      | Obligations =>
        level
        >>= (
          level => {
            // This is the highest and the second-highest level of this entry at the
            // moment.
            let (maxLevel, sndMaxLevel) =
              heroEntry.active
              |> ListH.Foldable.foldr(
                   (active: Hero.Activatable.single, (prevMax, prevSndMax)) =>
                     switch (active.level, active.customCost) {
                     // Only get the maximum from the current and the previous level, if
                     // the current has no custom cost
                     | (Just(activeLevel), Nothing) =>
                       if (activeLevel > prevMax) {
                         (activeLevel, prevMax);
                       } else {
                         (prevMax, prevSndMax);
                       }
                     // Otherwise always return the previous max
                     | _ => (prevMax, prevSndMax)
                     },
                   (0, 0),
                 );

            // If there is more than one entry on the same level if this entry is
            // active, it won't affect AP spent at all. Thus, if the entry is to
            // be added, there must be at least one (> 0) entry for this rule to
            // take effect.
            //
            // If the entry is not the one with the highest level, adding or
            // removing it won't affect AP spent at all
            if (maxLevel > level
                || countBy(
                     (e: Hero.Activatable.single) =>
                       Maybe.Foldable.elem(level, e.level),
                     heroEntry.active,
                   )
                > (isEntryToAdd ? 0 : 1)) {
              Nothing;
            } else {
              // Otherwise, the level difference results in the cost.
              apValue |> ensureFlat <&> ( * )(level - sndMaxLevel);
            };
          }
        )
      | BadHabit =>
        apValue
        |> ensureFlat
        |> Maybe.Foldable.find(_ =>
             countBy(
               (e: Hero.Activatable.single) => isNothing(e.customCost),
               heroEntry.active,
             )
             > (isEntryToAdd ? 2 : 3)
           )
      | Incompetent =>
        singleHeroEntry
        |> getOption1
        >>= (
          sid =>
            switch (sid, apValue) {
            | (`Skill(id), PerLevel(apValues)) =>
              IntMap.lookup(id, staticData.skills)
              >>= (static => apValues <!!> IC.icToIx(static.ic))
            | _ => Nothing
            }
        )
      | _ => getDefaultEntryCost(staticEntry, singleHeroEntry)
      }
    | SpecialAbility(entry) =>
      switch (Ids.SpecialAbilityId.fromInt(entry.id)) {
      | SkillSpecialization =>
        sid1
        >>= getSkillFromOption(staticData)
        <&> (
          skill =>
            // Multiply number of final occurences of the
            // same skill...
            (
              countBy(
                (e: Hero.Activatable.single) =>
                  e.options
                  |> listToMaybe
                  |> Maybe.Foldable.elem(`Skill(skill.id))
                  // Entries with custom cost are ignored for the rule
                  && isNothing(e.customCost),
                heroEntry.active,
              )
              + (isEntryToAdd ? 1 : 0)
            )
            // ...with the skill's IC
            * IC.getAPForActivatation(skill.ic)
        )
      | Language =>
        level
        >>= (
          fun
          // Native Tongue (level 4) does not cost anything
          | 4 => Just(0)
          | level => apValue |> ensureFlat <&> ( * )(level)
        )
      | PropertyKnowledge
      | AspectKnowledge =>
        apValue
        |> ensurePerLevel
        >>= (
          apPerLevel => {
            // Ignore custom cost activations in terms of calculated cost
            let amountActive =
              countBy(
                (e: Hero.Activatable.single) => isNothing(e.customCost),
                heroEntry.active,
              );

            let index = amountActive + (isEntryToAdd ? 0 : (-1));

            apPerLevel <!!> index;
          }
        )
      | TraditionWitches =>
        // There are two disadvantages that, when active, decrease the cost of
        // this tradition by 10 AP each
        let decreaseCost = (id, cost) =>
          hero.disadvantages |> IntMap.lookup(id) |> isActiveM
            ? cost - 10 : cost;

        apValue
        |> ensureFlat
        <&> (
          flatAp =>
            flatAp
            |> decreaseCost(Id.disadvantageToInt(NoFlyingBalm))
            |> decreaseCost(Id.disadvantageToInt(NoFamiliar))
        );
      | AdaptionZauber
      | FavoriteSpellwork =>
        singleHeroEntry
        |> getOption1
        >>= (
          sid =>
            switch (sid, apValue) {
            | (`Spell(id), PerLevel(apValues)) =>
              IntMap.lookup(id, staticData.spells)
              >>= (static => apValues <!!> IC.icToIx(static.ic))
            | _ => Nothing
            }
        )
      | Lieblingsliturgie =>
        singleHeroEntry
        |> getOption1
        >>= (
          sid =>
            switch (sid, apValue) {
            | (`LiturgicalChant(id), PerLevel(apValues)) =>
              IntMap.lookup(id, staticData.liturgicalChants)
              >>= (static => apValues <!!> IC.icToIx(static.ic))
            | _ => Nothing
            }
        )
      | Forschungsgebiet
      | Expertenwissen
      | Wissensdurst
      | WegDerGelehrten
      | WegDerKuenstlerin
      | Fachwissen
      | Handwerkskunst
      | KindDerNatur
      | KoerperlichesGeschick
      | SozialeKompetenz
      | Universalgenie =>
        singleHeroEntry
        |> getOption1
        >>= (
          sid =>
            switch (sid, apValue) {
            | (`Skill(id), PerLevel(apValues)) =>
              IntMap.lookup(id, staticData.skills)
              >>= (static => apValues <!!> IC.icToIx(static.ic))
            | _ => Nothing
            }
        )
      | Recherchegespuer =>
        // The AP cost for this SA consist of two parts: AP based on the IC of
        // the main subject (from "SA_531"/Wissensdurst) in addition to AP based
        // on the IC of the side subject selected in this SA.

        hero.specialAbilities
        |> IntMap.lookup(Id.specialAbilityToInt(Wissensdurst))
        >>= (
          wissensdurst =>
            apValue
            |> ensurePerLevel
            >>= (
              apPerLevel => {
                let getCostFromHeroEntry = entry =>
                  entry
                  |> getOption1
                  >>= getSkillFromOption(staticData)
                  >>= (skill => apPerLevel <!!> IC.icToIx(skill.ic));

                liftM2(
                  (+),
                  // Cost for side subject
                  getCostFromHeroEntry(singleHeroEntry),
                  // Cost for main subject from Wissensdurst
                  wissensdurst.active
                  |> listToMaybe
                  >>= (
                    fst =>
                      fst
                      |> Convert.singleToSingleWithId(heroEntry)
                      |> getCostFromHeroEntry
                  ),
                );
              }
            )
        )
      | LanguageSpecializations =>
        apValue
        |> ensureFlat
        >>= (
          flatAp =>
            sid1
            >>= getGenericId
            >>= (
              languageId =>
                hero.specialAbilities
                |> IntMap.lookup(Id.specialAbilityToInt(Language))
                >>= (
                  language =>
                    language.active
                    |> ListH.Foldable.find((e: Hero.Activatable.single) =>
                         e.options
                         |> listToMaybe
                         >>= getGenericId
                         |> Maybe.Foldable.elem(languageId)
                       )
                    >>= (
                      selectedLanguage =>
                        selectedLanguage.level
                        <&> (
                          fun
                          | 4 => 0
                          | _ => flatAp
                        )
                    )
                )
            )
        )
      | _ => getDefaultEntryCost(staticEntry, singleHeroEntry)
      }
    };
  };

  /**
   * Returns the AP you get when removing the ActiveObject.
   *
   * @param isEntryToAdd If `entry` has not been added to the list of active
   * entries yet, this must be `true`, otherwise `false`.
   */
  let getCost =
      (
        ~isEntryToAdd,
        ~automaticAdvantages,
        staticData,
        hero: Hero.t,
        staticEntry,
        heroEntry: Hero.Activatable.t,
        singleHeroEntry,
      ) => {
    let isAutomatic = ListH.elem(singleHeroEntry.id, automaticAdvantages);

    let modifyAbs =
      switch (staticEntry) {
      | Disadvantage(_) => Int.negate
      | Advantage(_)
      | SpecialAbility(_) => id
      };

    switch (singleHeroEntry.customCost) {
    | Just(customCost) => (modifyAbs(customCost), isAutomatic)
    | Nothing =>
      getEntrySpecificCost(
        ~isEntryToAdd,
        staticData,
        hero,
        staticEntry,
        heroEntry,
        singleHeroEntry,
      )
      |> fromMaybe(0)
      |> modifyAbs
      |> (apValue => (apValue, isAutomatic))
    };
  };
};
