package tc.oc.commons.core.chat;

import com.google.common.base.Strings;
import net.md_5.bungee.api.ChatColor;

import java.util.ArrayList;
import java.util.List;

public class ChatUtils {

    public static final int MAX_CHAT_WIDTH = 300;

    /**
     * Get the pixel width of the given character in the Minecraft font, excluding
     * the space between characters and drop shadow. Handles alphanumerics and most
     * common english punctuation.
     */
    public static int pixelWidth(char c) {
        if(Character.isUpperCase(c)) {
            return c == 'I' ? 3 : 5;
        } else if(Character.isDigit(c)) {
            return 5;
        } else if(Character.isLowerCase(c)) {
            switch(c) {
                case 'i':
                    return 1;

                case 'l':
                    return 2;

                case 't':
                    return 3;

                case 'f':
                case 'k':
                    return 4;

                default:
                    return 5;
            }
        } else {
            switch(c) {
                case '!':
                case '.':
                case ',':
                case ';':
                case ':':
                case '|':
                    return 1;

                case '\'':
                    return 2;

                case '[':
                case ']':
                case ' ':
                    return 3;

                case '*':
                case '(':
                case ')':
                case '{':
                case '}':
                case '<':
                case '>':
                    return 4;

                case '@':
                    return 6;

                default:
                    return 5;
            }
        }
    }

    /**
     * A complete text formatting state, used by Parser
     */
    private static class Format {
        public boolean obfuscated = false;
        public boolean bold = false;
        public boolean strikethrough = false;
        public boolean underline = false;
        public boolean italic = false;
        public ChatColor color;

        private Format(boolean obfuscated,
                       boolean bold,
                       boolean strikethrough,
                       boolean underline,
                       boolean italic,
                       ChatColor color) {
            this.obfuscated = obfuscated;
            this.bold = bold;
            this.strikethrough = strikethrough;
            this.underline = underline;
            this.italic = italic;
            this.color = color;
        }

        private Format(Format format) {
            this(format.obfuscated,
                 format.bold,
                 format.strikethrough,
                 format.underline,
                 format.italic,
                 format.color);
        }

        private Format() {
        }

        @Override
        public String toString() {
            String str = "";

            if(this.color != null) { str += this.color; }
            if(this.obfuscated) { str += ChatColor.MAGIC; }
            if(this.bold) { str += ChatColor.BOLD; }
            if(this.strikethrough) { str += ChatColor.STRIKETHROUGH; }
            if(this.underline) { str += ChatColor.UNDERLINE; }
            if(this.italic) { str += ChatColor.ITALIC; }

            return str;
        }
    }

    /**
     * Holds the state while parsing formatted text, while calculating
     * the pixel width at various points.
     */
    private static class Parser {
        public final String text;

        public boolean formatting = false;      // last char was §
        public int chars = 0;                   // consumed char count
        public int pixels = 0;                  // total pixel width of consumed chars
        public Format format;                   // formatting state at current pos

        public int charsVisible = 0;            // char count at last visible char
        public int pixelsVisible = 0;           // pixel width at last visible char

        public int charsWord = 0;               // char count at last word ending
        public int pixelsWord = 0;              // pixel width at last word ending
        public Format wordFormat;               // formatting state at last word ending

        private Parser(String text) {
            this.text = text;
            this.format = new Format();
            this.wordFormat = new Format();
        }

        public boolean atEnd() {
            return this.chars == this.text.length();
        }

        /**
         * Return the pixels that will be added to width if the given char is consumed next
         */
        public int getAdvance(char c) {
            if(c == ChatColor.COLOR_CHAR && this.chars < this.text.length() - 1) {
                return 0;
            } else {
                int width = pixelWidth(c);

                if(this.format.bold) {
                    // Bold chars are one pixel wider
                    width += 1;
                }

                if(this.chars != 0) {
                    // If not the first char, add the gap between chars
                    width += 1;
                }

                return width;
            }
        }

        /**
         * Return the number of pixels that will be added to width by the next char
         */
        public int nextAdvance() {
            return this.getAdvance(this.text.charAt(this.chars));
        }

        /**
         * Consume a character
         */
        public void advance() {
            char c = this.text.charAt(this.chars++);

            if(this.formatting) {
                // Previous char was §, so this char will always be hidden
                switch(c) {
                    case 'k':
                        this.format.obfuscated = true;
                        break;

                    case 'l':
                        this.format.bold = true;
                        break;

                    case 'm':
                        this.format.strikethrough = true;
                        break;

                    case 'n':
                        this.format.underline = true;
                        break;

                    case 'o':
                        this.format.italic = true;
                        break;

                    default:
                        this.format.color = ChatColor.getByChar(c);
                        this.format.obfuscated = false;
                        this.format.bold = false;
                        this.format.strikethrough = false;
                        this.format.underline = false;
                        this.format.italic = false;
                        break;

                    case 'r':
                        this.format.obfuscated = false;
                        this.format.bold = false;
                        this.format.strikethrough = false;
                        this.format.underline = false;
                        this.format.italic = false;
                        this.format.color = null;
                        break;
                }

                this.formatting = false;
            } else {
                if(c == ChatColor.COLOR_CHAR && this.chars != this.text.length() - 1) {
                    // If we encounter a § and it's not the last char in the string,
                    // switch to the format state
                    this.formatting = true;
                } else {
                    // Otherwise, this char will be visible, so update the position
                    // pointer and increment the width
                    this.charsVisible = this.chars;
                    this.pixelsVisible = this.pixels;

                    if(this.chars == this.text.length() || Character.isWhitespace(this.text.charAt(this.chars))) {
                        this.charsWord = this.chars;
                        this.pixelsWord = this.pixels;
                        this.wordFormat = new Format(this.format);
                    }

                    this.pixels += getAdvance(c);
                }
            }
        }

        /**
         * Consume the maximum characters that will increase the width by no more than the given amount
         */
        public void advance(int width) {
            int startWidth = this.pixels;

            while(this.pixels + this.nextAdvance() <= startWidth + width) {
                this.advance();

                if(this.atEnd()) {
                    return;
                }
            }
        }

        /**
         * Consume the maximum words that will increase the width by no more than the given amount
         */
        public void advanceWords(int width) {
            int startWidth = this.pixels;

            while(this.pixels + this.nextAdvance() <= startWidth + width) {
                this.advance();

                if(this.atEnd()) {
                    return;
                }
            }

            this.chars = this.charsWord;
            this.pixels = this.pixelsWord;
            this.format = new Format(this.wordFormat);
        }
    }

    /**
     * Get the pixel width of the given text in the Minecraft font, excluding the drop shadow.
     * Formatting codes are accounted for, and so is bold text.
     */
    public static int pixelWidth(String text) {
        return pixelWidth(text, false);
    }

    public static int pixelWidth(String text, boolean bold) {
        Parser parser = new Parser(text);
        if(bold) parser.format.bold = true;
        while(!parser.atEnd()) {
            parser.advance();
        }
        return parser.pixels;
    }

    /**
     * Return the length of the longest prefix of the given string that fits
     * within the given pixel width when rendered in the Minecraft font. If
     * the prefix is shorter than the entire string, then it is guaranteed to
     * end with a visible character, and not any formatting character.
     */
    public static int longestPrefix(String text, int maxWidth) {
        int pos = 0;
        Parser parser = new Parser(text);

        while(parser.pixels < maxWidth) {
            if(parser.atEnd()) {
                return text.length();
            }
            pos = parser.charsVisible;
            parser.advance();
        }

        return pos;
    }


    /**
     * Return the length of the longest prefix of the given string that ends
     * on a word boundary and fits within the given pixel width when rendered
     * in the Minecraft font. If the prefix is shorter than the entire string,
     * then it is guaranteed to end with a visible character, and not any
     * formatting character.
     */
    public static int longestWordPrefix(String text, int maxWidth) {
        int pos = 0;
        Parser parser = new Parser(text);

        while(parser.pixels < maxWidth) {
            if(parser.atEnd()) {
                return text.length();
            }
            pos = parser.charsWord;
            parser.advance();
        }

        return pos;
    }

    public static final int SPACE_PIXEL_WIDTH = pixelWidth(' ');

    /**
     * Return a horizontal line spanning the width of the chat window
     * @param lineColor color of the line
     * @param width width of the line in pixels
     * @return the line as a string
     */
    public static String horizontalLine(ChatColor lineColor, int width) {
        return lineColor.toString() + ChatColor.STRIKETHROUGH + Strings.repeat(" ", (width + 1) / (SPACE_PIXEL_WIDTH + 1));
    }

    /**
     * Return some text centered in a horizontal line spanning the chat window
     * @param text the text to center in the line (can contain formatting codes)
     * @param lineColor the color of the line
     * @param width width of the line in pixels
     * @return the heading as a string
     */
    public static String horizontalLineHeading(String text, ChatColor lineColor, int width) {
        text = ChatColor.RESET + " " + text + ChatColor.RESET + " ";
        int textWidth = pixelWidth(text);
        int spaceCount = Math.max(0, ((width - textWidth) / 2 + 1) / (SPACE_PIXEL_WIDTH + 1));
        return lineColor.toString() + ChatColor.STRIKETHROUGH + Strings.repeat(" ", spaceCount) +
               text +
               lineColor.toString() + ChatColor.STRIKETHROUGH + Strings.repeat(" ", spaceCount);
    }

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

    /**
     * Word-wrap the given text to the given pixel width. Output is appended to the
     * given String list, which is also returned. Formatting codes are correctly
     * handled and propagated across lines.
     *
     * @param text to wrap
     * @param width pixel width of the chat window
     * @param lines list of lines to append the wrapped text to
     * @return lines
     */
    public static List<String> wordWrap(String text, int width, List<String> lines) {
        Parser parser = new Parser(text);
        Format format = null;
        int lineStart = 0;

        while(!parser.atEnd()) {
            parser.advanceWords(width);
            if(parser.chars == lineStart) {
                parser.advance(width);
                if(parser.chars == lineStart) {
                    parser.advance();
                }
            }

            if(format == null) {
                lines.add(text.substring(lineStart, parser.chars));
            } else {
                lines.add(format.toString() + text.substring(lineStart, parser.chars));
            }

            lineStart = parser.chars;
            format = new Format(parser.format);
        }

        return lines;
    }

    /**
     * Underlines a specific string maintaining colors.
     * 
     * @param str The string to underline.
     * @return The underlined string.
     */
    public static String underline(String str) {
        return applyFormat(ChatColor.UNDERLINE, str);
    }

    /**
     * Makes a string bolded, maintaining all colors.
     * 
     * @param str The string to be bolded.
     * @return The bolded string.
     */
    public static String bold(String str) {
        return applyFormat(ChatColor.BOLD, str);
    }

    /**
     * Italicizes a string, maintaining all previous colors.
     * 
     * @param str The string to be italicized.
     * @return The intaliciezd string.
     */
    public static String italicize(String str) {
        return applyFormat(ChatColor.ITALIC, str);
    }

    /**
     * Strikes out a string, keeping all previous colors.
     * 
     * @param str The string to strike out.
     * @return The striked-out string.
     */
    public static String strikethrough(String str) {
        return applyFormat(ChatColor.STRIKETHROUGH, str);
    }

    /**
     * Replaces all instances of one color in "str" from `from` to `to`.
     * @param str The base string.
     * @param from The base ChatColor.
     * @param to The ChatColor to replace to.
     * 
     * @return The replaced String.
     */
    public static String replaceColorCodes(String str, ChatColor from, ChatColor to) {
        return str.replaceAll(from.toString(), to.toString());
    }

    public static boolean isFormat(ChatColor color) {
        switch(color) {
            case BOLD:
            case ITALIC:
            case UNDERLINE:
            case STRIKETHROUGH:
            case MAGIC:
                return true;

            default:
                return false;
        }
    }

    /**
     * Apply formatting to an entire string that may contain other color codes
     * 
     * @param format The format to apply
     * @param text The string to apply the format to
     * 
     * @return New string with the format applied
     */
    private static String applyFormat(ChatColor format, String text) {
        StringBuilder buffer = new StringBuilder();
        boolean escaped = false; // previous char was the escape char
        boolean needsFormat = true; // format has been reset and needs to be re-applied

        for(int i = 0; i < text.length(); ++i) {
            char c = text.charAt(i);

            if(escaped) {
                escaped = false;
                ChatColor color = ChatColor.getByChar(c);
                needsFormat = color == null || !isFormat(color);
            } else if(c == ChatColor.COLOR_CHAR) {
                escaped = true;
            } else if(needsFormat) {
                needsFormat = false;
                buffer.append(format);
            }

            buffer.append(c);
        }

        return buffer.toString();
    }

    /**
     * Make text really small (only works on numbers right now)
     */
    public static String tiny(String text) {
        char[] chars = text.toCharArray();
        for(int i = 0; i < chars.length; i++) {
            switch(chars[i]) {
                case '.': chars[i] = '\u2024'; break;
                default: chars[i] = (char) (chars[i] - '0' + '\u2080'); break;
            }
        }
        return String.valueOf(chars);
    }

    public static net.md_5.bungee.api.ChatColor convert(Enum<?> color) {
        return net.md_5.bungee.api.ChatColor.valueOf(color.name());
    }
}
