import React from "react";
import { useContext } from "react";
import { DispatchProp } from "react-redux";
import { NavigateFunction, useNavigate } from "react-router-dom";
import { AnyAction } from "redux";

import {
  ApiAdapterPromise,
  ApiAdapterRequestConfig,
  ApiResponse,
  BaseSpellContract,
  characterActions,
  CharacterPreferences,
  CharClass,
  ChoiceData,
  ClassDefinitionContract,
  ClassSpellInfo,
  Constants,
  DataOriginRefData,
  DefinitionContract,
  DefinitionPool,
  DefinitionUtils,
  EntitledEntity,
  FeatLookup,
  FeatDefinitionContract,
  HelperUtils,
  InfusionChoiceLookup,
  InfusionChoiceUtils,
  InfusionDefinitionContract,
  InfusionUtils,
  KnownInfusionUtils,
  Modifier,
  OverallSpellInfo,
  PrerequisiteData,
  RuleData,
  RuleDataUtils,
  rulesEngineSelectors,
  serviceDataActions,
  serviceDataSelectors,
  Spell,
  SpellCasterInfo,
  TypeValueLookup,
  InfusionChoice,
  ClassFeatureDefinitionContract,
  ClassFeatureUtils,
  ClassSpellListSpellsLookup,
  OptionalClassFeatureLookup,
  OptionalClassFeatureUtils,
  CharacterTheme,
  InventoryLookup,
  ItemUtils,
  InventoryManager,
  EntityRestrictionData,
} from "@dndbeyond/character-rules-engine/es";

import { HpSummary } from "~/subApps/builder/components/HpSummary";
import { RouteKey } from "~/subApps/builder/constants";
import {
  ModalData,
  useModalManager,
} from "~/subApps/builder/contexts/ModalManager";

import { SimpleClassSpellList } from "../../../../Shared/components/SimpleClassSpellList";
import { InventoryManagerContext } from "../../../../Shared/managers/InventoryManagerContext";
import { apiCreatorSelectors } from "../../../../Shared/selectors";
import {
  MakeClassBasedApiClassFeaturesRequest,
  MakeClassBasedApiSpellsRequest,
} from "../../../../Shared/selectors/composite/apiCreator";
import { AppNotificationUtils } from "../../../../Shared/utils";
import Page from "../../../components/Page";
import { navigationConfig } from "../../../config";
import { ProgressionManager } from "../../ProgressionManager";
import ConnectedBuilderPage from "../ConnectedBuilderPage";
import ClassManager from "./ClassManager";

function getClassSpellList(
  charClass: CharClass,
  classSpellLists: Array<ClassSpellInfo>
): ClassSpellInfo | null {
  const foundClassSpellList = classSpellLists.find(
    (classSpellList) => classSpellList.charClass.id === charClass.id
  );
  return foundClassSpellList ? foundClassSpellList : null;
}

// TODO: We are typing with AnyAction here to work around issues with UnknownAction, the type that was introduced in Redux 5.
// If we decide to modernize to Redux 5, we will need to refactor much of the Rules Engine redux usage.
interface Props extends DispatchProp<AnyAction> {
  ruleData: RuleData;
  overallSpellInfo: OverallSpellInfo;
  prerequisiteData: PrerequisiteData;
  totalClassLevel: number;
  choiceInfo: ChoiceData;
  classSpellLists: Array<ClassSpellInfo>;
  spellCasterInfo: SpellCasterInfo;
  preferences: CharacterPreferences;
  globalModifiers: Array<Modifier>;
  typeValueLookup: TypeValueLookup;
  definitionPool: DefinitionPool;
  classes: Array<CharClass>;
  loadAvailableOptionalClassFeatures: MakeClassBasedApiClassFeaturesRequest;
  loadAvailableSubclasses: (
    baseClassId: number,
    additionalConfig?: Partial<ApiAdapterRequestConfig>
  ) => ApiAdapterPromise<ApiResponse<Array<ClassDefinitionContract>>>;
  loadAvailableFeats: (
    additionalConfig?: Partial<ApiAdapterRequestConfig>
  ) => ApiAdapterPromise<ApiResponse<Array<FeatDefinitionContract>>>;
  loadAvailableInfusions: (
    additionalConfig?: Partial<ApiAdapterRequestConfig>
  ) => ApiAdapterPromise<
    ApiResponse<EntitledEntity<InfusionDefinitionContract>>
  >;
  makeLoadClassAlwaysKnownSpells: MakeClassBasedApiSpellsRequest;
  makeLoadClassRemainingSpells: MakeClassBasedApiSpellsRequest;
  infusionChoiceLookup: InfusionChoiceLookup;
  featLookup: FeatLookup;
  knownInfusionLookup: Record<string, InfusionChoice>;
  knownReplicatedItems: Array<string>;
  dataOriginRefData: DataOriginRefData;
  classSpellListSpellsLookup: ClassSpellListSpellsLookup;
  optionalClassFeatureLookup: OptionalClassFeatureLookup;
  theme: CharacterTheme;
  inventoryLookup: InventoryLookup;
  inventoryManager: InventoryManager;
  navigate: NavigateFunction;
  characterId: number;
  activeSourceCategories: Array<number>;
  createModal: (modalData: ModalData) => void;
  entityRestrictionData: EntityRestrictionData;
}

class ClassesManage extends React.PureComponent<Props> {
  handleInfusionChoiceItemChangePromise = (
    choiceKey: string,
    infusionId: string,
    itemDefinitionKey: string | null,
    itemName: string | null,
    accept: () => void,
    reject: () => void
  ): void => {
    // Bag of Holding change to something else
    const {
      dispatch,
      createModal,
      infusionChoiceLookup,
      inventoryLookup,
      inventoryManager,
    } = this.props;

    let itemId: string | null = null;
    let itemTypeId: string | null = null;
    if (itemDefinitionKey !== null) {
      itemId = DefinitionUtils.getDefinitionKeyId(itemDefinitionKey);
      itemTypeId = DefinitionUtils.getDefinitionKeyType(itemDefinitionKey);
    }

    const infusionChoice = HelperUtils.lookupDataOrFallback(
      infusionChoiceLookup,
      choiceKey
    );
    if (infusionChoice === null) {
      reject();
      return;
    }
    const infusion = InfusionChoiceUtils.getInfusion(infusionChoice);

    if (infusion) {
      const knownInfusion =
        InfusionChoiceUtils.getKnownInfusion(infusionChoice);
      let knownInfusionItemName: string | null = null;
      if (knownInfusion !== null) {
        knownInfusionItemName = KnownInfusionUtils.getItemName(knownInfusion);
      }
      const infusionId = InfusionUtils.getId(infusion);
      if (infusionId === null) {
        reject();
        return;
      }

      // Get the item
      const inventoryMappingId = InfusionUtils.getInventoryMappingId(infusion);
      const item = HelperUtils.lookupDataOrFallback(
        inventoryLookup,
        inventoryMappingId
      );

      createModal({
        content: (
          <div>
            <p>
              You are about to remove the existing replicated item{" "}
              <strong>{knownInfusionItemName}</strong>.
            </p>
            <p>
              Are you sure you want to change your choice to{" "}
              <strong>{itemName}</strong>?
            </p>
          </div>
        ),
        props: {
          heading: "Change Infusion Item Warning",
          color: "secondary",
          size: "fit-content",
          onConfirm: () => {
            if (item) {
              const isContainer = ItemUtils.isContainer(item);

              if (isContainer) {
                inventoryManager.handleRemove({ item });
                dispatch(
                  serviceDataActions.knownInfusionMappingSet(
                    choiceKey,
                    infusionId,
                    itemId,
                    itemTypeId,
                    itemName
                  )
                );
                accept();
                return;
              }
            }
            dispatch(
              serviceDataActions.infusionMappingDestroy(
                infusionId,
                InfusionUtils.getInventoryMappingId(infusion)
              )
            );
            dispatch(
              serviceDataActions.knownInfusionMappingSet(
                choiceKey,
                infusionId,
                itemId,
                itemTypeId,
                itemName
              )
            );
            accept();
          },
          onClose: () => {
            reject();
          },
        },
      });
    } else {
      dispatch(
        serviceDataActions.knownInfusionMappingSet(
          choiceKey,
          infusionId,
          itemId,
          itemTypeId,
          itemName
        )
      );
      accept();
    }
  };

  handleInfusionChoiceItemDestroyPromise = (
    choiceKey: string,
    accept: () => void,
    reject: () => void
  ): void => {
    const {
      dispatch,
      createModal,
      infusionChoiceLookup,
      inventoryLookup,
      inventoryManager,
    } = this.props;

    const infusionChoice = HelperUtils.lookupDataOrFallback(
      infusionChoiceLookup,
      choiceKey
    );
    if (infusionChoice === null) {
      reject();
      return;
    }
    const knownInfusion = InfusionChoiceUtils.getKnownInfusion(infusionChoice);
    if (knownInfusion === null) {
      reject();
      return;
    }
    const simulatedInfusion =
      KnownInfusionUtils.getSimulatedInfusion(knownInfusion);
    if (simulatedInfusion === null) {
      reject();
      return;
    }
    const simulatedInfusionId = InfusionUtils.getId(simulatedInfusion);
    if (simulatedInfusionId === null) {
      reject();
      return;
    }

    const infusion = InfusionChoiceUtils.getInfusion(infusionChoice);

    if (infusion) {
      const infusionId = InfusionUtils.getId(infusion);
      if (infusionId === null) {
        reject();
        return;
      }

      // Get the item
      const inventoryMappingId = InfusionUtils.getInventoryMappingId(infusion);
      const item = HelperUtils.lookupDataOrFallback(
        inventoryLookup,
        inventoryMappingId
      );

      createModal({
        content: (
          <div>
            <p>
              Are you sure you want to remove the existing replicated item{" "}
              <strong>{KnownInfusionUtils.getItemName(knownInfusion)}</strong>?
            </p>
          </div>
        ),
        props: {
          heading: "Remove Infusion Item Warning",
          variant: "remove",
          confirmButtonText: "Remove",
          size: "fit-content",
          onConfirm: () => {
            if (item) {
              const isContainer = ItemUtils.isContainer(item);

              if (isContainer) {
                inventoryManager.handleRemove({ item });
                dispatch(
                  serviceDataActions.knownInfusionMappingSet(
                    choiceKey,
                    simulatedInfusionId,
                    null,
                    null,
                    null
                  )
                );
                accept();
                return;
              }
            }
            dispatch(
              serviceDataActions.infusionMappingDestroy(
                infusionId,
                InfusionUtils.getInventoryMappingId(infusion)
              )
            );
            dispatch(
              serviceDataActions.knownInfusionMappingSet(
                choiceKey,
                simulatedInfusionId,
                null,
                null,
                null
              )
            );
            accept();
          },
          onClose: () => {
            reject();
          },
        },
      });
    } else {
      dispatch(
        serviceDataActions.knownInfusionMappingSet(
          choiceKey,
          simulatedInfusionId,
          null,
          null,
          null
        )
      );
      accept();
    }
  };

  handleInfusionChoiceChangePromise = (
    choiceKey: string,
    infusionId: string,
    accept: () => void,
    reject: () => void
  ): void => {
    // Changing from replicate magic item to something else

    const {
      dispatch,
      createModal,
      infusionChoiceLookup,
      definitionPool,
      inventoryLookup,
      inventoryManager,
    } = this.props;

    const infusionChoice = HelperUtils.lookupDataOrFallback(
      infusionChoiceLookup,
      choiceKey
    );
    if (infusionChoice === null) {
      reject();
      return;
    }
    const infusion = InfusionChoiceUtils.getInfusion(infusionChoice);

    // if an infusion exists
    if (infusion) {
      const knownInfusion =
        InfusionChoiceUtils.getKnownInfusion(infusionChoice);
      let knownInfusionName: string | null = null;
      if (knownInfusion !== null) {
        const simulatedInfusion =
          KnownInfusionUtils.getSimulatedInfusion(knownInfusion);
        if (simulatedInfusion !== null) {
          knownInfusionName = InfusionUtils.getName(simulatedInfusion);
        }
      }

      const definitionKey = DefinitionUtils.generateDefinitionKey(
        Constants.DefinitionTypeEnum.INFUSION,
        infusionId
      );
      const newInfusion = InfusionUtils.simulateInfusion(
        definitionKey,
        definitionPool
      );
      let newInfusionName: string | null = null;
      if (newInfusion !== null) {
        newInfusionName = InfusionUtils.getName(newInfusion);
      }

      const destroyInfusionId = InfusionUtils.getId(infusion);
      if (destroyInfusionId === null) {
        reject();
        return;
      }

      // Get the item
      const inventoryMappingId = InfusionUtils.getInventoryMappingId(infusion);
      const item = HelperUtils.lookupDataOrFallback(
        inventoryLookup,
        inventoryMappingId
      );

      createModal({
        content: (
          <div>
            <p>
              You are about to remove all infusion data related to the{" "}
              <strong>{knownInfusionName}</strong> Infusion.
            </p>
            <p>
              Are you sure you want to change your infusion to{" "}
              <strong>{newInfusionName}</strong>?
            </p>
          </div>
        ),
        props: {
          heading: "Change Infusion Warning",
          color: "secondary",
          size: "fit-content",
          onConfirm: () => {
            if (item) {
              const isContainer = ItemUtils.isContainer(item);

              if (isContainer) {
                inventoryManager.handleRemove({ item });
                dispatch(
                  serviceDataActions.knownInfusionMappingSet(
                    choiceKey,
                    infusionId,
                    null,
                    null,
                    null
                  )
                );
                accept();
                return;
              }
            }
            dispatch(
              serviceDataActions.infusionMappingDestroy(
                destroyInfusionId,
                InfusionUtils.getInventoryMappingId(infusion)
              )
            );
            dispatch(
              serviceDataActions.knownInfusionMappingSet(
                choiceKey,
                infusionId,
                null,
                null,
                null
              )
            );
            accept();
          },
          onClose: () => {
            reject();
          },
        },
      });
    } else {
      dispatch(
        serviceDataActions.knownInfusionMappingSet(
          choiceKey,
          infusionId,
          null,
          null,
          null
        )
      );
      accept();
    }
  };

  handleInfusionChoiceDestroyPromise = (
    choiceKey: string,
    accept: () => void,
    reject: () => void
  ): void => {
    const {
      dispatch,
      createModal,
      infusionChoiceLookup,
      inventoryLookup,
      inventoryManager,
    } = this.props;

    const infusionChoice = HelperUtils.lookupDataOrFallback(
      infusionChoiceLookup,
      choiceKey
    );
    if (infusionChoice === null) {
      reject();
      return;
    }
    const infusion = InfusionChoiceUtils.getInfusion(infusionChoice);

    // if an infusion exists
    if (infusion) {
      const knownInfusion =
        InfusionChoiceUtils.getKnownInfusion(infusionChoice);
      let knownInfusionName: string | null = null;
      if (knownInfusion !== null) {
        const simulatedInfusion =
          KnownInfusionUtils.getSimulatedInfusion(knownInfusion);
        if (simulatedInfusion !== null) {
          knownInfusionName = InfusionUtils.getName(simulatedInfusion);
        }
      }

      // Get the item
      const inventoryMappingId = InfusionUtils.getInventoryMappingId(infusion);
      const item = HelperUtils.lookupDataOrFallback(
        inventoryLookup,
        inventoryMappingId
      );

      createModal({
        content: (
          <div>
            <p>
              Are you sure you want to remove all infusion data related to the{" "}
              <strong>{knownInfusionName}</strong> Infusion?
            </p>
          </div>
        ),
        props: {
          heading: "Remove Infusion Warning",
          variant: "remove",
          size: "fit-content",
          onConfirm: () => {
            if (item) {
              const isContainer = ItemUtils.isContainer(item);

              if (isContainer) {
                inventoryManager.handleRemove({ item });
                dispatch(
                  serviceDataActions.knownInfusionMappingDestroy(choiceKey)
                );
                accept();
                return;
              }
            }
            dispatch(serviceDataActions.knownInfusionMappingDestroy(choiceKey));
            accept();
          },
          onClose: () => {
            reject();
          },
        },
      });
    } else {
      dispatch(serviceDataActions.knownInfusionMappingDestroy(choiceKey));
      accept();
    }
  };

  handleInfusionChoiceCreatePromise = (
    choiceKey: string,
    infusionId: string,
    accept: () => void,
    reject: () => void
  ): void => {
    const { dispatch } = this.props;

    dispatch(
      serviceDataActions.knownInfusionMappingCreate(choiceKey, infusionId)
    );
    accept();
  };

  handleOptionalFeatureSelection = (
    definitionKey: string,
    affectedClassFeatureDefinitionKey: string | null
  ): void => {
    const { dispatch } = this.props;

    const classFeatureId =
      DefinitionUtils.hack__getDefinitionKeyId(definitionKey);
    if (!classFeatureId) {
      return;
    }

    const affectedClassFeatureId = affectedClassFeatureDefinitionKey
      ? DefinitionUtils.hack__getDefinitionKeyId(
          affectedClassFeatureDefinitionKey
        )
      : null;

    dispatch(
      characterActions.optionalClassFeatureCreate(
        classFeatureId,
        affectedClassFeatureId
      )
    );
  };

  handleRemoveOptionalFeatureSelectionPromise = (
    definitionKey: string,
    newIsEnabled: boolean,
    accept: () => void,
    reject: () => void
  ): void => {
    const {
      dispatch,
      createModal,
      optionalClassFeatureLookup,
      classSpellListSpellsLookup,
    } = this.props;

    const optionalFeature = HelperUtils.lookupDataOrFallback(
      optionalClassFeatureLookup,
      definitionKey
    );
    if (!optionalFeature) {
      return;
    }

    const optionalClassFeature =
      OptionalClassFeatureUtils.getClassFeature(optionalFeature);
    if (!optionalClassFeature) {
      return;
    }

    const classFeatureId =
      OptionalClassFeatureUtils.getClassFeatureId(optionalFeature);
    const spellListIds =
      OptionalClassFeatureUtils.getRemoveMappingSpellListIds(optionalFeature);
    const hasSpellsToRemove = spellListIds.some((id) =>
      classSpellListSpellsLookup.hasOwnProperty(id)
    );

    if (!hasSpellsToRemove) {
      dispatch(characterActions.optionalClassFeatureDestroy(classFeatureId));
      accept();
    } else {
      createModal({
        content: (
          <div>
            <p>
              You are about to remove{" "}
              <strong>{ClassFeatureUtils.getName(optionalClassFeature)}</strong>{" "}
              from your character.
            </p>
            <p>
              After doing so, the following spells provided by this feature will
              be removed from your character:
            </p>
            <SimpleClassSpellList
              spellListIds={spellListIds}
              classSpellListSpellsLookup={classSpellListSpellsLookup}
            />
          </div>
        ),
        props: {
          heading: "Optional Feature Warning",
          variant: "remove",
          size: "fit-content",
          onConfirm: () => {
            dispatch(
              characterActions.optionalClassFeatureDestroy(classFeatureId)
            );
            accept();
          },
          onClose: () => {
            reject();
          },
        },
      });
    }
  };

  handleOnChangeOptionalClassFeatureReplacementPromise = (
    definitionKey: string,
    newAffectedDefinitionKey: string | null,
    oldAffectedDefinitionKey: string | null,
    accept: () => void,
    reject: () => void
  ): void => {
    const {
      dispatch,
      createModal,
      optionalClassFeatureLookup,
      classSpellListSpellsLookup,
    } = this.props;

    if (newAffectedDefinitionKey === oldAffectedDefinitionKey) {
      return;
    }

    const optionalFeature = HelperUtils.lookupDataOrFallback(
      optionalClassFeatureLookup,
      definitionKey
    );
    if (!optionalFeature) {
      return;
    }

    const optionalClassFeature =
      OptionalClassFeatureUtils.getClassFeature(optionalFeature);
    if (!optionalClassFeature) {
      return;
    }

    const classFeatureId =
      OptionalClassFeatureUtils.getClassFeatureId(optionalFeature);
    const newAffectedClassFeatureId: number | null = newAffectedDefinitionKey
      ? DefinitionUtils.hack__getDefinitionKeyId(newAffectedDefinitionKey)
      : null;
    const spellListIds =
      OptionalClassFeatureUtils.getUpdateMappingSpellListIdsToRemove(
        optionalFeature,
        {
          affectedClassFeatureId: newAffectedClassFeatureId,
        }
      );
    const hasSpellsToRemove = spellListIds.some((id) =>
      classSpellListSpellsLookup.hasOwnProperty(id)
    );

    if (!hasSpellsToRemove) {
      dispatch(
        characterActions.optionalClassFeatureSetRequest(
          classFeatureId,
          newAffectedClassFeatureId
        )
      );
      accept();
    } else {
      createModal({
        content: (
          <div>
            <p>
              You are about to change the Class Feature to be replaced by{" "}
              <strong>{ClassFeatureUtils.getName(optionalClassFeature)}</strong>
              .
            </p>
            <p>
              After doing so, the following spells provided by this feature will
              be removed from your character:
            </p>
            <SimpleClassSpellList
              spellListIds={spellListIds}
              classSpellListSpellsLookup={classSpellListSpellsLookup}
            />
          </div>
        ),
        props: {
          heading: "Optional Feature Warning",
          variant: "remove",
          size: "fit-content",
          onConfirm: () => {
            dispatch(
              characterActions.optionalClassFeatureSetRequest(
                classFeatureId,
                newAffectedClassFeatureId
              )
            );
            accept();
          },
          onClose: () => {
            reject();
          },
        },
      });
    }
  };

  handleSpellAdd = (
    charClass: CharClass,
    spell: Spell,
    classMappingId: number
  ): void => {
    const { dispatch } = this.props;
    dispatch(
      characterActions.spellCreate(
        spell,
        classMappingId,
        AppNotificationUtils.handleSpellCreateAccepted.bind(
          this,
          spell,
          charClass
        )
      )
    );
  };

  handlePrepare = (spell: Spell, characterClassId: number): void => {
    const { dispatch } = this.props;
    dispatch(characterActions.spellPreparedSet(spell, characterClassId, true));
  };

  handleUnprepare = (spell: Spell, characterClassId: number): void => {
    const { dispatch } = this.props;
    dispatch(characterActions.spellPreparedSet(spell, characterClassId, false));
  };

  handleRemove = (spell: Spell, characterClassId: number): void => {
    const { dispatch } = this.props;
    dispatch(characterActions.spellRemove(spell, characterClassId));
  };

  handleAlwaysKnownLoad = (
    spells: Array<BaseSpellContract>,
    classId: number
  ): void => {
    const { dispatch } = this.props;
    dispatch(serviceDataActions.classAlwaysKnownSpellsSet(spells, classId));
  };

  handleClassFeatureChoiceChange = (
    classId: number,
    classFeatureId: number,
    choiceType: any,
    choiceId: string,
    optionValue: number | null,
    parentChoiceId: string | null,
    hasItemMappings: boolean
  ): void => {
    const { dispatch, inventoryManager } = this.props;
    dispatch(
      characterActions.classFeatureChoiceSetRequest(
        classId,
        classFeatureId,
        choiceType,
        choiceId,
        optionValue,
        parentChoiceId,
        hasItemMappings
          ? inventoryManager.handleAcceptOnSuccess(true)
          : undefined
      )
    );
  };

  handleDefinitionsLoaded = (
    definitions: Array<DefinitionContract>,
    accessTypes: Record<string, number>
  ): void => {
    const { dispatch } = this.props;
    dispatch(serviceDataActions.definitionPoolAdd(definitions, accessTypes));
  };

  handleFeatureDefinitionsLoaded = (
    entitledData: EntitledEntity<ClassFeatureDefinitionContract>
  ): void => {
    const { dispatch } = this.props;

    dispatch(
      serviceDataActions.definitionPoolAdd(
        entitledData.definitionData,
        entitledData.accessTypes
      )
    );
  };

  handleAddAnotherClass = (): void => {
    const { navigate, characterId } = this.props;
    navigate(
      navigationConfig
        .getRouteDefPath(RouteKey.CLASS_CHOOSE)
        .replace(":characterId", characterId.toString())
    );
  };

  render() {
    const {
      classes,
      totalClassLevel,
      choiceInfo,
      classSpellLists,
      spellCasterInfo,
      preferences,
      ruleData,
      loadAvailableFeats,
      loadAvailableSubclasses,
      loadAvailableInfusions,
      loadAvailableOptionalClassFeatures,
      makeLoadClassAlwaysKnownSpells,
      makeLoadClassRemainingSpells,
      globalModifiers,
      typeValueLookup,
      definitionPool,
      overallSpellInfo,
      prerequisiteData,
      featLookup,
      knownReplicatedItems,
      knownInfusionLookup,
      dataOriginRefData,
      classSpellListSpellsLookup,
      optionalClassFeatureLookup,
      theme,
      activeSourceCategories,
      inventoryManager,
      entityRestrictionData,
    } = this.props;

    const levelsRemaining: number = Math.max(
      0,
      RuleDataUtils.getMaxCharacterLevel(ruleData) - totalClassLevel
    );

    return (
      <Page clsNames={["classes-manage"]}>
        <div className="classes-manage-primary">
          <div className="classes-manage-primary-section classes-manage-primary-section-progression">
            <ProgressionManager />
          </div>
          <HpSummary />
        </div>
        {classes.map((charClass) => (
          <ClassManager
            theme={theme}
            charClass={charClass}
            loadAvailableFeats={loadAvailableFeats}
            loadAvailableSubclasses={loadAvailableSubclasses}
            loadRemainingSpellList={makeLoadClassRemainingSpells(charClass)}
            loadAlwaysKnownSpells={makeLoadClassAlwaysKnownSpells(charClass)}
            loadAvailableOptionalClassFeatures={loadAvailableOptionalClassFeatures(
              charClass
            )}
            loadAvailableInfusions={loadAvailableInfusions}
            levelsRemaining={levelsRemaining}
            onClassFeatureChoiceChange={this.handleClassFeatureChoiceChange}
            onSpellPrepare={this.handlePrepare}
            onSpellUnprepare={this.handleUnprepare}
            onSpellRemove={this.handleRemove}
            onSpellAdd={this.handleSpellAdd.bind(this, charClass)}
            onAlwaysKnownLoad={this.handleAlwaysKnownLoad}
            onInfusionChoiceItemChangePromise={
              this.handleInfusionChoiceItemChangePromise
            }
            onInfusionChoiceItemDestroyPromise={
              this.handleInfusionChoiceItemDestroyPromise
            }
            onInfusionChoiceChangePromise={
              this.handleInfusionChoiceChangePromise
            }
            onInfusionChoiceDestroyPromise={
              this.handleInfusionChoiceDestroyPromise
            }
            onInfusionChoiceCreatePromise={
              this.handleInfusionChoiceCreatePromise
            }
            onOptionalFeatureSelection={this.handleOptionalFeatureSelection}
            onRemoveSelectionPromise={
              this.handleRemoveOptionalFeatureSelectionPromise
            }
            onChangeReplacementPromise={
              this.handleOnChangeOptionalClassFeatureReplacementPromise
            }
            onDefinitionsLoaded={this.handleDefinitionsLoaded}
            onFeatureDefinitionsLoaded={this.handleFeatureDefinitionsLoaded}
            key={charClass.id}
            isMulticlass={classes.length > 1}
            choiceInfo={choiceInfo}
            preferences={preferences}
            classSpellList={getClassSpellList(charClass, classSpellLists)}
            spellCasterInfo={spellCasterInfo}
            ruleData={ruleData}
            overallSpellInfo={overallSpellInfo}
            prerequisiteData={prerequisiteData}
            typeValueLookup={typeValueLookup}
            globalModifiers={globalModifiers}
            definitionPool={definitionPool}
            featLookup={featLookup}
            knownInfusionLookup={knownInfusionLookup}
            knownReplicatedItems={knownReplicatedItems}
            dataOriginRefData={dataOriginRefData}
            optionalClassFeatureLookup={optionalClassFeatureLookup}
            classSpellListSpellsLookup={classSpellListSpellsLookup}
            activeSourceCategories={activeSourceCategories}
            inventoryManager={inventoryManager}
            entityRestrictionData={entityRestrictionData}
          />
        ))}
        {levelsRemaining !== 0 && (
          <div className="classes-manage-actions">
            <div
              className="classes-manage-actions-action"
              onClick={this.handleAddAnotherClass}
            >
              + Add Another Class
            </div>
          </div>
        )}
      </Page>
    );
  }
}

function ClassesManageContainer(props) {
  const { inventoryManager } = useContext(InventoryManagerContext);
  const navigate = useNavigate();
  const { createModal } = useModalManager();

  return (
    <ClassesManage
      inventoryManager={inventoryManager}
      navigate={navigate}
      createModal={createModal}
      {...props}
    />
  );
}

export default ConnectedBuilderPage(
  ClassesManageContainer,
  RouteKey.CLASS_MANAGE,
  (state) => {
    return {
      classes: rulesEngineSelectors.getClasses(state),
      classSpellLists: rulesEngineSelectors.getClassSpellLists(state),
      loadAvailableFeats: apiCreatorSelectors.makeLoadAvailableFeats(state),
      loadAvailableSubclasses:
        apiCreatorSelectors.makeLoadAvailableSubclasses(state),
      loadAvailableInfusions:
        apiCreatorSelectors.makeLoadAvailableInfusions(state),
      loadAvailableOptionalClassFeatures:
        apiCreatorSelectors.makeLoadAvailableOptionalClassFeatures(state),
      makeLoadClassRemainingSpells:
        apiCreatorSelectors.makeLoadClassRemainingSpells(state),
      makeLoadClassAlwaysKnownSpells:
        apiCreatorSelectors.makeLoadClassAlwaysKnownSpells(state),
      totalClassLevel: rulesEngineSelectors.getTotalClassLevel(state),
      choiceInfo: rulesEngineSelectors.getChoiceInfo(state),
      preferences: rulesEngineSelectors.getCharacterPreferences(state),
      spellCasterInfo: rulesEngineSelectors.getSpellCasterInfo(state),
      overallSpellInfo: rulesEngineSelectors.getOverallSpellInfo(state),
      ruleData: rulesEngineSelectors.getRuleData(state),
      prerequisiteData: rulesEngineSelectors.getPrerequisiteData(state),
      globalModifiers: rulesEngineSelectors.getValidGlobalModifiers(state),
      typeValueLookup:
        rulesEngineSelectors.getCharacterValueLookupByType(state),
      definitionPool: serviceDataSelectors.getDefinitionPool(state),
      infusionChoiceLookup: rulesEngineSelectors.getInfusionChoiceLookup(state),
      featLookup: rulesEngineSelectors.getFeatLookup(state),
      knownInfusionLookup: rulesEngineSelectors.getKnownInfusionLookup(state),
      knownReplicatedItems: rulesEngineSelectors.getKnownReplicatedItems(state),
      classSpellListSpellsLookup:
        rulesEngineSelectors.getClassSpellListSpellsLookup(state),
      dataOriginRefData: rulesEngineSelectors.getDataOriginRefData(state),
      optionalClassFeatureLookup:
        rulesEngineSelectors.getOptionalClassFeatureLookup(state),
      theme: rulesEngineSelectors.getCharacterTheme(state),
      inventoryLookup: rulesEngineSelectors.getInventoryLookup(state),
      characterId: rulesEngineSelectors.getId(state),
      activeSourceCategories:
        rulesEngineSelectors.getActiveSourceCategories(state),
      entityRestrictionData:
        rulesEngineSelectors.getEntityRestrictionData(state),
    };
  }
);
