/*
 * This file is part of UltimateCore, licensed under the MIT License (MIT).
 *
 * Copyright (c) Bammerbom
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package bammerbom.ultimatecore.sponge.api.language.utils;

import org.spongepowered.api.CatalogTypes;
import org.spongepowered.api.Sponge;
import org.spongepowered.api.command.CommandSource;
import org.spongepowered.api.text.LiteralText;
import org.spongepowered.api.text.Text;
import org.spongepowered.api.text.format.TextColor;
import org.spongepowered.api.text.format.TextColors;
import org.spongepowered.api.text.format.TextStyle;
import org.spongepowered.api.text.format.TextStyles;
import org.spongepowered.api.text.serializer.TextSerializers;

import java.util.*;

public class TextUtil {

    private static Map<TextColor, Character> colors;
    private static Map<TextStyle, Character> styles;

    static {
        colors = new HashMap<>();
        colors.put(TextColors.BLACK, '0');
        colors.put(TextColors.DARK_BLUE, '1');
        colors.put(TextColors.DARK_GREEN, '2');
        colors.put(TextColors.DARK_AQUA, '3');
        colors.put(TextColors.DARK_RED, '4');
        colors.put(TextColors.DARK_PURPLE, '5');
        colors.put(TextColors.GOLD, '6');
        colors.put(TextColors.GRAY, '7');
        colors.put(TextColors.DARK_GRAY, '8');
        colors.put(TextColors.BLUE, '9');
        colors.put(TextColors.GREEN, 'a');
        colors.put(TextColors.AQUA, 'b');
        colors.put(TextColors.RED, 'c');
        colors.put(TextColors.LIGHT_PURPLE, 'd');
        colors.put(TextColors.YELLOW, 'e');
        colors.put(TextColors.WHITE, 'f');

        styles = new HashMap<>();
        styles.put(TextStyles.OBFUSCATED, 'k');
        styles.put(TextStyles.BOLD, 'l');
        styles.put(TextStyles.STRIKETHROUGH, 'm');
        styles.put(TextStyles.UNDERLINE, 'n');
        styles.put(TextStyles.ITALIC, 'o');
        styles.put(TextStyles.RESET, 'r');
    }

    public static Character getColorChar(TextColor color) {
        return colors.get(color);
    }

    public static Character getStyleChar(TextStyle style) {
        return styles.get(style);
    }

    public static Text replaceColors(Text text, CommandSource p, String permissionPrefix) {
        Text.Builder builder = Text.builder();
        for (Text child : getAllChildren(text)) {
            Text fnl = merge(replaceColors(child.toPlain(), p, permissionPrefix), child);
            builder.append(fnl);
        }
        return builder.toText();
    }

    public static Text replaceColors(String rawmessage, CommandSource p, String permissionPrefix) {
        for (TextColor color : Sponge.getRegistry().getAllOf(CatalogTypes.TEXT_COLOR)) {
            if (!p.hasPermission(permissionPrefix + ".color." + color.getId().toLowerCase())) {
                continue;
            }
            Character ch = TextUtil.getColorChar(color);
            rawmessage = rawmessage.replaceAll("&" + ch, "\u00A7" + ch);
            //rawmessage = TextUtil.replace(rawmessage, );
            //rawmessage = TextUtil.replace(rawmessage, "&" + , Text.of(color, "a"));
        }
        for (TextStyle.Base style : Sponge.getRegistry().getAllOf(TextStyle.Base.class)) {
            if (!p.hasPermission(permissionPrefix + ".style." + style.getId().toLowerCase())) {
                continue;
            }
            Character ch = TextUtil.getStyleChar(style);
            rawmessage = rawmessage.replaceAll("&" + ch, "\u00A7" + ch);
            //rawmessage = TextUtil.replace(rawmessage, "&", Text.of("ยง"));
        }
        return TextSerializers.LEGACY_FORMATTING_CODE.deserialize(rawmessage);
    }

    public static List<String> getVariables(Text text) {
        String plain = text.toPlain();
        List<String> results = new ArrayList<>();
        StringBuilder builder = new StringBuilder();
        boolean invar = false;
        for (char c : plain.toCharArray()) {
            if (c == '%') {
                invar = !invar;
                //If not in var, add builder to list and start new builder
                if (!invar) {
                    builder.append(c);
                    results.add(builder.toString());
                    builder = new StringBuilder();
                    continue;
                }
            }
            //If in var, append character to builder
            if (invar) {
                builder.append(c);
            }
        }
        return results;
    }

    /**
     * Get a list of Text objects, all containing a char from the text.
     * This is the same as {@link String#toCharArray()}, but keeps formatting.
     *
     * @param text The text to get the characters for
     * @return A list of formatted chars
     */
    public static List<LiteralText> getFormattedChars(Text text) {
        //Split the text in a list of chars
        List<LiteralText> chars = new ArrayList<>();
        //Get all children
        List<Text> children = getAllChildren(text);
        //Get all chars
        for (Text child : children) {
            for (char c : getContent(child).toCharArray()) {
                LiteralText.Builder builder =
                    LiteralText.builder().append(Text.of(c)).format(child.getFormat()).onClick(child.getClickAction().orElse(null)).onHover(child.getHoverAction().orElse(null)).onShiftClick(child.getShiftClickAction().orElse(null));
                chars.add(builder.build());
            }
        }
        return chars;
    }

    /**
     * This will get a specific piece of text, starting at {@code init} and ending at {@code end - 1}.
     * This is the same as {@link String#substring(int, int)}, but keeps formatting.
     *
     * @param text  The text to get the subtext for
     * @param start The beginning index, inclusive
     * @param end   The ending index, exclusive
     * @return The subtext
     */
    public static LiteralText subtext(Text text, int start, int end) {
        if (start == end) {
            return Text.of("");
        }
        List<LiteralText> chars = getFormattedChars(text);
        //Get the chars needed
        LiteralText.Builder sub = LiteralText.builder();
        for (Text tex : Arrays.copyOfRange(chars.toArray(new Text[chars.size()]), start, end)) {
            sub.append(tex);
        }
        return (LiteralText) TextSimplifier.simplify(sub.build());
    }

    /**
     * This will replace all literal matches of {@code find} with {@code replace}.
     * This is the same as {@link String#replace(CharSequence, CharSequence)}, but keeps formatting.
     * This does NOT support regex.
     *
     * @param text    The text to search & replace in.
     * @param find    The string to search for.
     * @param replace The text to replace the string with.
     * @return The text, where every match has been replaced.
     */
    public static Text replace(Text text, String find, Text replace) {
        //TODO better escape
        if (replace.toPlain().contains(find)) {
            replace = replace(replace, find, Text.of());
        }

        int index = text.toPlain().indexOf(find);
        while (index != -1) {
            //This will make sure the replacement get formatted correctly (see merge method)
            Text charr = getChar(text, index);
            Text replacenew = merge(replace, charr);

            //Get the text before and after the found text, and put the replacement in
            Text front = subtext(text, 0, index);
            Text after = subtext(text, index + find.length(), text.toPlain().length());
            text = Text.of(front, replacenew, after);
            index = indexOf(text, find, true);
        }
        return text;
    }

    /**
     * This is the same as indexOf for a string, but is for texts and supports exclusions.
     *
     * @param text       The text to search in
     * @param find       The string to find
     * @param exclusions If true if before the match is a backslash, it is skipped
     * @return The first time the string is found.
     */
    public static int indexOf(Text text, String find, boolean exclusions) {
        List<Integer> indexes = indexesOf(text, find);
        String plain = text.toPlain();
        for (Integer i : indexes) {
            if (i == -1) return -1;
            if (exclusions && plain.toCharArray()[i - 1] == '\\') {
                continue;
            }
            return i;
        }
        return -1;
    }

    /**
     * This will replace the first literal match of {@code find} with {@code replace}.
     * This is the same as {@link String#replaceFirst(String, String)}, but keeps formatting.
     * This does NOT support regex.
     *
     * @param text    The text to search & replace in.
     * @param find    The string to search for.
     * @param replace The text to replace the string with.
     * @param from    From which index to look for replacements.
     * @return The text, where the first match has been replaced.
     */
    public static Text replaceFirst(Text text, String find, Text replace, int from) {
        int index = text.toPlain().indexOf(find, from);

        //This will make sure the replacement get formatted correctly (see merge method)
        Text charr = getChar(text, index);
        Text replacenew = merge(replace, charr);

        //Get the text before and after the found text, and put the replacement in
        Text front = subtext(text, 0, index);
        Text after = subtext(text, index + find.length(), text.toPlain().length());
        return Text.of(front, replacenew, after);
    }

    /**
     * Get a list of all literal matches of {@code split}.
     * The integer is the index of the first character of the found match.
     * This does NOT support regex.
     *
     * @param text  The text to search in
     * @param split The string to search for
     * @return The list of found indexes, empty if none are found
     */
    public static List<Integer> indexesOf(Text text, String split) {
        String string = text.toPlain();
        HashSet<Integer> results = new HashSet<>();

        int i = 0;
        while (i < string.length()) {
            int indexof = string.indexOf(split, i);
            if (indexof >= 0) {
                results.add(indexof);
            }
            i++;
        }

        return new ArrayList<>(results);
    }

    /**
     * This method gets all children of a text, including the children of the children of the children, etc....
     *
     * @param parent The {@link Text} to get the children from
     * @return A list of all the children the text has
     */
    public static List<Text> getAllChildren(Text parent) {
        ArrayList<Text> list = new ArrayList<>();
        parent.withChildren().forEach(list::add);
        return list;
    }

    /**
     * Get the content of a Text.
     * This is the toPlain() without the text of any children.
     * <p>
     * //TODO Wait for: https://github.com/SpongePowered/SpongeAPI/issues/1347
     *
     * @param text The text to get the content of
     * @return The plain content of the text
     */
    public static String getContent(Text text) {
        return text.toBuilder().removeAll().build().toPlain();
    }

    /**
     * Get the char at the specified index.
     *
     * @param text  The text to get the char in
     * @param index The index the char is at
     * @return The char
     */
    public static Text getChar(Text text, int index) {
        return getFormattedChars(text).get(index);
    }

    /**
     * Merge two texts to one Text
     * This might not do what you expect it does, read below:
     * <p>
     * This will take the text of the first, plus any formatting and actions it has.
     * It will ignore any text the second argument has, and then add formatting if the first argument has none, and add all actions the first argument doesn't have.
     * Repeat until all texts have been merged.
     * </p>
     */
    public static Text merge(Text... rawtexts) {
        //Make a modifyable list of all texts
        List<Text> texts = new ArrayList<>(Arrays.<Text>asList(rawtexts));

        if (texts.isEmpty()) {
            throw new IllegalArgumentException("Can not have zero arguments for merge.");
        }

        Text.Builder start = texts.get(0).toBuilder();
        texts.remove(0);

        while (!texts.isEmpty()) {
            Text merge = texts.get(0);

            if (start.getFormat().isEmpty()) {
                start.format(merge.getFormat());
            }
            if (!start.getClickAction().isPresent()) {
                start.onClick(merge.getClickAction().orElse(null));
            }
            if (!start.getHoverAction().isPresent()) {
                start.onHover(merge.getHoverAction().orElse(null));
            }
            if (!start.getShiftClickAction().isPresent()) {
                start.onShiftClick(merge.getShiftClickAction().orElse(null));
            }

            texts.remove(0);
        }

        return start.build();
    }

    /**
     * Split the text on {@code split}, and return all
     * This is the same as {@link String#split(String)}, but keeps formatting.
     * This does NOT support regex.
     *
     * @param text  The text to split.
     * @param split The string to split on.
     * @return All results of the split, or only the {@code text} if the {@code split} was not found.
     */
    public static List<Text> split(Text text, String split) {
        List<Integer> found = indexesOf(text, split);
        if (found.isEmpty()) {
            return Arrays.asList(text);
        }
        //Actually split the text instance
        //0 - firstmatch
        //firstmatch+split - secondmatch
        //secondmatch+split - thirdmatch
        //...
        List<Text> results = new ArrayList<>();
        results.add(subtext(text, 0, found.get(0)));
        int cur = 0;
        for (Integer res : found) {
            Integer next = found.size() > (cur + 1) ? found.get(cur + 1) : null;
            results.add(subtext(text, res + split.length(), next == null ? text.toPlain().length() : next));
            cur++;
        }
        return results;
    }

    public static Text toUppercase(Text text) {
        Text.Builder builder = Text.builder();
        for (Text stext : getAllChildren(text)) {
            builder.append(merge(Text.of(stext.toPlain().toUpperCase())), stext);
        }
        return builder.build();
    }

    public static Text toLowerCase(Text text) {
        Text.Builder builder = Text.builder();
        for (Text stext : getAllChildren(text)) {
            builder.append(merge(Text.of(stext.toPlain().toLowerCase())), stext);
        }
        return builder.build();
    }

    /**
     * Makes the first character in this string uppercase, and the others lowercase
     *
     * @param text The text to modify
     * @return The text with the first character uppercase
     */
    public static Text firstUppercase(Text text) {
        return Text.of(subtext(toUppercase(text), 0, 1), subtext(toLowerCase(text), 1, text.toPlain().length()));
    }
}