package com.github.czyzby.lml.parser.impl.tag;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.ui.Skin;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.ObjectMap;
import com.github.czyzby.kiwi.util.common.Strings;
import com.github.czyzby.kiwi.util.gdx.collection.GdxArrays;
import com.github.czyzby.lml.parser.LmlParser;
import com.github.czyzby.lml.parser.tag.LmlActorBuilder;
import com.github.czyzby.lml.parser.tag.LmlTag;
import com.github.czyzby.lml.util.LmlUtilities;
import com.github.czyzby.lml.util.collection.IgnoreCaseStringMap;
/** Common base for all tag handlers.
*
* @author MJ
* @see AbstractMacroLmlTag
* @see AbstractActorLmlTag */
public abstract class AbstractLmlTag implements LmlTag {
private final LmlParser parser;
private final Array<String> attributes;
private final ObjectMap<String, String> namedAttributes;
private final String tagName;
private final LmlTag parentTag;
private final boolean parent, macro;
public AbstractLmlTag(final LmlParser parser, final LmlTag parentTag, final StringBuilder rawTagData) {
this.parser = parser;
this.parentTag = parentTag;
final Array<String> entities = extractTagEntities(rawTagData, parser);
final String tagName = entities.first();
macro = Strings.startsWith(tagName, parser.getSyntax().getMacroMarker());
this.tagName = LmlUtilities.stripMarker(tagName, parser.getSyntax().getMacroMarker());
final String lastAttribute = GdxArrays.getLast(entities);
if (Strings.endsWith(lastAttribute, parser.getSyntax().getClosedTagMarker())) {
// The tag ends with a closing marker, which means it is a child.
parent = false;
if (lastAttribute.length() == 1) {
GdxArrays.removeLast(entities);
} else {
entities.set(GdxArrays.sizeOf(entities) - 1, LmlUtilities.stripEnding(lastAttribute));
}
} else {
parent = true;
}
if (hasAttributes(entities) || hasDefaultAttributes(tagName)) {
entities.removeIndex(0); // Removing tag name from attributes.
attributes = entities;
if (supportsNamedAttributes() || supportsOptionalNamedAttributes()) {
namedAttributes = new IgnoreCaseStringMap<String>();
} else {
namedAttributes = null;
}
fillAttributes(parser, entities);
} else {
attributes = null;
namedAttributes = null;
}
}
/** @param tagName name of the tag.
* @return true if the tag has default attributes assigned. */
protected boolean hasDefaultAttributes(final String tagName) {
return false;
}
/** @return true if this tag type supports named attributes and they should be mapped. */
protected abstract boolean supportsNamedAttributes();
/** @return true if this tag can have both named and unnamed attributes. Named attributes will be filled only if ALL
* attributes are valid and named, but exception will not be thrown if some of the attributes are invalid.
* This is default behavior for some obscure macros. */
protected boolean supportsOptionalNamedAttributes() {
return false;
}
private static boolean hasAttributes(final Array<String> entities) {
// The first entity is name, so at least 2 entities are required:
return entities.size > 1;
}
private static Array<String> extractTagEntities(final StringBuilder rawTagData, final LmlParser parser) {
Strings.replace(rawTagData, "\\n", "\n");
Strings.replace(rawTagData, ">", ">");
boolean inQuotation = false, inDoubleQuotation = false, lastCharWhitespace = true;
final Array<String> entities = GdxArrays.newArray(String.class);
final StringBuilder builder = new StringBuilder();
lastCharWhitespace = true;
for (int index = 0, length = rawTagData.length(); index < length; index++) {
final char character = rawTagData.charAt(index);
if (Strings.isWhitespace(character)) {
if (inQuotation || inDoubleQuotation) {
builder.append(character);
continue;
} else if (lastCharWhitespace) {
continue;
}
lastCharWhitespace = true;
entities.add(builder.toString());
Strings.clearBuilder(builder);
} else if (character == '\'') {
lastCharWhitespace = false;
builder.append(character);
if (!inDoubleQuotation) {
inQuotation = !inQuotation;
}
} else if (character == '"') {
lastCharWhitespace = false;
builder.append(character);
if (!inQuotation) {
inDoubleQuotation = !inDoubleQuotation;
}
} else {
lastCharWhitespace = false;
builder.append(character);
}
}
if (!lastCharWhitespace) {
entities.add(builder.toString());
}
return entities;
}
private void fillAttributes(final LmlParser parser, final Array<String> entities) {
// Starting from 1, since 0 index is the tag name.
for (int index = entities.size - 1; index >= 0; index--) { // Iterating backwards to take removal into account.
final String rawAttribute = entities.get(index);
if (Strings.isBlank(rawAttribute)) {
entities.removeIndex(index);
continue;
}
final String entity = LmlUtilities.stripQuotation(rawAttribute);
entities.set(index, entity);
if ((supportsNamedAttributes() || supportsOptionalNamedAttributes()) && Strings.isNotEmpty(entity)) {
final int separatorIndex = entity.indexOf(parser.getSyntax().getAttributeSeparator());
if (Strings.isCharacterAbsent(separatorIndex)) {
if (supportsNamedAttributes()) {
parser.throwErrorIfStrict("Invalid attribute format: \"" + entity
+ "\". Attribute might be missing assignment character ('"
+ parser.getSyntax().getAttributeSeparator() + "') or be otherwise unparseable.");
}
// Supports optionally: parsing another attribute.
continue;
}
final String attributeName = entity.substring(0, separatorIndex);
final String attributeValue = LmlUtilities
.stripQuotation(entity.substring(separatorIndex + 1, entity.length()));
namedAttributes.put(attributeName, attributeValue);
}
}
}
@Override
public boolean isParent() {
return parent;
}
@Override
public boolean isChild() {
return !parent;
}
@Override
public boolean isMacro() {
return macro;
}
@Override
public LmlTag getParent() {
return parentTag;
}
@Override
public String getTagName() {
return tagName;
}
@Override
public Array<String> getAttributes() {
return attributes;
}
@Override
public ObjectMap<String, String> getNamedAttributes() {
return namedAttributes;
}
@Override
public boolean hasAttribute(final String name) {
return namedAttributes != null && namedAttributes.containsKey(name);
}
@Override
public String getAttribute(final String name) {
return namedAttributes == null ? null : namedAttributes.get(name);
}
/** @return parser used to create this tag. */
protected LmlParser getParser() {
return parser;
}
/** @param builder contains ID of the skin.
* @return utility shortcut method that returns skin from parser's LML data object. */
protected Skin getSkin(final LmlActorBuilder builder) {
return getSkin(builder.getSkinName());
}
/** @param name ID of the skin.
* @return utility shortcut method that returns skin from parser's LML data object. */
protected Skin getSkin(final String name) {
final Skin skin = findSkin(name);
if (skin == null) {
parser.throwError("Unknown skin ID. Skin with name: " + name + " is unavailable.");
}
return skin;
}
private Skin findSkin(final String name) {
if (name == null) {
return parser.getData().getDefaultSkin();
}
return parser.getData().getSkin(name);
}
@Override
public boolean isAttachable() {
// By default, both macros and simple widgets are not attachable.
return false;
}
@Override
public void attachTo(final LmlTag tag) {
// By default, both macros and simple widgets are not attachable.
throw new IllegalStateException("This tag is not attachable: " + tagName);
}
/** Utility method. Allows to extract a {@link Stage} instance of either the passed actor or any of its parents.
*
* @param actor can be null. Its stage can be null.
* @return a {@link Stage} instance if any of the actors in hierarchy is added to one. */
protected Stage determineStage(final Actor actor) {
Stage stage = actor == null ? null : actor.getStage();
if (stage == null) {
LmlTag ancestorTag = getParent();
while (ancestorTag != null && stage == null) {
final Actor ancestor = ancestorTag.getActor();
if (ancestor != null) {
stage = ancestor.getStage();
}
ancestorTag = ancestorTag.getParent();
}
}
return stage;
}
}