package tc.oc.commons.core.chat;

import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.text.AttributedCharacterIterator;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import javax.annotation.Nullable;

import com.google.common.base.Function;
import com.google.common.collect.Lists;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.ClickEvent;
import net.md_5.bungee.api.chat.HoverEvent;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.chat.TranslatableComponent;

import static com.google.common.base.Preconditions.checkNotNull;

/**
 * Utils for working with {@link BaseComponent}s
 */
public abstract class Components {

    public static BaseComponent[] fromLegacyTextMulti(String legacyText) {
        return TextComponent.fromLegacyText(ChatColor.translateAlternateColorCodes('`', legacyText));
    }

    /**
     * Convert text with legacy formatting codes to a {@link Component}
     */
    public static Component fromLegacyText(String legacyText) {
        return new Component(fromLegacyTextMulti(legacyText));
    }

    private static final Component SPACE = new Component(" ");
    private static final Component NEWLINE = new Component("\n");

    public static BaseComponent blank() { return BlankComponent.INSTANCE; }
    public static BaseComponent space() { return SPACE; }
    public static BaseComponent newline() { return NEWLINE; }

    public static List<BaseComponent> repeat(Supplier<BaseComponent> component, int amount) {
        return IntStream.rangeClosed(1, amount).mapToObj(i -> component.get()).collect(Collectors.toList());
    }

    public static boolean isBlank(@Nullable BaseComponent c) {
        return c == null || c instanceof BlankComponent;
    }

    /**
     * See {@link #format(MessageFormat, List)}
     */
    public static BaseComponent[] format(String format, BaseComponent... arguments) {
        return format(new MessageFormat(format), arguments);
    }

    /**
     * See {@link #format(MessageFormat, List)}
     */
    public static BaseComponent[] format(String format, List<BaseComponent> arguments) {
        return format(new MessageFormat(format), arguments);
    }

    /**
     * See {@link #format(MessageFormat, List)}
     */
    public static BaseComponent[] format(MessageFormat format, BaseComponent... arguments) {
        return format(format, Arrays.asList(arguments));
    }

    /**
     * Render the given {@link MessageFormat} to component form, using the given arguments.
     * This is equivalent to {@link MessageFormat#format} but a component tree is generated,
     * instead of a String, and the arguments are included directly in the tree.
     *
     * To accomplish this, the message is first rendered to a String with placeholder arguments.
     * Then {@link MessageFormat#formatToCharacterIterator} is used to figure out where the
     * arguments appear in the result, and the text between them is spliced out and used to
     * build the component tree along with the actual arguments.
     */
    public static BaseComponent[] format(MessageFormat format, List<BaseComponent> arguments) {
        if(arguments == null || arguments.isEmpty()) {
            return new BaseComponent[] { new TextComponent(format.format(null, new StringBuffer(), null).toString()) };
        }

        List<BaseComponent> parts = new ArrayList<>(arguments.size() * 2 + 1);

        Object[] dummies = new Object[arguments.size()];
        StringBuffer sb = format.format(dummies, new StringBuffer(), null);
        AttributedCharacterIterator iter = format.formatToCharacterIterator(dummies);

        while(iter.getIndex() < iter.getEndIndex()) {
            int end = iter.getRunLimit();
            Integer index = (Integer) iter.getAttribute(MessageFormat.Field.ARGUMENT);
            if(index == null) {
                parts.add(new TextComponent(sb.substring(iter.getIndex(), end)));
            } else {
                parts.add(arguments.get(index));
            }
            iter.setIndex(end);
        }

        return parts.toArray(new BaseComponent[parts.size()]);
    }

    /**
     * Recursively compare the given components for equality
     */
    public static boolean equals(HoverEvent a, HoverEvent b) {
        return (a == b) ||
               (a != null && b != null &&
                a.getAction() == b.getAction() &&
                equals(a.getValue(), b.getValue()));
    }

    /**
     * Recursively compare the given components for equality
     */
    public static boolean equals(ClickEvent a, ClickEvent b) {
        return (a == b) ||
               (a != null && b != null &&
                a.getAction() == b.getAction() &&
                Objects.equals(a.getValue(), b.getValue()));
    }

    /**
     * Recursively compare the given components for equality
     */
    public static boolean equals(BaseComponent a, BaseComponent b) {
        return (a == b) ||
               (a != null && b != null &&
                Objects.equals(a.isBoldRaw(), b.isBoldRaw()) &&
                Objects.equals(a.isItalicRaw(), b.isItalicRaw()) &&
                Objects.equals(a.isObfuscatedRaw(), b.isItalicRaw()) &&
                Objects.equals(a.isStrikethroughRaw(), b.isItalicRaw()) &&
                Objects.equals(a.isUnderlinedRaw(), b.isItalicRaw()) &&
                equals(a.getClickEvent(), b.getClickEvent()) &&
                equals(a.getHoverEvent(), b.getHoverEvent()));
    }

    /**
     * Recursively compare the given components for equality
     */
    public static boolean equals(BaseComponent[] a, BaseComponent[] b) {
        if(a == b) return true;
        if(a == null || b == null) return false;
        if(a.length != b.length) return false;

        for(int i = 0; i < a.length; i++) {
            if(!equals(a[i], b[i])) return false;
        }

        return true;
    }

    /**
     * Recursively compare the given components for equality
     */
    public static boolean equals(Collection<BaseComponent> a, Collection<BaseComponent> b) {
        if(a == b) return true;
        if(a == null || b == null) return false;
        if(a.size() != b.size()) return false;

        for(Iterator<BaseComponent> ia = a.iterator(), ib = b.iterator(); ia.hasNext() && ib.hasNext(); ) {
            if(!equals(ia.next(), ib.next())) return false;
        }

        return true;
    }

    public static BaseComponent color(BaseComponent c, @Nullable ChatColor color) {
        c.setColor(color);
        return c;
    }

    public static BaseComponent bold(BaseComponent c, @Nullable Boolean yes) {
        c.setBold(yes);
        return c;
    }

    public static BaseComponent italic(BaseComponent c, @Nullable Boolean yes) {
        c.setItalic(yes);
        return c;
    }

    public static BaseComponent underlined(BaseComponent c, @Nullable Boolean yes) {
        c.setUnderlined(yes);
        return c;
    }

    public static BaseComponent strikethrough(BaseComponent c, @Nullable Boolean yes) {
        c.setStrikethrough(yes);
        return c;
    }

    public static BaseComponent obfuscated(BaseComponent c, @Nullable Boolean yes) {
        c.setObfuscated(yes);
        return c;
    }

    public static BaseComponent clickEvent(BaseComponent c, @Nullable ClickEvent event) {
        c.setClickEvent(event);
        return c;
    }

    public static BaseComponent clickEvent(BaseComponent c, ClickEvent.Action action, String value) {
        c.setClickEvent(new ClickEvent(action, value));
        return c;
    }

    public static BaseComponent hoverEvent(BaseComponent c, @Nullable HoverEvent event) {
        c.setHoverEvent(event);
        return c;
    }

    public static BaseComponent hoverEvent(BaseComponent c, HoverEvent.Action action, BaseComponent...values) {
        c.setHoverEvent(new HoverEvent(action, values));
        return c;
    }

    public static BaseComponent extra(BaseComponent c, BaseComponent...extras) {
        for(BaseComponent extra : extras) {
            checkNotNull(extra);
            c.addExtra(extra);
        }
        return c;
    }

    public static BaseComponent addFormats(BaseComponent component, ChatColor... formats) {
        return addFormats(component, Arrays.asList(formats));
    }

    public static BaseComponent addFormats(BaseComponent component, Iterable<ChatColor> formats) {
        for(ChatColor format : formats) {
            checkNotNull(format);
            switch(format) {
                case BOLD:          component.setBold(true); break;
                case ITALIC:        component.setItalic(true); break;
                case UNDERLINE:     component.setUnderlined(true); break;
                case STRIKETHROUGH: component.setStrikethrough(true); break;
                case MAGIC:         component.setObfuscated(true); break;
                case RESET:         throw new IllegalArgumentException("Cannot add format " + format);
                default:            component.setColor(format); break;
            }
        }
        return component;
    }

    public static BaseComponent removeFormats(BaseComponent component, ChatColor... formats) {
        for(ChatColor format : formats) {
            checkNotNull(format);
            switch(format) {
                case BOLD:          component.setBold(false); break;
                case ITALIC:        component.setItalic(false); break;
                case UNDERLINE:     component.setUnderlined(false); break;
                case STRIKETHROUGH: component.setStrikethrough(false); break;
                case MAGIC:         component.setObfuscated(false); break;
                default:            throw new IllegalArgumentException("Cannot remove format " + format);
            }
        }
        return component;
    }

    public static boolean hasFormat(BaseComponent c) {
        return c.getColorRaw() != null ||
               c.isBoldRaw() != null ||
               c.isItalicRaw() != null ||
               c.isUnderlinedRaw() != null ||
               c.isStrikethroughRaw() != null ||
               c.isObfuscatedRaw() != null ||
               c.getClickEvent() != null ||
               c.getHoverEvent() != null;
    }

    public static void copyFormat(BaseComponent from, BaseComponent to) {
        to.setColor(from.getColorRaw());
        to.setBold(from.isBoldRaw());
        to.setItalic(from.isItalicRaw());
        to.setUnderlined(from.isUnderlinedRaw());
        to.setStrikethrough(from.isStrikethroughRaw());
        to.setObfuscated(from.isObfuscatedRaw());
    }

    public static void copyEvents(BaseComponent from, BaseComponent to) {
        to.setClickEvent(from.getClickEvent());
        to.setHoverEvent(from.getHoverEvent());
    }

    public static void copyFormatAndEvents(BaseComponent from, BaseComponent to) {
        copyFormat(from, to);
        copyEvents(from, to);
    }

    public static void softMergeFormat(BaseComponent from, BaseComponent to) {
        if(to.getColorRaw() == null) to.setColor(from.getColorRaw());
        if(to.isBoldRaw() == null) to.setBold(from.isBoldRaw());
        if(to.isItalicRaw() == null) to.setItalic(from.isItalicRaw());
        if(to.isUnderlinedRaw() == null) to.setUnderlined(from.isUnderlinedRaw());
        if(to.isStrikethroughRaw() == null) to.setStrikethrough(from.isStrikethroughRaw());
        if(to.isObfuscatedRaw() == null) to.setObfuscated(from.isObfuscatedRaw());
        if(to.getClickEvent() == null) to.setClickEvent(from.getClickEvent());
        if(to.getHoverEvent() == null) to.setHoverEvent(from.getHoverEvent());
    }

    public static void hardMergeFormat(BaseComponent from, BaseComponent to) {
        if(from.getColorRaw() != null) to.setColor(from.getColorRaw());
        if(from.isBoldRaw() != null) to.setBold(from.isBoldRaw());
        if(from.isItalicRaw() != null) to.setItalic(from.isItalicRaw());
        if(from.isUnderlinedRaw() != null) to.setUnderlined(from.isUnderlinedRaw());
        if(from.isStrikethroughRaw() != null) to.setStrikethrough(from.isStrikethroughRaw());
        if(from.isObfuscatedRaw() != null) to.setObfuscated(from.isObfuscatedRaw());
        if(from.getClickEvent() != null) to.setClickEvent(from.getClickEvent());
        if(from.getHoverEvent() != null) to.setHoverEvent(from.getHoverEvent());
    }

    public static void copyLastFormat(String legacy, BaseComponent to) {
        int length = legacy.length();

        // Search backwards from the end as it is faster
        for (int index = length - 1; index > -1; index--) {
            char section = legacy.charAt(index);
            if (section == ChatColor.COLOR_CHAR && index < length - 1) {
                char c = legacy.charAt(index + 1);
                ChatColor color = ChatColor.getByChar(c);

                if (color != null) {
                    addFormats(to, color);

                    // Once we find a color or reset we can stop searching
                    if(color.equals(ChatColor.RESET)) break;
                    if((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) break;
                }
            }
        }
    }

    public static void addExtra(BaseComponent c, List<BaseComponent> extra) {
        if(extra != null) {
            if(c.getExtra() == null) {
                c.setExtra(extra);
            } else {
                c.getExtra().addAll(extra);
            }
        }
    }

    public static BaseComponent shallowCopy(BaseComponent original) {
        if(original instanceof TextComponent) {
            return shallowCopy((TextComponent) original);
        } else if(original instanceof TranslatableComponent) {
            return shallowCopy((TranslatableComponent) original);
        } else {
            throw new IllegalArgumentException("Don't know how to copy a " + original.getClass().getName());
        }
    }

    public static TextComponent shallowCopy(TextComponent original) {
        TextComponent copy = new TextComponent(original.getText());
        copyFormatAndEvents(original, copy);
        copy.setExtra(original.getExtra());
        return copy;
    }

    public static TranslatableComponent shallowCopy(TranslatableComponent original) {
        TranslatableComponent copy = new TranslatableComponent(original.getTranslate());
        copy.setWith(original.getWith());
        copyFormatAndEvents(original, copy);
        copy.setExtra(original.getExtra());
        return copy;
    }

    public static BaseComponent concat(BaseComponent... components) {
        switch(components.length) {
            case 0: return blank();
            case 1: return components[0];
            default: return new Component(components);
        }
    }

    public static BaseComponent link(String protocol, String host, String path, ChatColor...formats) {
        try {
            return link(new URL(protocol, host, path), formats);
        } catch(MalformedURLException e) {
            throw new IllegalStateException(e);
        }
    }

    public static BaseComponent link(URL url, ChatColor...formats) {
        try {
            final URI uri = url.toURI();

            // The encoded form escapes all illegal characters e.g. " " becomes "%20",
            // which is required by the client.
            final String encoded = url.toExternalForm();

            // The display form leaves the illegal chars in the path, which tends to look nicer.
            // It also removes any trailing slash.
            final String display = encoded
                .replace(uri.getRawPath(), uri.getPath())
                .replaceAll("/$", "");

            final Component c = new Component(display).clickEvent(ClickEvent.Action.OPEN_URL, encoded);
            if(formats.length == 0) {
                c.color(ChatColor.BLUE).underlined(true);
            } else {
                c.add(formats);
            }
            return c;
        } catch(URISyntaxException e) {
            return blank();
        }
    }

    public static int pixelWidth(Collection<BaseComponent> components, boolean bold) {
        int width = 0;
        for(BaseComponent component : components) {
            width += pixelWidth(component);
        }
        return width;
    }

    public static int pixelWidth(BaseComponent component, boolean bold) {
        if(component.isBoldRaw() != null) {
            bold = component.isBold();
        }
        int width = 0;
        if(component instanceof TextComponent) {
            String text = ((TextComponent) component).getText();
            width += ChatUtils.pixelWidth(text, bold);
        }
        if(component.getExtra() != null) {
            width += pixelWidth(component.getExtra(), bold);
        }
        return width;
    }

    public static int pixelWidth(BaseComponent component) {
        return pixelWidth(component, false);
    }

    public static List<BaseComponent> transform(List<String> strings) {
        return Lists.transform(strings, new Function<String, BaseComponent>() {
            @Override public BaseComponent apply(String s) {
                return new Component(s);
            }
        });
    }

    public static BaseComponent join(BaseComponent delimiter, Collection<? extends BaseComponent> elements) {
        Component c = new Component();
        boolean first = true;
        for(BaseComponent el : elements) {
            if(!first) {
                c.extra(delimiter);
            }
            c.extra(el);
            first = false;
        }
        return c;
    }

    public static List<BaseComponent> wordWrap(BaseComponent text, int width) {
        return wordWrap(new ArrayList<>(), text, width);
    }

    public static List<BaseComponent> wordWrap(List<BaseComponent> lines, BaseComponent text, int width) {
        for(String line : ChatUtils.wordWrap(text.toLegacyText(), width)) {
            lines.add(concat(TextComponent.fromLegacyText(line)));
        }
        return lines;
    }

    public static BaseComponent naturalList(Stream<? extends BaseComponent> elements) {
        return naturalList(elements.collect(Collectors.toList()));
    }

    public static BaseComponent naturalList(Collection<? extends BaseComponent> elements) {
        switch(elements.size()) {
            case 0: return Components.blank();
            case 1: return elements.iterator().next();
            case 2:
                return new TranslatableComponent("misc.list.pair", elements.toArray());
            default:
                Iterator<? extends BaseComponent> iter = elements.iterator();
                BaseComponent a = new TranslatableComponent("misc.list.start", iter.next(), iter.next());
                BaseComponent b = iter.next();
                while(iter.hasNext()) {
                    a = new TranslatableComponent("misc.list.middle", a, b);
                    b = iter.next();
                }
                return new TranslatableComponent("misc.list.end", a, b);
        }
    }

    public static BaseComponent warning(BaseComponent content) {
        return new Component(ChatColor.RED).extra(new Component(" \u26a0 ", ChatColor.YELLOW), content);
    }
}
