package cn.citycraft.TellRaw;

import static cn.citycraft.TellRaw.common.TextualComponent.rawText;

import java.io.IOException;
import java.io.StringWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;

import org.bukkit.Achievement;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Material;
import org.bukkit.Statistic;
import org.bukkit.command.CommandSender;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
import org.bukkit.configuration.serialization.ConfigurationSerialization;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.Plugin;

import com.comphenix.protocol.utility.MinecraftReflection;

import cn.citycraft.GsonAgent.GsonAgent;
import cn.citycraft.GsonAgent.api.JsonArray;
import cn.citycraft.GsonAgent.api.JsonElement;
import cn.citycraft.GsonAgent.api.JsonObject;
import cn.citycraft.GsonAgent.api.JsonParser;
import cn.citycraft.GsonAgent.api.stream.JsonWriter;
import cn.citycraft.TellRaw.common.ArrayWrapper;
import cn.citycraft.TellRaw.common.JsonRepresentedObject;
import cn.citycraft.TellRaw.common.JsonString;
import cn.citycraft.TellRaw.common.MessagePart;
import cn.citycraft.TellRaw.common.Reflection;
import cn.citycraft.TellRaw.common.TextualComponent;
import cn.citycraft.TellRaw.internal.FancyMessageInternal;
import cn.citycraft.TellRaw.manual.FancyMessageManual;

/**
 * Represents a formattable message. Such messages can use elements such as
 * colors, formatting codes, hover and click data, and other features provided
 * by the vanilla Minecraft <a
 * href="http://minecraft.gamepedia.com/Tellraw#Raw_JSON_Text">JSON message
 * formatter</a>.
 * This class allows plugins to emulate the functionality of the vanilla
 * Minecraft <a href="http://minecraft.gamepedia.com/Commands#tellraw">tellraw
 * command</a>.
 * <p>
 * This class follows the builder pattern, allowing for method chaining. It is
 * set up such that invocations of property-setting methods will affect the
 * current editing component, and a call to {@link #then()} or
 * {@link #then(Object)} will append a new editing component to the end of the
 * message, optionally initializing it with text. Further property-setting
 * method calls will affect that editing component.
 * </p>
 */
public abstract class FancyMessage implements JsonRepresentedObject, Cloneable, Iterable<MessagePart>, ConfigurationSerializable {

    private static JsonParser _stringParser;

    private static Class<? extends FancyMessage> internalClass;

    protected static Class<?> chatSerializerClazz;
    protected static Method fromJsonMethod;
    protected static Method getNMSAchievementMethod = null;

    protected static Method getNMSEntityStatisticMethod = null;
    protected static Method getNMSMaterialStatisticMethod = null;
    protected static Method getNMSSaveNBTMethod = null;
    protected static Method getNMSStatisticMethod = null;

    protected static Method getOBCasNMSCopyMethod = null;

    protected static Field nmsAchievement_NameField = null;

    protected static Class<?> nmsAchievementClass = null;

    protected static Object nmsChatSerializerGsonInstance;

    protected static Class<?> nmsIChatBaseComponentClass;
    protected static Class<?> nmsItemStack = null;

    protected static Class<?> nmsNBTTagCompound = null;
    protected static Class<?> nmsPacketClass;
    protected static Constructor<?> nmsPacketPlayOutChatConstructor;

    protected static Field nmsPlayerConnectionField = null;
    protected static Method nmsSendPacketMethod = null;

    protected static Field nmsStatistic_NameField = null;

    protected static Class<?> nmsStatisticClass = null;
    protected static Class<?> obcCraftItemStack = null;
    protected static Class<?> obcCraftStatisticClass = null;

    static {
        ConfigurationSerialization.registerClass(FancyMessage.class);
        if (PluginHelperConfig.getConfig().getBoolean("TellrawManualHandle", false)) {
            internalClass = FancyMessageManual.class;
            Bukkit.getLogger().info("[TellRawLib] 指定使用手动序列化类处理...");
        } else {
            boolean useProtocolLib = false;
            final Plugin plugin = Bukkit.getPluginManager().getPlugin("ProtocolLib");
            if (plugin != null) {
                if (!plugin.isEnabled()) {
                    Bukkit.getPluginManager().enablePlugin(plugin);
                }
                if (plugin.isEnabled()) {
                    useProtocolLib = true;
                }
            }
            try {
                _stringParser = GsonAgent.newJsonParser();
                try {
                    nmsPacketPlayOutChatConstructor = com.comphenix.protocol.PacketType.Play.Server.CHAT.getPacketClass().getDeclaredConstructor(MinecraftReflection.getIChatBaseComponentClass());
                } catch (final Exception | Error e) {
                    nmsPacketPlayOutChatConstructor = Reflection.getNMSClass("PacketPlayOutChat").getDeclaredConstructor(Reflection.getNMSClass("IChatBaseComponent"));
                }
                nmsPacketPlayOutChatConstructor.setAccessible(true);
                try {
                    obcCraftStatisticClass = MinecraftReflection.getCraftBukkitClass("CraftStatistic");
                    getNMSStatisticMethod = Reflection.getMethod(obcCraftStatisticClass, "getNMSStatistic", Statistic.class);
                    nmsStatisticClass = MinecraftReflection.getMinecraftClass("Statistic");
                    try {
                        nmsStatistic_NameField = Reflection.getField(nmsStatisticClass, "name");
                    } catch (final Exception e) {
                        nmsStatistic_NameField = Reflection.getDeclaredFieldByType(nmsStatisticClass, String.class).get(0);
                    }
                    getNMSMaterialStatisticMethod = Reflection.getMethod(obcCraftStatisticClass, "getMaterialStatistic", Statistic.class, Material.class);
                    getNMSEntityStatisticMethod = Reflection.getMethod(obcCraftStatisticClass, "getEntityStatistic", Statistic.class, EntityType.class);
                    nmsAchievementClass = MinecraftReflection.getMinecraftClass("Achievement");
                    getNMSAchievementMethod = Reflection.getMethod(obcCraftStatisticClass, "getNMSAchievement", Achievement.class);
                    try {
                        nmsAchievement_NameField = Reflection.getField(nmsAchievementClass, "name");
                    } catch (final Exception e) {
                        nmsAchievement_NameField = Reflection.getDeclaredFieldByType(nmsAchievementClass, String.class).get(0);
                    }
                    obcCraftItemStack = MinecraftReflection.getCraftItemStackClass();
                    getOBCasNMSCopyMethod = Reflection.getMethod(obcCraftItemStack, "asNMSCopy", ItemStack.class);
                    nmsItemStack = MinecraftReflection.getItemStackClass();
                    nmsNBTTagCompound = MinecraftReflection.getNBTCompoundClass();
                    try {
                        getNMSSaveNBTMethod = Reflection.getMethod(nmsItemStack, "save", nmsNBTTagCompound);
                    } catch (final Exception e) {
                        getNMSSaveNBTMethod = Reflection.getMethodByParamsAndType(nmsItemStack, nmsNBTTagCompound, nmsNBTTagCompound).get(0);
                    }
                    nmsPacketClass = MinecraftReflection.getPacketClass();
                    chatSerializerClazz = MinecraftReflection.getChatSerializerClass();
                    nmsIChatBaseComponentClass = MinecraftReflection.getIChatBaseComponentClass();
                } catch (final Exception | Error ex) {
                    try {
                        obcCraftStatisticClass = Reflection.getOBCClass("CraftStatistic");
                        getNMSStatisticMethod = Reflection.getMethod(obcCraftStatisticClass, "getNMSStatistic", Statistic.class);
                        nmsStatisticClass = Reflection.getNMSClass("Statistic");
                        try {
                            nmsStatistic_NameField = Reflection.getField(nmsStatisticClass, "name");
                        } catch (final Exception e) {
                            nmsStatistic_NameField = Reflection.getDeclaredFieldByType(nmsStatisticClass, String.class).get(0);
                        }
                        getNMSMaterialStatisticMethod = Reflection.getMethod(obcCraftStatisticClass, "getMaterialStatistic", Statistic.class, Material.class);
                        getNMSEntityStatisticMethod = Reflection.getMethod(obcCraftStatisticClass, "getEntityStatistic", Statistic.class, EntityType.class);
                        nmsAchievementClass = Reflection.getNMSClass("Achievement");
                        getNMSAchievementMethod = Reflection.getMethod(obcCraftStatisticClass, "getNMSAchievement", Achievement.class);
                        try {
                            nmsAchievement_NameField = Reflection.getField(nmsAchievementClass, "name");
                        } catch (final Exception e) {
                            nmsAchievement_NameField = Reflection.getDeclaredFieldByType(nmsAchievementClass, String.class).get(0);
                        }
                        obcCraftItemStack = Reflection.getOBCClass("inventory.CraftItemStack");
                        getOBCasNMSCopyMethod = Reflection.getMethod(obcCraftItemStack, "asNMSCopy", ItemStack.class);
                        nmsItemStack = Reflection.getNMSClass("ItemStack");
                        nmsNBTTagCompound = Reflection.getNMSClass("NBTTagCompound");
                        try {
                            getNMSSaveNBTMethod = Reflection.getMethod(nmsItemStack, "save", nmsNBTTagCompound);
                        } catch (final Exception e) {
                            getNMSSaveNBTMethod = Reflection.getMethodByParamsAndType(nmsItemStack, nmsNBTTagCompound, nmsNBTTagCompound).get(0);
                        }
                        nmsPacketClass = Reflection.getNMSClass("Packet");
                        try {
                            chatSerializerClazz = Reflection.getNMSClass("IChatBaseComponent$ChatSerializer");
                        } catch (final Exception e) {
                            chatSerializerClazz = Reflection.getNMSClass("ChatSerializer");
                        }
                        nmsIChatBaseComponentClass = Reflection.getNMSClass("IChatBaseComponent");
                    } catch (final Exception e) {
                        throw e;
                    }
                }
                if (nmsChatSerializerGsonInstance == null) {
                    // Find the field and its value, completely bypassing obfuscation
                    if (chatSerializerClazz == null) {
                        throw new ClassNotFoundException("Can't find the ChatSerializer class");
                    }
                    for (final Field declaredField : chatSerializerClazz.getDeclaredFields()) {
                        if (Modifier.isFinal(declaredField.getModifiers()) && Modifier.isStatic(declaredField.getModifiers()) && declaredField.getType().getName().endsWith("Gson")) {
                            // We've found our field
                            declaredField.setAccessible(true);
                            nmsChatSerializerGsonInstance = declaredField.get(null);
                            fromJsonMethod = nmsChatSerializerGsonInstance.getClass().getMethod("fromJson", String.class, Class.class);
                            break;
                        }
                    }
                }
                if (getNMSSaveNBTMethod == null || nmsNBTTagCompound == null) {
                    throw new ClassNotFoundException("Can't find the NBTMethod or NBTTagCompound class");
                }
                internalClass = FancyMessageInternal.class;
                if (!useProtocolLib) {
                    Bukkit.getLogger().info("[TellRawLib] 使用ChatSerializer序列化类处理...");
                } else {
                    Bukkit.getLogger().info("[TellRawLib] 使用ProtocolLib序列化类处理...");
                }
            } catch (final Exception | Error e) {
                internalClass = FancyMessageManual.class;
                Bukkit.getLogger().info("[TellRawLib] 使用GItemStack序列化类处理...");
            }
        }
    }

    private boolean dirty;

    private String jsonString;

    protected List<MessagePart> messageParts;

    public FancyMessage(final TextualComponent firstPartText) {
        messageParts = new ArrayList<MessagePart>();
        messageParts.add(new MessagePart(firstPartText));
    }

    /**
     * Deserializes a JSON-represented message from a mapping of key-value
     * pairs.
     * This is called by the Bukkit serialization API.
     * It is not intended for direct public API consumption.
     *
     * @param serialized
     *            The key-value mapping which represents a fancy message.
     */
    @SuppressWarnings("unchecked")
    public static FancyMessage deserialize(final Map<String, Object> serialized) {
        final FancyMessage msg = newFM();
        msg.messageParts = (List<MessagePart>) serialized.get("messageParts");
        msg.jsonString = serialized.containsKey("JSON") ? serialized.get("JSON").toString() : null;
        msg.dirty = !serialized.containsKey("JSON");
        return msg;
    }

    /**
     * Deserializes a fancy message from its JSON representation. This JSON
     * representation is of the format of
     * that returned by {@link #toJSONString()}, and is compatible with vanilla
     * inputs.
     *
     * @param json
     *            The JSON string which represents a fancy message.
     * @return A {@code FancyMessage} representing the parameterized JSON
     *         message.
     */
    public static FancyMessage deserialize(final String json) {
        final JsonObject serialized = _stringParser.parse(json).getAsJsonObject();
        final JsonArray extra = serialized.getAsJsonArray("extra"); // Get the extra
        // component
        final FancyMessage returnVal = newFM();
        returnVal.messageParts.clear();
        for (final JsonElement mPrt : extra) {
            final MessagePart component = new MessagePart();
            final JsonObject messagePart = mPrt.getAsJsonObject();
            for (final Map.Entry<String, JsonElement> entry : messagePart.entrySet()) {
                // Deserialize text
                if (TextualComponent.isTextKey(entry.getKey())) {
                    // The map mimics the YAML serialization, which has a "key" field and one or more "value" fields
                    final Map<String, Object> serializedMapForm = new HashMap<String, Object>(); // Must be object due to Bukkit serializer API compliance
                    serializedMapForm.put("key", entry.getKey());
                    if (entry.getValue().isJsonPrimitive()) {
                        // Assume string
                        serializedMapForm.put("value", entry.getValue().getAsString());
                    } else {
                        // Composite object, but we assume each element is a string
                        for (final Map.Entry<String, JsonElement> compositeNestedElement : entry.getValue().getAsJsonObject().entrySet()) {
                            serializedMapForm.put("value." + compositeNestedElement.getKey(), compositeNestedElement.getValue().getAsString());
                        }
                    }
                    component.text = TextualComponent.deserialize(serializedMapForm);
                } else if (MessagePart.stylesToNames.inverse().containsKey(entry.getKey())) {
                    if (entry.getValue().getAsBoolean()) {
                        component.styles.add(MessagePart.stylesToNames.inverse().get(entry.getKey()));
                    }
                } else if (entry.getKey().equals("color")) {
                    component.color = ChatColor.valueOf(entry.getValue().getAsString().toUpperCase());
                } else if (entry.getKey().equals("clickEvent")) {
                    final JsonObject object = entry.getValue().getAsJsonObject();
                    component.clickActionName = object.get("action").getAsString();
                    component.clickActionData = object.get("value").getAsString();
                } else if (entry.getKey().equals("hoverEvent")) {
                    final JsonObject object = entry.getValue().getAsJsonObject();
                    component.hoverActionName = object.get("action").getAsString();
                    if (object.get("value").isJsonPrimitive()) {
                        // Assume string
                        component.hoverActionData = new JsonString(object.get("value").getAsString());
                    } else {
                        // Assume composite type
                        component.hoverActionData = deserialize(object.get("value").toString());
                    }
                } else if (entry.getKey().equals("insertion")) {
                    component.insertionData = entry.getValue().getAsString();
                } else if (entry.getKey().equals("with")) {
                    for (final JsonElement object : entry.getValue().getAsJsonArray()) {
                        if (object.isJsonPrimitive()) {
                            component.translationReplacements.add(new JsonString(object.getAsString()));
                        } else {
                            component.translationReplacements.add(deserialize(object.toString()));
                        }
                    }
                }
            }
            returnVal.messageParts.add(component);
        }
        return returnVal;
    }

    /**
     * 获得消息工厂类
     *
     * @return 消息工厂
     */
    public static FancyMessage newFM() {
        try {
            return FancyMessage.internalClass.newInstance();
        } catch (final Exception e) {
            return new FancyMessageManual();
        }
    }

    /**
     * 获得消息工厂类
     *
     * @param firstPartText
     *            首字符串
     * @return 消息工厂
     */
    public static FancyMessage newFM(final String firstPartText) {
        return newFM().text(firstPartText);
    }

    /**
     * 在客户端显示一个成就.
     * </p>
     * 当前方法将不会继承本类的颜色样式等参数.
     * </p>
     *
     * @param which
     *            需要显示的成就
     *
     * @return {@link FancyMessage}
     */
    public abstract FancyMessage achievementTooltip(final Achievement which);

    /**
     * 在客户端显示一个成就.
     * </p>
     * 当前方法将不会继承本类的颜色样式等参数.
     * </p>
     *
     * @param name
     *            需要显示的成就名称
     *
     * @return {@link FancyMessage}
     */
    public FancyMessage achievementTooltip(final String name) {
        onHover("show_achievement", new JsonString("achievement." + name));
        return this;
    }

    @Override
    public FancyMessage clone() throws CloneNotSupportedException {
        final FancyMessage instance = (FancyMessage) super.clone();
        instance.messageParts = new ArrayList<MessagePart>(messageParts.size());
        for (int i = 0; i < messageParts.size(); i++) {
            instance.messageParts.add(i, messageParts.get(i).clone());
        }
        instance.dirty = false;
        instance.jsonString = null;
        return instance;
    }

    /**
     * 设置当前被编辑串的
     *
     * @param color
     *            当前串的颜色
     * @return {@link FancyMessage}
     * @exception IllegalArgumentException
     *                如果参数不是一个 {@link ChatColor} 的颜色值 (是一个样式字符).
     */
    public FancyMessage color(final ChatColor color) {
        if (!color.isColor()) {
            throw new IllegalArgumentException(color.name() + "不是一个颜色");
        }
        latest().color = color;
        dirty = true;
        return this;
    }

    /**
     * 服务器将特殊的字符串发送给客户端.
     * </p>
     * 当玩家点击时 客户端 <b>将会</b> 立即将命令发送给服务器.
     * </p>
     * 命令将绑定在当前编辑的文本上
     * </p>
     *
     * @param command
     *            点击执行命令
     * @return {@link FancyMessage}
     */
    public FancyMessage command(final String command) {
        onClick("run_command", command);
        return this;
    }

    /**
     * Set the behavior of the current editing component to instruct the client
     * to open a file on the client side filesystem when the currently edited
     * part of the {@code FancyMessage} is clicked.
     *
     * @param path
     *            The path of the file on the client filesystem.
     * @return {@link FancyMessage}
     */
    public FancyMessage file(final String path) {
        onClick("open_file", path);
        return this;
    }

    /**
     * Set the behavior of the current editing component to display formatted
     * text when the client hovers over the text.
     * <p>
     * Tooltips do not inherit display characteristics, such as color and
     * styles, from the message component on which they are applied.
     * </p>
     *
     * @param text
     *            The formatted text which will be displayed to the client upon
     *            hovering.
     * @return {@link FancyMessage}
     */
    public FancyMessage formattedTooltip(final FancyMessage text) {
        for (final MessagePart component : text.messageParts) {
            if (component.clickActionData != null && component.clickActionName != null) {
                throw new IllegalArgumentException("The tooltip text cannot have click data.");
            } else if (component.hoverActionData != null && component.hoverActionName != null) {
                throw new IllegalArgumentException("The tooltip text cannot have a tooltip.");
            }
        }
        onHover("show_text", text);
        return this;
    }

    /**
     * Set the behavior of the current editing component to display the
     * specified lines of formatted text when the client hovers over the text.
     * <p>
     * Tooltips do not inherit display characteristics, such as color and
     * styles, from the message component on which they are applied.
     * </p>
     *
     * @param lines
     *            The lines of formatted text which will be displayed to the
     *            client upon hovering.
     * @return {@link FancyMessage}
     */
    public FancyMessage formattedTooltip(final FancyMessage... lines) {
        if (lines.length < 1) {
            onHover(null, null); // Clear tooltip
            return this;
        }

        final FancyMessage result = newFM();
        result.messageParts.clear(); // Remove the one existing text component
                                     // that exists by default, which
                                     // destabilizes the object

        for (int i = 0; i < lines.length; i++) {
            try {
                for (final MessagePart component : lines[i]) {
                    if (component.clickActionData != null && component.clickActionName != null) {
                        throw new IllegalArgumentException("The tooltip text cannot have click data.");
                    } else if (component.hoverActionData != null && component.hoverActionName != null) {
                        throw new IllegalArgumentException("The tooltip text cannot have a tooltip.");
                    }
                    if (component.hasText()) {
                        result.messageParts.add(component.clone());
                    }
                }
                if (i != lines.length - 1) {
                    result.messageParts.add(new MessagePart(rawText("\n")));
                }
            } catch (final CloneNotSupportedException e) {
                Bukkit.getLogger().log(Level.WARNING, "Failed to clone object", e);
                return this;
            }
        }
        return formattedTooltip(result.messageParts.isEmpty() ? null : result);
    }

    /**
     * Set the behavior of the current editing component to display the
     * specified lines of formatted text when the client hovers over the text.
     * <p>
     * Tooltips do not inherit display characteristics, such as color and
     * styles, from the message component on which they are applied.
     * </p>
     *
     * @param lines
     *            The lines of text which will be displayed to the client upon
     *            hovering. The iteration order of this object will be the order
     *            in which the lines of the tooltip are created.
     * @return {@link FancyMessage}
     */
    public FancyMessage formattedTooltip(final Iterable<FancyMessage> lines) {
        return formattedTooltip(ArrayWrapper.toArray(lines, FancyMessage.class));
    }

    /**
     * Set the behavior of the current editing component to instruct the client
     * to append the chat input box content with the specified string when the
     * currently edited part of the {@code FancyMessage} is SHIFT-CLICKED.
     * The client will not immediately send the command to the server to be
     * executed unless the client player submits the command/chat message,
     * usually with the enter key.
     *
     * @param command
     *            The text to append to the chat bar of the client.
     * @return {@link FancyMessage}
     */
    public FancyMessage insert(final String command) {
        latest().insertionData = command;
        dirty = true;
        return this;
    }

    /**
     * Set the behavior of the current editing component to display information
     * about an item when the client hovers over the text.
     * <p>
     * Tooltips do not inherit display characteristics, such as color and
     * styles, from the message component on which they are applied.
     * </p>
     *
     * @param itemStack
     *            The stack for which to display information.
     * @return {@link FancyMessage}
     */
    public abstract FancyMessage itemTooltip(final ItemStack itemStack);

    /**
     * Set the behavior of the current editing component to display information
     * about an item when the client hovers over the text.
     * <p>
     * Tooltips do not inherit display characteristics, such as color and
     * styles, from the message component on which they are applied.
     * </p>
     *
     * @param itemJSON
     *            A string representing the JSON-serialized NBT data tag of an
     *            {@link ItemStack}.
     * @return {@link FancyMessage}
     */
    public FancyMessage itemTooltip(final String itemJSON) {
        onHover("show_item", new JsonString(itemJSON));
        return this;
    }

    /**
     * <b>Internally called method. Not for API consumption.</b>
     */
    @Override
    public Iterator<MessagePart> iterator() {
        return messageParts.iterator();
    }

    /**
     * Set the behavior of the current editing component to instruct the client
     * to open a webpage in the client's web browser when the currently edited
     * part of the {@code FancyMessage} is clicked.
     *
     * @param url
     *            The URL of the page to open when the link is clicked.
     * @return {@link FancyMessage}
     */
    public FancyMessage link(final String url) {
        onClick("open_url", url);
        return this;
    }

    /**
     * Sends this message to a command sender.
     * If the sender is a player, they will receive the fully-fledged formatted
     * display of this message.
     * Otherwise, they will receive a version of this message with less
     * formatting.
     *
     * @param sender
     *            The command sender who will receive the message.
     * @see #toOldMessageFormat()
     */
    public void send(final CommandSender sender) {
        send(sender, toJSONString());
    }

    public abstract void send(final CommandSender sender, final String jsonString);

    /**
     * Sends this message to multiple command senders.
     *
     * @param senders
     *            The command senders who will receive the message.
     * @see #send(CommandSender)
     */
    public void send(final Iterable<? extends CommandSender> senders) {
        final String string = toJSONString();
        for (final CommandSender sender : senders) {
            send(sender, string);
        }
    }

    /**
     * Sends this message to a player. The player will receive the fully-fledged
     * formatted display of this message.
     *
     * @param player
     *            The player who will receive the message.
     */
    public void send(final Player player) {
        send(player, toJSONString());
    }

    @Override
    public Map<String, Object> serialize() {
        final HashMap<String, Object> map = new HashMap<String, Object>();
        map.put("messageParts", messageParts);
        // map.put("JSON", toJSONString());
        return map;
    }

    /**
     * Set the behavior of the current editing component to display information
     * about a parameterless statistic when the client hovers over the text.
     * <p>
     * Tooltips do not inherit display characteristics, such as color and
     * styles, from the message component on which they are applied.
     * </p>
     *
     * @param which
     *            The statistic to display.
     * @return {@link FancyMessage}
     * @exception IllegalArgumentException
     *                If the statistic requires a parameter which was not
     *                supplied.
     */
    public abstract FancyMessage statisticTooltip(final Statistic which);

    /**
     * Set the behavior of the current editing component to display information
     * about a statistic parameter with an entity type when the client hovers
     * over the text.
     * <p>
     * Tooltips do not inherit display characteristics, such as color and
     * styles, from the message component on which they are applied.
     * </p>
     *
     * @param which
     *            The statistic to display.
     * @param entity
     *            The sole entity type parameter to the statistic.
     * @return {@link FancyMessage}
     * @exception IllegalArgumentException
     *                If the statistic requires a parameter which was not
     *                supplied, or was supplied a parameter that was not
     *                required.
     */
    public abstract FancyMessage statisticTooltip(final Statistic which, final EntityType entity);

    /**
     * Set the behavior of the current editing component to display information
     * about a statistic parameter with a material when the client hovers over
     * the text.
     * <p>
     * Tooltips do not inherit display characteristics, such as color and
     * styles, from the message component on which they are applied.
     * </p>
     *
     * @param which
     *            The statistic to display.
     * @param item
     *            The sole material parameter to the statistic.
     * @return {@link FancyMessage}
     * @exception IllegalArgumentException
     *                If the statistic requires a parameter which was not
     *                supplied, or was supplied a parameter that was not
     *                required.
     */
    public abstract FancyMessage statisticTooltip(final Statistic which, final Material item);

    /**
     * Sets the stylization of the current editing component.
     *
     * @param styles
     *            The array of styles to apply to the editing component.
     * @return {@link FancyMessage}
     * @exception IllegalArgumentException
     *                If any of the enumeration values in the array do not
     *                represent formatters.
     */
    public FancyMessage style(final ChatColor... styles) {
        for (final ChatColor style : styles) {
            if (!style.isFormat()) {
                throw new IllegalArgumentException(style.name() + " is not a style");
            }
        }
        latest().styles.addAll(Arrays.asList(styles));
        dirty = true;
        return this;
    }

    /**
     * Set the behavior of the current editing component to instruct the client
     * to replace the chat input box content with the specified string when the
     * currently edited part of the {@code FancyMessage} is clicked.
     * The client will not immediately send the command to the server to be
     * executed unless the client player submits the command/chat message,
     * usually with the enter key.
     *
     * @param command
     *            The text to display in the chat bar of the client.
     * @return {@link FancyMessage}
     */
    public FancyMessage suggest(final String command) {
        onClick("suggest_command", command);
        return this;
    }

    /**
     * 设置当前消息串的文本
     *
     * @param text
     *            需要设置的文本
     * @return {@link FancyMessage}
     */
    public FancyMessage text(final String text) {
        final MessagePart latest = latest();
        latest.text = rawText(text);
        dirty = true;
        return this;
    }

    /**
     * 设置当前消息串的文本
     *
     * @param text
     *            需要设置的文本
     * @return {@link FancyMessage}
     */
    public FancyMessage text(final TextualComponent text) {
        final MessagePart latest = latest();
        latest.text = text;
        dirty = true;
        return this;
    }

    /**
     * 停止编辑当前的消息串 并且开始一个新的消息串
     * </p>
     * 之后的操作将会应用于新的消息串上
     *
     * @return {@link FancyMessage}
     */
    public FancyMessage then() {
        if (!latest().hasText()) {
            throw new IllegalStateException("previous message part has no text");
        }
        messageParts.add(new MessagePart());
        dirty = true;
        return this;
    }

    /**
     * 停止编辑当前的消息串 并且开始一个新的消息串
     * </p>
     * 之后的操作将会应用于新的消息串上
     *
     * @param text
     *            一个新的操作串需要的文本.
     * @return {@link FancyMessage}
     */
    public FancyMessage then(final String text) {
        return then(rawText(text));
    }

    /**
     * 停止编辑当前的消息串 并且开始一个新的消息串
     * </p>
     * 之后的操作将会应用于新的消息串上
     *
     * @param text
     *            一个新的操作串.
     * @return {@link FancyMessage}
     */
    public FancyMessage then(final TextualComponent text) {
        if (!latest().hasText()) {
            throw new IllegalStateException("previous message part has no text");
        }
        messageParts.add(new MessagePart(text));
        dirty = true;
        return this;
    }

    /**
     * 序列化当前对象, 使用 {@link JsonWriter} 转换为一个有效的Json串
     * 当前Json串可用于类似 {@code /tellraw} 的命令上
     *
     * @return 返回这个对象的Json序列化字符串.
     */
    public String toJSONString() {
        if (!dirty && jsonString != null) {
            return jsonString;
        }
        final StringWriter string = new StringWriter();
        final JsonWriter json = GsonAgent.newJsonWriter(string);
        try {
            writeJson(json);
            json.close();
        } catch (final IOException e) {
            throw new RuntimeException("invalid message");
        }
        jsonString = string.toString();
        dirty = false;
        return jsonString;
    }

    /**
     * 将此消息转换为具有有限格式的人可读字符串。
     * 此方法用于发送此消息给没有JSON格式支持客户端。
     * <p>
     * 序列化每个消息部分（每个部分都需要分别序列化）：
     * <ol>
     * <li>消息串的颜色.</li>
     * <li>消息串的样式.</li>
     * <li>消息串的文本.</li>
     * </ol>
     * 这个方法会丢失点击操作和悬浮操作 所以仅用于最后的手段
     * </p>
     * <p>
     * 颜色和格式可以从返回的字符串中删除 通过{@link ChatColor#stripColor(String)}.
     * </p>
     *
     * @return 发送给老版本客户端以及控制台。
     */
    public String toOldMessageFormat() {
        final StringBuilder result = new StringBuilder();
        for (final MessagePart part : this) {
            result.append(part.color == null ? "" : part.color);
            for (final ChatColor formatSpecifier : part.styles) {
                result.append(formatSpecifier);
            }
            result.append(part.text);
        }
        return result.toString();
    }

    /**
     * 设置当前的操作串 当鼠标悬浮时将会显示文本
     * </p>
     * 当前方法将不会继承本类的颜色样式等参数.
     * </p>
     *
     * @param lines
     *            当鼠标悬浮时他将会显示 支持换行符
     * @return {@link FancyMessage}
     */
    public FancyMessage tooltip(final Iterable<String> lines) {
        tooltip(ArrayWrapper.toArray(lines, String.class));
        return this;
    }

    /**
     * 设置当前的操作串 当鼠标悬浮时将会显示文本
     * </p>
     * 当前方法将不会继承本类的颜色样式等参数.
     * </p>
     *
     * @param lines
     *            当鼠标悬浮时他将会显示 支持换行符
     * @return {@link FancyMessage}
     */
    public FancyMessage tooltip(final String text) {
        onHover("show_text", new JsonString(text));
        return this;
    }

    /**
     * 设置当前的操作串 当鼠标悬浮时将会显示文本
     * </p>
     * 当前方法将不会继承本类的颜色样式等参数.
     * </p>
     *
     * @param lines
     *            当鼠标悬浮时他将会显示 支持换行符
     * @return {@link FancyMessage}
     */
    public FancyMessage tooltip(final String... lines) {
        final StringBuilder builder = new StringBuilder();
        for (int i = 0; i < lines.length; i++) {
            builder.append(lines[i]);
            if (i != lines.length - 1) {
                builder.append('\n');
            }
        }
        tooltip(builder.toString());
        return this;
    }

    /**
     * If the text is a translatable key, and it has replaceable values, this
     * function can be used to set the replacements that will be used in the
     * message.
     *
     * @param replacements
     *            The replacements, in order, that will be used in the
     *            language-specific message.
     * @return {@link FancyMessage}
     */
    public FancyMessage translationReplacements(final FancyMessage... replacements) {
        for (final FancyMessage str : replacements) {
            latest().translationReplacements.add(str);
        }

        dirty = true;

        return this;
    }

    /**
     * If the text is a translatable key, and it has replaceable values, this
     * function can be used to set the replacements that will be used in the
     * message.
     *
     * @param replacements
     *            The replacements, in order, that will be used in the
     *            language-specific message.
     * @return {@link FancyMessage}
     */
    public FancyMessage translationReplacements(final Iterable<FancyMessage> replacements) {
        return translationReplacements(ArrayWrapper.toArray(replacements, FancyMessage.class));
    }

    /**
     * If the text is a translatable key, and it has replaceable values, this
     * function can be used to set the replacements that will be used in the
     * message.
     *
     * @param replacements
     *            The replacements, in order, that will be used in the
     *            language-specific message.
     * @return {@link FancyMessage}
     */
    public FancyMessage translationReplacements(final String... replacements) {
        for (final String str : replacements) {
            latest().translationReplacements.add(new JsonString(str));
        }
        dirty = true;
        return this;
    }

    @Override
    public void writeJson(final JsonWriter writer) throws IOException {
        if (messageParts.size() == 1) {
            latest().writeJson(writer);
        } else {
            writer.beginObject().name("text").value("").name("extra").beginArray();
            for (final MessagePart part : this) {
                part.writeJson(writer);
            }
            writer.endArray().endObject();
        }
    }

    /**
     * 获得最后一个操作串
     *
     * @return 最后一个操作的消息串
     */
    private MessagePart latest() {
        return messageParts.get(messageParts.size() - 1);
    }

    /**
     * 添加点击操作
     *
     * @param name
     *            点击名称
     * @param data
     *            点击操作
     */
    private void onClick(final String name, final String data) {
        final MessagePart latest = latest();
        latest.clickActionName = name;
        latest.clickActionData = data;
        dirty = true;
    }

    /**
     * 添加显示操作
     *
     * @param name
     *            悬浮显示
     * @param data
     *            显示内容
     */
    private void onHover(final String name, final JsonRepresentedObject data) {
        final MessagePart latest = latest();
        latest.hoverActionName = name;
        latest.hoverActionData = data;
        dirty = true;
    }
}
