Metadatable.java

package it.fulminazzo.yagl;

import it.fulminazzo.fulmicollection.objects.Refl;
import it.fulminazzo.fulmicollection.utils.ReflectionUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.lang.reflect.Field;
import java.util.*;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * An interface that holds key-value pairs of {@link String}s.
 */
public interface Metadatable {
    /**
     * A function used to convert the variables in {@link #apply(Object)}.
     */
    Function<@NotNull String, @NotNull String> VARIABLE_FORMAT = s -> "<" + s + ">";
    /**
     * A function used to parse the variables names.
     */
    Function<@NotNull String, @NotNull String> VARIABLE_PARSER = s -> {
        final String separator = "_";

        StringBuilder builder = new StringBuilder();
        for (String string : s.split("")) {
            if (string.matches("[A-Z]")) builder.append(separator);
            else if (string.matches("[\r\t\n -]")) {
                builder.append(separator);
                continue;
            }
            builder.append(string);
        }

        String output = builder.toString().toLowerCase();
        while (output.startsWith(separator))
            output = output.substring(separator.length());
        return output;
    };

    /**
     * Sets variable.
     *
     * @param name  the name
     * @param value the value
     * @return this metadatable
     */
    default @NotNull Metadatable setVariable(final @NotNull String name, final @NotNull String value) {
        variables().put(VARIABLE_PARSER.apply(name), value);
        return this;
    }

    /**
     * Unset variable metadatable.
     *
     * @param name the name
     * @return this metadatable
     */
    default @NotNull Metadatable unsetVariable(final @NotNull String name) {
        variables().remove(VARIABLE_PARSER.apply(name));
        return this;
    }

    /**
     * Checks if the current {@link Metadatable} has the given variable.
     *
     * @param name the name
     * @return true if it contains
     */
    default boolean hasVariable(final @NotNull String name) {
        return getVariable(VARIABLE_PARSER.apply(name)) != null;
    }

    /**
     * Gets variable.
     * Returns <i>null</i> in case of missing.
     *
     * @param name the name
     * @return the variable
     */
    default @Nullable String getVariable(final @NotNull String name) {
        return variables().get(VARIABLE_PARSER.apply(name));
    }

    /**
     * Copies all the variables from this metadatable to the given one.
     *
     * @param other   the other metadatable
     * @param replace if false, if the other already has the variable, it will not be replaced
     * @return this metadatable
     */
    default @NotNull Metadatable copyAll(final @NotNull Metadatable other, final boolean replace) {
        variables().forEach((k, v) -> {
            if (!other.hasVariable(k) || replace) other.setVariable(k, v);
        });
        return this;
    }

    /**
     * Uses {@link #copyAll(Metadatable, boolean)} to copy from the given {@link Metadatable} to this one.
     *
     * @param other   the other metadatable
     * @param replace if false, if this already has the variable, it will not be replaced
     * @return this metadatable
     */
    default @NotNull Metadatable copyFrom(final @NotNull Metadatable other, final boolean replace) {
        other.copyAll(this, replace);
        return this;
    }

    /**
     * Applies all the current variables to the given object fields,
     * by replacing in every string all the variables in the format {@link #VARIABLE_FORMAT}.
     *
     * @param <T>    the type parameter
     * @param object the object
     * @return the object parsed
     */
    @SuppressWarnings("unchecked")
    default <T> T apply(final T object) {
        if (object == null) return null;
        else if (object.getClass().isEnum()) return object;
        else if (object instanceof String) return (T) apply((String) object);
        else if (object instanceof Collection) return (T) apply((Collection<Object>) object);
        else if (object instanceof Map) return (T) apply((Map<Object, Object>) object);
        else if (!ReflectionUtils.isPrimitiveOrWrapper(object.getClass())) {
            final Refl<T> refl = new Refl<>(object);
            for (Field field : refl.getNonStaticFields())
                ReflectionUtils.setAccessible(field)
                        .map(f -> f.get(object))
                        .filter(o -> !o.toString().contains("Lambda"))
                        .ifPresent(o -> field.set(object, apply(o)));
        }
        return object;
    }

    /**
     * Applies all the current variables to the given string.
     *
     * @param string the string
     * @return the string parsed
     */
    default String apply(@NotNull String string) {
        for (final String v : variables().keySet()) {
            final String s = getVariable(v);
            if (s == null) continue;
            final Matcher matcher = Pattern.compile("([^\\\\]|^)" + VARIABLE_FORMAT.apply(v)).matcher(string);
            while (matcher.find())
                string = string.substring(0, matcher.start()) + matcher.group(1) + s +
                        string.substring(matcher.end());
        }
        return string;
    }

    /**
     * Applies all the current variables to the given collection.
     *
     * @param collection the collection
     * @return the collection parsed
     */
    @SuppressWarnings("unchecked")
    default @NotNull Collection<Object> apply(Collection<Object> collection) {
        Class<?> clazz = collection.getClass();
        // In the case of creation with Arrays.asList()
        if (clazz.getCanonicalName().equals(Arrays.class.getCanonicalName() + ".ArrayList"))
            clazz = ArrayList.class;
        Class<Collection<Object>> finalClass = (Class<Collection<Object>>) clazz;
        return collection.stream().map(this::apply)
                .collect(Collectors.toCollection(() -> new Refl<>(finalClass, new Object[0]).getObject()));
    }

    /**
     * Applies all the current variables to the given map.
     *
     * @param map the map
     * @return the map parsed
     */
    default @NotNull Map<Object, Object> apply(final @NotNull Map<Object, Object> map) {
        final List<Object> keys = new ArrayList<>(map.keySet());
        for (Object key : keys) {
            if (key == null) continue;
            Object value = map.get(key);
            if (value != null) {
                map.remove(key);
                map.put(apply(key), apply(value));
            }
        }
        return map;
    }

    /**
     * Gets the current variables in form of {@link Map}.
     *
     * @return the map
     */
    @NotNull Map<String, String> variables();
}