/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package org.mage.card.arcane;
import java.awt.Image;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import mage.view.CardView;
import org.apache.log4j.Logger;
import org.apache.log4j.jmx.LoggerDynamicMBean;
/**
*
* @author StravantUser
*/
public final class TextboxRuleParser {
private static final Logger LOGGER = Logger.getLogger(CardPanel.class);
private static final Pattern BasicManaAbility = Pattern.compile("\\{T\\}: Add \\{(\\w)\\} to your mana pool\\.");
private static final Pattern LevelAbilityPattern = Pattern.compile("Level (\\d+)-?(\\d*)(\\+?)");
private static final Pattern LoyaltyAbilityPattern = Pattern.compile("^(\\+|\\-)(\\d+|X): ");
private static final Pattern SimpleKeywordPattern = Pattern.compile("^(\\w+( \\w+)?)\\s*(\\([^\\)]*\\))?\\s*$");
// Parse a given rule (given as a string) into a TextboxRule, replacing
// symbol annotations, italics, etc, parsing out information such as
// if the ability is a loyalty ability, and returning an TextboxRule
// representing that information, which can be used to render the rule in
// the textbox of a card.
public static TextboxRule parse(CardView source, String rule) {
// List of regions to apply
ArrayList<TextboxRule.AttributeRegion> regions = new ArrayList<>();
// Leveler / loyalty / basic
boolean isLeveler = false;
int levelFrom = 0;
int levelTo = 0;
boolean isLoyalty = false;
int loyaltyChange = 0;
boolean isBasicMana = false;
String basicManaSymbol = "";
// Parse the attributedString contents
int index = 0;
int outputIndex = 0;
// Is it a simple keyword ability?
{
Matcher simpleKeywordMatch = SimpleKeywordPattern.matcher(rule);
if (simpleKeywordMatch.find()) {
return new TextboxKeywordRule(simpleKeywordMatch.group(1), regions);
}
}
// Is it a basic mana ability?
{
Matcher basicManaMatcher = BasicManaAbility.matcher(rule);
if (basicManaMatcher.find()) {
isBasicMana = true;
basicManaSymbol = basicManaMatcher.group(1);
}
}
// Check if it's a loyalty ability. Must be right at the start of the rule
{
Matcher loyaltyMatch = LoyaltyAbilityPattern.matcher(rule);
if (loyaltyMatch.find()) {
// Get the loyalty change
if (loyaltyMatch.group(2).equals("X")) {
loyaltyChange = TextboxLoyaltyRule.MINUS_X;
} else {
loyaltyChange = Integer.parseInt(loyaltyMatch.group(2));
if (loyaltyMatch.group(1).equals("-")) {
loyaltyChange = -loyaltyChange;
}
}
isLoyalty = true;
// Go past the match
index = loyaltyMatch.group().length();
}
}
Deque<Integer> openingStack = new ArrayDeque<>();
StringBuilder build = new StringBuilder();
while (index < rule.length()) {
int initialIndex = index;
char ch = rule.charAt(index);
switch (ch) {
case '{': {
// Handling for `{this}`
int closeIndex = rule.indexOf('}', index);
if (closeIndex == -1) {
// Malformed input, nothing to do
++index;
++outputIndex;
build.append(ch);
} else {
String contents = rule.substring(index + 1, closeIndex);
if (contents.equals("this") || contents.equals("source")) {
// Replace {this} with the card's name
String cardName = source.getName();
build.append(cardName);
index += contents.length() + 2;
outputIndex += cardName.length();
} else {
Image symbol = ManaSymbols.getSizedManaSymbol(contents.replace("/", ""), 10);
if (symbol != null) {
// Mana or other inline symbol
build.append('#');
regions.add(new TextboxRule.EmbeddedSymbol(contents, outputIndex));
++outputIndex;
index = closeIndex + 1;
} else {
// Bad entry
build.append('{');
build.append(contents);
build.append('}');
index = closeIndex + 1;
outputIndex += (contents.length() + 2);
}
}
}
break;
}
case '&':
// Handling for `—`
if (rule.startsWith("—", index)) {
build.append('—');
index += 7;
++outputIndex;
} else if (rule.startsWith("&bull", index)) {
build.append('•');
index += 5;
++outputIndex;
} else {
LOGGER.error("Bad &...; sequence `" + rule.substring(index + 1, index + 10) + "` in rule.");
build.append('&');
++index;
++outputIndex;
}
break;
case '<': {
// Handling for `<i>` and `<br/>`
int closeIndex = rule.indexOf('>', index);
if (closeIndex != -1) {
// Is a tag
String tag = rule.substring(index + 1, closeIndex);
if (tag.charAt(tag.length() - 1) == '/') {
// Pure closing tag (like <br/>)
if (tag.equals("br/")) {
build.append('\n');
++outputIndex;
} else {
// Unknown
build.append('<').append(tag).append('>');
outputIndex += (tag.length() + 2);
}
} else if (tag.charAt(0) == '/') {
// Opening index for the tag
int openingIndex;
if (openingStack.isEmpty()) {
// Malformed input, just make an empty interval
openingIndex = outputIndex;
} else {
openingIndex = openingStack.pop();
}
// What tag is it?
switch (tag) {
case "/i":
// Italics
regions.add(new TextboxRule.ItalicRegion(openingIndex, outputIndex));
break;
case "/b":
// Bold, see if it's a level ability
String content = build.substring(openingIndex);
Matcher levelMatch = LevelAbilityPattern.matcher(content);
if (levelMatch.find()) {
try {
levelFrom = Integer.parseInt(levelMatch.group(1));
if (!levelMatch.group(2).isEmpty()) {
levelTo = Integer.parseInt(levelMatch.group(2));
}
if (!levelMatch.group(3).isEmpty()) {
levelTo = TextboxLevelRule.AND_HIGHER;
}
isLeveler = true;
} catch (Exception e) {
LOGGER.error("Bad leveler levels in rule `" + rule + "`.");
}
}
break;
default:
// Unknown
build.append('<').append(tag).append('>');
outputIndex += (tag.length() + 2);
break;
}
} else // Is it a <br> tag special case? [Why can't it have a closing `/`... =( ]
{
if (tag.equals("br")) {
build.append('\n');
++outputIndex;
} else {
// Opening tag
openingStack.push(outputIndex);
}
}
// Skip characters
index = closeIndex + 1;
} else {
// Malformed tag
build.append('<');
++outputIndex;
++index;
}
break;
}
default:
// Normal character
++index;
++outputIndex;
build.append(ch);
break;
}
if (outputIndex != build.length()) {
// Somehow our parsing code output symbols but didn't update the output index correspondingly
LOGGER.error("The human is dead; mismatch! Failed on rule: `" + rule + "` due to not updating outputIndex properly.");
// Bail out
build = new StringBuilder(rule);
regions.clear();
break;
}
if (index == initialIndex) {
// Somehow our parsing failed to consume the
LOGGER.error("Failed on rule `" + rule + "` due to not consuming a character.");
// Bail out
build = new StringBuilder(rule);
regions.clear();
break;
}
}
// Build and return the rule
rule = build.toString();
if (isLoyalty) {
return new TextboxLoyaltyRule(rule, regions, loyaltyChange);
} else if (isLeveler) {
return new TextboxLevelRule(rule, regions, levelFrom, levelTo);
} else if (isBasicMana) {
return new TextboxBasicManaRule(rule, regions, basicManaSymbol);
} else {
return new TextboxRule(rule, regions);
}
}
}