/*
* Copyright (c) 1998-2017 by Richard A. Wilkes. All rights reserved.
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, version 2.0. If a copy of the MPL was not distributed with
* this file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* This Source Code Form is "Incompatible With Secondary Licenses", as
* defined by the Mozilla Public License, version 2.0.
*/
package com.trollworks.gcs.character;
import com.trollworks.gcs.advantage.Advantage;
import com.trollworks.gcs.equipment.Equipment;
import com.trollworks.gcs.feature.Bonus;
import com.trollworks.gcs.feature.Feature;
import com.trollworks.gcs.modifier.Modifier;
import com.trollworks.gcs.preferences.SheetPreferences;
import com.trollworks.gcs.skill.Skill;
import com.trollworks.gcs.skill.Technique;
import com.trollworks.gcs.spell.Spell;
import com.trollworks.gcs.widgets.outline.ListRow;
import com.trollworks.toolkit.annotation.Localize;
import com.trollworks.toolkit.utility.Localization;
import com.trollworks.toolkit.utility.Preferences;
import com.trollworks.toolkit.utility.notification.NotifierTarget;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
/**
* A thread for doing background updates of the prerequisite status of a character sheet.
*/
public class PrerequisitesThread extends Thread implements NotifierTarget {
@Localize("Reason:")
private static String REASON;
static {
Localization.initialize();
}
private static HashMap<GURPSCharacter, PrerequisitesThread> MAP = new HashMap<>();
private static int COUNTER = 0;
private CharacterSheet mSheet;
private GURPSCharacter mCharacter;
private boolean mNeedUpdate;
private boolean mNeedRepaint;
private boolean mIsProcessing;
/**
* @param character The character being processed.
* @return The thread that does the processing.
*/
public static PrerequisitesThread getThread(GURPSCharacter character) {
synchronized (MAP) {
return MAP.get(character);
}
}
/**
* Returns only when the prerequisites thread is idle.
*
* @param character The character to wait for.
* @return The thread that does the processing.
*/
public static PrerequisitesThread waitForProcessingToFinish(GURPSCharacter character) {
PrerequisitesThread thread = getThread(character);
if (thread != null && thread != Thread.currentThread()) {
boolean checkAgain = true;
while (checkAgain) {
synchronized (thread) {
checkAgain = thread.mIsProcessing || thread.mNeedUpdate;
}
try {
sleep(200);
} catch (Exception exception) {
// Ignore...
}
}
}
return thread;
}
/**
* Creates a new prerequisites thread.
*
* @param sheet The sheet we're attached to.
*/
public PrerequisitesThread(CharacterSheet sheet) {
super("Prerequisites #" + ++COUNTER); //$NON-NLS-1$
setPriority(NORM_PRIORITY);
setDaemon(true);
mSheet = sheet;
mCharacter = sheet.getCharacter();
mNeedUpdate = true;
mCharacter.addTarget(this, Profile.ID_TECH_LEVEL, GURPSCharacter.ID_STRENGTH, GURPSCharacter.ID_DEXTERITY, GURPSCharacter.ID_INTELLIGENCE, GURPSCharacter.ID_HEALTH, GURPSCharacter.ID_WILL, GURPSCharacter.ID_PERCEPTION, Spell.ID_NAME, Spell.ID_COLLEGE, Spell.ID_POINTS, Spell.ID_LIST_CHANGED, Skill.ID_NAME, Skill.ID_SPECIALIZATION, Skill.ID_LEVEL, Skill.ID_RELATIVE_LEVEL, Skill.ID_ENCUMBRANCE_PENALTY, Skill.ID_POINTS, Skill.ID_TECH_LEVEL, Skill.ID_LIST_CHANGED, Advantage.ID_NAME, Advantage.ID_LEVELS, Advantage.ID_LIST_CHANGED, Equipment.ID_EXTENDED_WEIGHT, Equipment.ID_STATE, Equipment.ID_QUANTITY, Equipment.ID_LIST_CHANGED);
Preferences.getInstance().getNotifier().add(this, SheetPreferences.OPTIONAL_IQ_RULES_PREF_KEY, SheetPreferences.OPTIONAL_MODIFIER_RULES_PREF_KEY, SheetPreferences.OPTIONAL_STRENGTH_RULES_PREF_KEY);
synchronized (MAP) {
MAP.put(mCharacter, this);
}
}
@Override
public void run() {
try {
while (!mSheet.hasBeenDisposed()) {
try {
boolean needUpdate;
synchronized (this) {
needUpdate = mNeedUpdate;
mNeedUpdate = false;
mIsProcessing = needUpdate;
}
if (!needUpdate) {
sleep(500);
} else {
processFeatures();
processRows(mCharacter.getAdvantagesIterator(false));
processRows(mCharacter.getSkillsIterator());
processRows(mCharacter.getSpellsIterator());
processRows(mCharacter.getEquipmentIterator());
if (mNeedRepaint) {
mSheet.repaint();
}
synchronized (this) {
mIsProcessing = false;
}
}
} catch (InterruptedException iEx) {
throw iEx;
} catch (Exception exception) {
// Catch everything here so that manipulations to the character
// sheet that invalidate state don't stop our thread from
// continuing.
synchronized (this) {
mNeedUpdate = true;
}
if (mNeedRepaint) {
mSheet.repaint();
}
sleep(200);
}
}
} catch (InterruptedException outerIEx) {
// Someone is trying to terminate us... let them.
}
mNeedUpdate = mIsProcessing = false;
Preferences.getInstance().getNotifier().remove(this);
synchronized (MAP) {
MAP.remove(mCharacter);
}
}
private void processFeatures() throws Exception {
HashMap<String, ArrayList<Feature>> map = new HashMap<>();
buildFeatureMap(map, mCharacter.getAdvantagesIterator(false));
buildFeatureMap(map, mCharacter.getSkillsIterator());
buildFeatureMap(map, mCharacter.getSpellsIterator());
buildFeatureMap(map, mCharacter.getEquipmentIterator());
mCharacter.setFeatureMap(map);
}
private void buildFeatureMap(HashMap<String, ArrayList<Feature>> map, Iterator<? extends ListRow> iterator) throws Exception {
while (iterator.hasNext()) {
ListRow row = iterator.next();
if (row instanceof Equipment) {
Equipment equipment = (Equipment) row;
if (!equipment.isEquipped() || equipment.getQuantity() < 1) {
// Don't allow unequipped equipment to affect the character
continue;
}
}
for (Feature feature : row.getFeatures()) {
processFeature(map, row instanceof Advantage ? ((Advantage) row).getLevels() : 0, feature);
}
if (row instanceof Advantage) {
Advantage advantage = (Advantage) row;
for (Bonus bonus : advantage.getCRAdj().getBonuses(advantage.getCR())) {
processFeature(map, 0, bonus);
}
for (Modifier modifier : advantage.getModifiers()) {
if (modifier.isEnabled()) {
for (Feature feature : modifier.getFeatures()) {
processFeature(map, modifier.getLevels(), feature);
}
}
}
}
checkIfUpdated();
}
}
private static void processFeature(HashMap<String, ArrayList<Feature>> map, int levels, Feature feature) {
String key = feature.getKey().toLowerCase();
ArrayList<Feature> list = map.get(key);
if (list == null) {
list = new ArrayList<>(1);
map.put(key, list);
}
if (feature instanceof Bonus) {
((Bonus) feature).getAmount().setLevel(levels);
}
list.add(feature);
}
private void checkIfUpdated() throws Exception {
boolean needUpdate;
synchronized (this) {
needUpdate = mNeedUpdate;
}
if (needUpdate || mSheet.hasBeenDisposed()) {
throw new Exception();
}
}
private void processRows(Iterator<? extends ListRow> iterator) throws Exception {
StringBuilder builder = new StringBuilder();
while (iterator.hasNext()) {
ListRow row = iterator.next();
builder.setLength(0);
boolean satisfied = row.getPrereqs().satisfied(mCharacter, row, builder, "<li>"); //$NON-NLS-1$
if (satisfied && row instanceof Technique) {
satisfied = ((Technique) row).satisfied(builder, "<li>"); //$NON-NLS-1$
}
if (row.isSatisfied() != satisfied) {
row.setSatisfied(satisfied);
mNeedRepaint = true;
}
if (!satisfied) {
builder.insert(0, "<html><body>" + REASON + "<ul>"); //$NON-NLS-1$ //$NON-NLS-2$
builder.append("</ul></body></html>"); //$NON-NLS-1$
row.setReasonForUnsatisfied(builder.toString().replaceAll("<ul>", "<ul style='margin-top: 0; margin-bottom: 0;'>")); //$NON-NLS-1$ //$NON-NLS-2$
}
checkIfUpdated();
}
}
/** Marks an update request. */
public void markForUpdate() {
synchronized (this) {
mNeedUpdate = true;
}
}
@Override
public void handleNotification(Object producer, String type, Object data) {
if (SheetPreferences.OPTIONAL_IQ_RULES_PREF_KEY.equals(type)) {
mCharacter.updateWillAndPerceptionDueToOptionalIQRuleUseChange();
} else if (SheetPreferences.OPTIONAL_MODIFIER_RULES_PREF_KEY.equals(type)) {
mCharacter.notifySingle(Advantage.ID_LIST_CHANGED, null);
} else if (SheetPreferences.OPTIONAL_STRENGTH_RULES_PREF_KEY.equals(type)) {
mCharacter.notifySingle(type, data);
}
markForUpdate();
}
@Override
public int getNotificationPriority() {
return 0;
}
}