/*
* Copyright 2010-14 (C) Tom Parker <thpr@users.sourceforge.net>
*
* This library is free software; you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; either version 2.1 of the License, or (at your option)
* any later version.
*
* This library is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this library; if not, write to the Free Software Foundation, Inc.,
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package pcgen.cdom.choiceset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Stack;
import pcgen.base.util.ObjectContainer;
import pcgen.cdom.base.CDOMReference;
import pcgen.cdom.base.ChooseInformation;
import pcgen.cdom.base.Converter;
import pcgen.cdom.base.PrimitiveChoiceSet;
import pcgen.cdom.base.PrimitiveCollection;
import pcgen.cdom.base.PrimitiveFilter;
import pcgen.cdom.content.AbilitySelection;
import pcgen.cdom.enumeration.GroupingState;
import pcgen.cdom.enumeration.ObjectKey;
import pcgen.cdom.reference.CDOMSingleRef;
import pcgen.core.Ability;
import pcgen.core.AbilityCategory;
import pcgen.core.PlayerCharacter;
import pcgen.util.Logging;
public class CollectionToAbilitySelection implements
PrimitiveChoiceSet<AbilitySelection>
{
private final PrimitiveCollection<Ability> collection;
private final CDOMSingleRef<AbilityCategory> category;
private static Stack<Ability> infiniteLoopDetectionStack = new Stack<>();
public CollectionToAbilitySelection(CDOMSingleRef<AbilityCategory> cat, PrimitiveCollection<Ability> coll)
{
if (cat == null)
{
throw new IllegalArgumentException(
"Category must not be null");
}
if (coll == null)
{
throw new IllegalArgumentException(
"PrimitiveCollection must not be null");
}
category = cat;
collection = coll;
}
@Override
public Class<? super AbilitySelection> getChoiceClass()
{
return AbilitySelection.class;
}
@Override
public GroupingState getGroupingState()
{
return collection.getGroupingState();
}
@Override
public String getLSTformat(boolean useAny)
{
return collection.getLSTformat(useAny);
}
@Override
public Collection<AbilitySelection> getSet(PlayerCharacter pc)
{
Collection<? extends AbilityWithChoice> aColl =
collection.getCollection(pc, new ExpandingConverter(pc));
Set<AbilitySelection> returnSet = new HashSet<>();
for (AbilityWithChoice a : aColl)
{
processAbility(pc, returnSet, a);
}
return returnSet;
}
private void processAbility(PlayerCharacter character,
Set<AbilitySelection> returnSet, AbilityWithChoice awc)
{
Ability a = awc.getAbility();
if (infiniteLoopDetectionStack.contains(a))
{
Stack<Ability> current = new Stack<>();
current.addAll(infiniteLoopDetectionStack);
Logging.errorPrint("Error: Circular Expansion Found: "
+ reportCircularExpansion(current));
return;
}
try
{
infiniteLoopDetectionStack.push(a);
if (a.getSafe(ObjectKey.MULTIPLE_ALLOWED).booleanValue())
{
returnSet.addAll(addMultiplySelectableAbility(character, a,
awc.getChoice()));
}
else
{
returnSet.add(new AbilitySelection(a, null));
}
}
finally
{
infiniteLoopDetectionStack.pop();
}
}
private Collection<AbilitySelection> addMultiplySelectableAbility(
final PlayerCharacter aPC, Ability ability, String subName)
{
boolean isPattern = false;
String nameRoot = null;
if (subName != null)
{
final int percIdx = subName.indexOf('%');
if (percIdx > -1)
{
isPattern = true;
nameRoot = subName.substring(0, percIdx);
}
else if (!subName.isEmpty())
{
nameRoot = subName;
}
}
ChooseInformation<?> chooseInfo = ability.get(ObjectKey.CHOOSE_INFO);
final List<String> availableList = getAvailableList(aPC, chooseInfo);
// Remove any that don't match
if (nameRoot != null && !nameRoot.isEmpty())
{
for (int n = availableList.size() - 1; n >= 0; --n)
{
final String aString = availableList.get(n);
if (!aString.startsWith(nameRoot))
{
availableList.remove(n);
}
}
// Example: ADD:FEAT(Skill Focus(Craft (Basketweaving))) If you
// have no ranks in Craft (Basketweaving), the available list
// will
// be empty
//
// Make sure that the specified feat is available, even though
// it
// does not meet the prerequisite
if (isPattern && !availableList.isEmpty())
{
availableList.add(nameRoot);
}
}
List<AbilitySelection> returnList =
new ArrayList<>(availableList.size());
for (String s : availableList)
{
returnList.add(new AbilitySelection(ability, s));
}
return returnList;
}
private <T> List<String> getAvailableList(final PlayerCharacter aPC,
ChooseInformation<T> chooseInfo)
{
final List<String> availableList = new ArrayList<>();
Collection<? extends T> tempAvailList = chooseInfo.getSet(aPC);
// chooseInfo may have sent us back weaponprofs, abilities or
// strings, so we have to do a conversion here
for (T o : tempAvailList)
{
availableList.add(chooseInfo.encodeChoice(o));
}
return availableList;
}
private String reportCircularExpansion(Stack<Ability> s)
{
StringBuilder sb = new StringBuilder(2000);
processCircularExpansion(sb, s);
sb.append(" which is a circular reference");
return sb.toString();
}
private void processCircularExpansion(StringBuilder sb, Stack<Ability> s)
{
Ability a = s.pop();
if (!s.isEmpty())
{
processCircularExpansion(sb, s);
sb.append(" which includes");
}
sb.append(a.getCDOMCategory()).append(' ').append(a.getKeyName());
sb.append(" selects items: ");
sb.append(a.get(ObjectKey.CHOOSE_INFO).getLSTformat());
sb.append('\n');
}
public CDOMSingleRef<AbilityCategory> getCategory()
{
return category;
}
/**
* Returns the consistent-with-equals hashCode for this
* CollectionToAbilitySelection
*
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode()
{
return collection.hashCode();
}
/**
* Returns true if this CollectionToAbilitySelection is equal to the given
* Object. Equality is defined as being another CollectionToAbilitySelection
* object with equal underlying contents.
*
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj)
{
return (obj instanceof CollectionToAbilitySelection)
&& ((CollectionToAbilitySelection) obj).collection
.equals(collection);
}
public static class ExpandingConverter implements
Converter<Ability, AbilityWithChoice>
{
private final PlayerCharacter character;
public ExpandingConverter(PlayerCharacter pc)
{
character = pc;
}
@Override
public Collection<AbilityWithChoice> convert(ObjectContainer<Ability> ref)
{
Set<AbilityWithChoice> returnSet = new HashSet<>();
for (Ability a : ref.getContainedObjects())
{
processAbility(ref, returnSet, a);
}
return returnSet;
}
private void processAbility(ObjectContainer<Ability> ref,
Set<AbilityWithChoice> returnSet, Ability a)
{
String choice = null;
if (ref instanceof CDOMReference)
{
choice = ((CDOMReference<?>) ref).getChoice();
}
returnSet.add(new AbilityWithChoice(a, choice));
}
@Override
public Collection<AbilityWithChoice> convert(
ObjectContainer<Ability> ref, PrimitiveFilter<Ability> lim)
{
Set<AbilityWithChoice> returnSet = new HashSet<>();
for (Ability a : ref.getContainedObjects())
{
if (lim.allow(character, a))
{
processAbility(ref, returnSet, a);
}
}
return returnSet;
}
}
/*
* A custom object, NOT an AbilitySelection. The reasoning is that
* AbilitySelection does enforcement of MULT:YES to allow/require choices
* and (1) We can't guarantee we have that here as all expansions may be
* legal (2) We don't want to compromise on the tight enforcement by
* AbilitySelection
*/
private static class AbilityWithChoice
{
private final Ability ability;
private final String choice;
public AbilityWithChoice(Ability a, String c)
{
ability = a;
choice = c;
}
public Ability getAbility()
{
return ability;
}
public String getChoice()
{
return choice;
}
@Override
public int hashCode()
{
return ability.hashCode() ^ ((choice == null) ? 17 : choice.hashCode());
}
@Override
public boolean equals(Object o)
{
if (o == this)
{
return true;
}
if (o instanceof AbilityWithChoice)
{
AbilityWithChoice other = (AbilityWithChoice) o;
if (choice == null)
{
if (other.choice != null)
{
return false;
}
}
return ability.equals(other.ability)
&& ((choice == other.choice) || choice.equals(other.choice));
}
return false;
}
}
}