public class ConfigFont extends Config
{
    public static final String INTERNAL_FILE_PREFIX = "preset://";
    public static final String INTERNAL_FONT_DIR = "fonts/";
    public static final String INTERNAL_BORDER_DIR = "borders/";

    /**
     * The size of a step in the scale of a font or border on the sliders in ControlPanelFont
     */
    public static final float FONT_BORDER_SCALE_GRANULARITY = 0.25f;

    public static final float MIN_FONT_SCALE = 0.25f;
    public static final int MAX_FONT_SCALE = (int) (8 / FONT_BORDER_SCALE_GRANULARITY);

    public static final int MIN_BORDER_SCALE = 0;
    public static final int MAX_BORDER_SCALE = (int) (8 / FONT_BORDER_SCALE_GRANULARITY);

    public static final int MIN_BORDER_INSET = -256;
    public static final int MAX_BORDER_INSET = 256;

    public static final int MIN_SPACE_WIDTH = 0;
    public static final int MAX_SPACE_WIDTH = 250;

    public static final int MIN_BASELINE_OFFSET = -32;
    public static final int MAX_BASELINE_OFFSET = 64;

    public static final int MIN_LINE_SPACING = -16;
    public static final int MAX_LINE_SPACING = 32;

    public static final int MIN_CHAR_SPACING = -16;
    public static final int MAX_CHAR_SPACING = 32;

    public static final int MIN_MESSAGE_SPACING = 0;
    public static final int MAX_MESSAGE_SPACING = 128;

    private String fontFilename;

    private String borderFilename;

    private int gridWidth;

    private int gridHeight;

    private float fontScale;

    private float borderScale;

    private int borderInsetX;

    private int borderInsetY;

    private int spaceWidth;

    private int baselineOffset;

    private String characterKey;

    private char unknownChar;

    private Boolean extendedCharEnabled;

    private int lineSpacing;

    private int charSpacing;

    private int messageSpacing;

    private FontType fontType;

    /**
     * Default constructor for creating an empty config to fill with load method
     */
    public ConfigFont()
    {
    }

    @Override
    public void reset()
    {
        fontFilename = null;
        borderFilename = null;
        fontType = null;
        spaceWidth = 0;
        baselineOffset = 0;
        gridWidth = 0;
        gridHeight = 0;
        fontScale = 1.0f;
        borderScale = 1.0f;
        borderInsetX = 0;
        borderInsetY = 0;
        characterKey = null;
        unknownChar = '\0';
        extendedCharEnabled = null;
        lineSpacing = 0;
        charSpacing = 0;
        messageSpacing = 0;
    }

    public void validateFontFile(LoadConfigReport report, String fontFilename)
    {
        if (fontFilename.startsWith(INTERNAL_FILE_PREFIX))
        {
            final String plainFilename = fontFilename.substring(INTERNAL_FILE_PREFIX.length());
            if (getClass().getClassLoader().getResource(plainFilename) == null)
            {
                report.addError("Preset font " + plainFilename + " not found", LoadConfigErrorType.FILE_NOT_FOUND);
            }
        }
        else if (!new File(fontFilename).exists())
        {
            report.addError("Unable to find font PNG file \"" + fontFilename + "\"", LoadConfigErrorType.FILE_NOT_FOUND);
        }
    }

    public void validateBorderFile(LoadConfigReport report, String borderFilename)
    {
        if (borderFilename.startsWith(INTERNAL_FILE_PREFIX))
        {
            final String plainFilename = borderFilename.substring(INTERNAL_FILE_PREFIX.length());
            if (getClass().getClassLoader().getResource(plainFilename) == null)
            {
                report.addError("Preset border " + plainFilename + " not found", LoadConfigErrorType.FILE_NOT_FOUND);
            }
        }
        else if (!new File(borderFilename).exists())
        {
            report.addError("Unable to find border PNG file \"" + borderFilename + "\"", LoadConfigErrorType.FILE_NOT_FOUND);
        }
    }

    public void validateStrings(LoadConfigReport report, String widthStr, String heightStr, String charKey, String unknownCharStr)
    {
        if (unknownCharStr.length() != 1)
        {
            report.addError("Unknown character value must be a single character", LoadConfigErrorType.PARSE_ERROR_CHAR);
        }

        validateIntegerWithLimitString(FontificatorProperties.KEY_FONT_GRID_WIDTH, widthStr, 1, report);
        validateIntegerWithLimitString(FontificatorProperties.KEY_FONT_GRID_HEIGHT, heightStr, 1, report);

        if (report.isErrorFree())
        {
            int w = Integer.parseInt(widthStr);
            int h = Integer.parseInt(heightStr);
            if (w > 0 && w > 0)
            {
                if (w * h != charKey.length())
                {
                    report.addError("Character key length (" + charKey.length() + ") must match the number of characters in the font image (" + w + " x " + h + " = " + (w * h) + ")", LoadConfigErrorType.VALUE_OUT_OF_RANGE);
                }
            }

            if (!charKey.contains(unknownCharStr))
            {
                report.addError("The value for " + FontificatorProperties.KEY_FONT_UNKNOWN_CHAR + " (" + unknownCharStr + ") must also be in the value for " + FontificatorProperties.KEY_FONT_CHARACTERS, LoadConfigErrorType.VALUE_OUT_OF_RANGE);
            }
        }
    }

    public void validateStrings(LoadConfigReport report, String fontFilenameStr, String borderFilenameStr, String widthStr, String heightStr, String charKey, String unknownCharStr, String extendedCharStr, String scaleStr, String borderScaleStr, String borderInsetXStr, String borderInsetYStr, String spaceWidthStr, String baselineStr, String lineStr, String charStr, String msgStr, String fontTypeStr)
    {
        validateStrings(report, widthStr, heightStr, charKey, unknownCharStr);

        if (fontFilenameStr.isEmpty())
        {
            report.addError("A font filename is required", LoadConfigErrorType.MISSING_VALUE);
        }

        if (borderFilenameStr.isEmpty())
        {
            report.addError("A border filename is required", LoadConfigErrorType.MISSING_VALUE);
        }

        validateBooleanStrings(report, extendedCharStr);

        validateFloatWithLimitString(FontificatorProperties.KEY_FONT_SCALE, scaleStr, MIN_FONT_SCALE, MAX_FONT_SCALE, report);
        validateFloatWithLimitString(FontificatorProperties.KEY_FONT_BORDER_SCALE, borderScaleStr, MIN_BORDER_SCALE, MAX_BORDER_SCALE, report);
        validateIntegerWithLimitString(FontificatorProperties.KEY_FONT_BORDER_INSET_X, borderInsetXStr, MIN_BORDER_INSET, MAX_BORDER_INSET, report);
        validateIntegerWithLimitString(FontificatorProperties.KEY_FONT_BORDER_INSET_Y, borderInsetYStr, MIN_BORDER_INSET, MAX_BORDER_INSET, report);
        validateIntegerWithLimitString(FontificatorProperties.KEY_FONT_SPACE_WIDTH, spaceWidthStr, MIN_SPACE_WIDTH, MAX_SPACE_WIDTH, report);
        validateIntegerWithLimitString(FontificatorProperties.KEY_FONT_BASELINE_OFFSET, baselineStr, MIN_BASELINE_OFFSET, MAX_BASELINE_OFFSET, report);
        validateIntegerWithLimitString(FontificatorProperties.KEY_FONT_SPACING_LINE, lineStr, MIN_LINE_SPACING, MAX_LINE_SPACING, report);
        validateIntegerWithLimitString(FontificatorProperties.KEY_FONT_SPACING_CHAR, charStr, MIN_CHAR_SPACING, MAX_CHAR_SPACING, report);
        validateIntegerWithLimitString(FontificatorProperties.KEY_FONT_SPACING_MESSAGE, msgStr, MIN_MESSAGE_SPACING, MAX_MESSAGE_SPACING, report);

        if (!FontType.contains(fontTypeStr))
        {
            report.addError("Value of key \"" + FontificatorProperties.KEY_FONT_TYPE + "\" is invalid.", LoadConfigErrorType.PARSE_ERROR_ENUM);
        }
    }

    @Override
    public LoadConfigReport load(Properties props, LoadConfigReport report)
    {
        this.props = props;

        reset();

        // Check that the values exist
        baseValidation(props, FontificatorProperties.FONT_KEYS, report);

        if (report.isErrorFree())
        {
            final String gridWidthStr = props.getProperty(FontificatorProperties.KEY_FONT_GRID_WIDTH);
            final String gridHeightStr = props.getProperty(FontificatorProperties.KEY_FONT_GRID_HEIGHT);
            final String scaleStr = props.getProperty(FontificatorProperties.KEY_FONT_SCALE);
            final String borderScaleStr = props.getProperty(FontificatorProperties.KEY_FONT_BORDER_SCALE);
            final String borderInsetXStr = props.getProperty(FontificatorProperties.KEY_FONT_BORDER_INSET_X);
            final String borderInsetYStr = props.getProperty(FontificatorProperties.KEY_FONT_BORDER_INSET_Y);
            final String spaceWidthStr = props.getProperty(FontificatorProperties.KEY_FONT_SPACE_WIDTH);
            final String baselineStr = props.getProperty(FontificatorProperties.KEY_FONT_BASELINE_OFFSET);
            final String unknownCharStr = props.getProperty(FontificatorProperties.KEY_FONT_UNKNOWN_CHAR);
            final String extendedCharStr = props.getProperty(FontificatorProperties.KEY_FONT_EXTENDED_CHAR);
            final String lineSpacingStr = props.getProperty(FontificatorProperties.KEY_FONT_SPACING_LINE);
            final String charSpacingStr = props.getProperty(FontificatorProperties.KEY_FONT_SPACING_CHAR);
            final String messageSpacingStr = props.getProperty(FontificatorProperties.KEY_FONT_SPACING_MESSAGE);
            final String fontTypeStr = props.getProperty(FontificatorProperties.KEY_FONT_TYPE);
            final String charKeyStr = props.getProperty(FontificatorProperties.KEY_FONT_CHARACTERS);

            final String borderFilenameStr = props.getProperty(FontificatorProperties.KEY_FONT_FILE_BORDER);
            final String fontFilenameStr = props.getProperty(FontificatorProperties.KEY_FONT_FILE_FONT);

            // Check that the values are valid
            validateStrings(report, fontFilenameStr, borderFilenameStr, gridWidthStr, gridHeightStr, charKeyStr, unknownCharStr, extendedCharStr, scaleStr, borderScaleStr, borderInsetXStr, borderInsetYStr, spaceWidthStr, baselineStr, lineSpacingStr, charSpacingStr, messageSpacingStr, fontTypeStr);

            // Fill the values
            if (report.isErrorFree())
            {
                this.borderFilename = props.getProperty(FontificatorProperties.KEY_FONT_FILE_BORDER);
                this.fontFilename = props.getProperty(FontificatorProperties.KEY_FONT_FILE_FONT);
                this.fontType = FontType.valueOf(props.getProperty(FontificatorProperties.KEY_FONT_TYPE));
                this.unknownChar = unknownCharStr.charAt(0);
                this.extendedCharEnabled = evaluateBooleanString(props, FontificatorProperties.KEY_FONT_EXTENDED_CHAR, report);
                this.characterKey = props.getProperty(FontificatorProperties.KEY_FONT_CHARACTERS);
                this.gridWidth = Integer.parseInt(gridWidthStr);
                this.gridHeight = Integer.parseInt(gridHeightStr);
                this.fontScale = Float.parseFloat(scaleStr);
                this.borderScale = Float.parseFloat(borderScaleStr);
                this.borderInsetX = Integer.parseInt(borderInsetXStr);
                this.borderInsetY = Integer.parseInt(borderInsetYStr);
                this.spaceWidth = Integer.parseInt(spaceWidthStr);
                this.baselineOffset = Integer.parseInt(baselineStr);
                this.lineSpacing = Integer.parseInt(lineSpacingStr);
                this.charSpacing = Integer.parseInt(charSpacingStr);
                this.messageSpacing = Integer.parseInt(messageSpacingStr);
            }
        }

        return report;
    }

    public String getFontFilename()
    {
        return fontFilename;
    }

    public void setFontFilename(String fontFilename)
    {
        this.fontFilename = fontFilename;
        props.setProperty(FontificatorProperties.KEY_FONT_FILE_FONT, fontFilename);
    }

    public String getBorderFilename()
    {
        return borderFilename;
    }

    public void setBorderFilename(String borderFilename)
    {
        this.borderFilename = borderFilename;
        props.setProperty(FontificatorProperties.KEY_FONT_FILE_BORDER, borderFilename);
    }

    public int getGridWidth()
    {
        return gridWidth;
    }

    public void setGridWidth(int gridWidth)
    {
        this.gridWidth = gridWidth;
        props.setProperty(FontificatorProperties.KEY_FONT_GRID_WIDTH, Integer.toString(gridWidth));
    }

    public int getGridHeight()
    {
        return gridHeight;
    }

    public void setGridHeight(int gridHeight)
    {
        this.gridHeight = gridHeight;
        props.setProperty(FontificatorProperties.KEY_FONT_GRID_HEIGHT, Integer.toString(gridHeight));
    }

    public float getFontScale()
    {
        return fontScale;
    }

    public void setFontScale(float fontScale)
    {
        this.fontScale = fontScale;
        props.setProperty(FontificatorProperties.KEY_FONT_SCALE, Float.toString(fontScale));
    }

    public float getBorderScale()
    {
        return borderScale;
    }

    public void setBorderScale(float borderScale)
    {
        this.borderScale = borderScale;
        props.setProperty(FontificatorProperties.KEY_FONT_BORDER_SCALE, Float.toString(borderScale));
    }

    public int getBorderInsetX()
    {
        return borderInsetX;
    }

    public void setBorderInsetX(int borderInsetX)
    {
        this.borderInsetX = borderInsetX;
        props.setProperty(FontificatorProperties.KEY_FONT_BORDER_INSET_X, Integer.toString(borderInsetX));
    }

    public int getBorderInsetY()
    {
        return borderInsetY;
    }

    public void setBorderInsetY(int borderInsetY)
    {
        this.borderInsetY = borderInsetY;
        props.setProperty(FontificatorProperties.KEY_FONT_BORDER_INSET_Y, Integer.toString(borderInsetY));
    }

    public int getSpaceWidth()
    {
        return spaceWidth;
    }

    public void setSpaceWidth(int spaceWidth)
    {
        this.spaceWidth = spaceWidth;
        props.setProperty(FontificatorProperties.KEY_FONT_SPACE_WIDTH, Integer.toString(spaceWidth));
    }

    public int getBaselineOffset()
    {
        return baselineOffset;
    }

    public void setBaselineOffset(int baselineOffset)
    {
        this.baselineOffset = baselineOffset;
        props.getProperty(FontificatorProperties.KEY_FONT_BASELINE_OFFSET, Integer.toString(baselineOffset));
    }

    public String getCharacterKey()
    {
        return characterKey;
    }

    public void setCharacterKey(String characterKey)
    {
        this.characterKey = characterKey;
        props.setProperty(FontificatorProperties.KEY_FONT_CHARACTERS, characterKey);
    }

    public char getUnknownChar()
    {
        return unknownChar;
    }

    public void setUnknownChar(char unknownChar)
    {
        this.unknownChar = unknownChar;
        props.setProperty(FontificatorProperties.KEY_FONT_UNKNOWN_CHAR, Character.toString(unknownChar));
    }

    public boolean isExtendedCharEnabled()
    {
        return extendedCharEnabled;
    }

    public void setExtendedCharEnabled(boolean extendedCharEnabled)
    {
        this.extendedCharEnabled = extendedCharEnabled;
        props.setProperty(FontificatorProperties.KEY_FONT_EXTENDED_CHAR, Boolean.toString(extendedCharEnabled));
    }

    public int getLineSpacing()
    {
        return lineSpacing;
    }

    public void setLineSpacing(int lineSpacing)
    {
        this.lineSpacing = lineSpacing;
        props.setProperty(FontificatorProperties.KEY_FONT_SPACING_LINE, Integer.toString(lineSpacing));
    }

    public int getCharSpacing()
    {
        return charSpacing;
    }

    public void setCharSpacing(int charSpacing)
    {
        this.charSpacing = charSpacing;
        props.setProperty(FontificatorProperties.KEY_FONT_SPACING_CHAR, Integer.toString(charSpacing));
    }

    public int getMessageSpacing()
    {
        return messageSpacing;
    }

    public void setMessageSpacing(int messageSpacing)
    {
        this.messageSpacing = messageSpacing;
        props.setProperty(FontificatorProperties.KEY_FONT_SPACING_MESSAGE, Integer.toString(messageSpacing));
    }

    public FontType getFontType()
    {
        return fontType;
    }

    public void setFontType(FontType fontType)
    {
        this.fontType = fontType;
        props.setProperty(FontificatorProperties.KEY_FONT_TYPE, fontType.name());
    }

} public class FontificatorProperties extends Properties
{
    private static final Logger logger = Logger.getLogger(FontificatorProperties.class);

    private static final long serialVersionUID = 1L;

    /**
     * Copy of properties to compare with when checking if there have been changes
     */
    private FontificatorProperties lastSavedCopy;

    private final static String ENC_PASSWORD = "Eastmost penninsula is the secret.";

    /**
     * The name of the file that holds the location of the last file saved or loaded by a user, to be used when the
     * program starts to automatically load the previously used configuration
     */
    private static final String CONFIG_FILE_LAST_LOCATION = ".fontificator.conf";

    public static final String KEY_IRC_USER = "ircUser";
    public static final String KEY_IRC_HOST = "ircHost";
    public static final String KEY_IRC_PORT = "ircPort";
    public static final String KEY_IRC_AUTH = "ircAuth";
    public static final String KEY_IRC_ANON = "ircAnon";
    public static final String KEY_IRC_CHAN = "ircChannel";
    public static final String KEY_IRC_AUTO_RECONNECT = "ircAutoReconnect";

    public static final String[] IRC_KEYS = new String[] { KEY_IRC_USER, KEY_IRC_HOST, KEY_IRC_PORT, KEY_IRC_AUTH, KEY_IRC_ANON, KEY_IRC_CHAN, KEY_IRC_AUTO_RECONNECT };

    public static final String KEY_FONT_FILE_BORDER = "fontBorderFile";
    public static final String KEY_FONT_FILE_FONT = "fontFile";
    public static final String KEY_FONT_TYPE = "fontType";
    public static final String KEY_FONT_GRID_WIDTH = "fontGridWidth";
    public static final String KEY_FONT_GRID_HEIGHT = "fontGridHeight";
    public static final String KEY_FONT_SCALE = "fontScale";
    public static final String KEY_FONT_BORDER_SCALE = "fontBorderScale";
    public static final String KEY_FONT_BORDER_INSET_X = "fontBorderInsetX";
    public static final String KEY_FONT_BORDER_INSET_Y = "fontBorderInsetY";
    public static final String KEY_FONT_SPACE_WIDTH = "fontSpaceWidth";
    public static final String KEY_FONT_BASELINE_OFFSET = "fontBaselineOffset";
    public static final String KEY_FONT_UNKNOWN_CHAR = "fontUnknownChar";
    public static final String KEY_FONT_EXTENDED_CHAR = "fontExtendedChar";
    public static final String KEY_FONT_CHARACTERS = "fontCharacters";
    public static final String KEY_FONT_SPACING_LINE = "fontLineSpacing";
    public static final String KEY_FONT_SPACING_CHAR = "fontCharSpacing";
    public static final String KEY_FONT_SPACING_MESSAGE = "fontMessageSpacing";

    public static final String[] FONT_KEYS = new String[] { KEY_FONT_FILE_BORDER, KEY_FONT_FILE_FONT, KEY_FONT_TYPE, KEY_FONT_GRID_WIDTH, KEY_FONT_GRID_HEIGHT, KEY_FONT_SCALE, KEY_FONT_BORDER_SCALE, KEY_FONT_BORDER_INSET_X, KEY_FONT_BORDER_INSET_Y, KEY_FONT_SPACE_WIDTH, KEY_FONT_BASELINE_OFFSET, KEY_FONT_UNKNOWN_CHAR, KEY_FONT_EXTENDED_CHAR, KEY_FONT_CHARACTERS, KEY_FONT_SPACING_LINE, KEY_FONT_SPACING_CHAR, KEY_FONT_SPACING_MESSAGE };

    public static final String KEY_CHAT_SCROLL = "chatScrollEnabled";
    public static final String KEY_CHAT_RESIZABLE = "chatResizable";
    public static final String KEY_CHAT_POSITION = "chatPosition";
    public static final String KEY_CHAT_POSITION_X = "chatPositionX";
    public static final String KEY_CHAT_POSITION_Y = "chatPositionY";
    public static final String KEY_CHAT_FROM_BOTTOM = "chatFromBottom";
    public static final String KEY_CHAT_WINDOW_WIDTH = "chatWidth"; // Legacy, no longer saved, used for chat window, not chat window content pane
    public static final String KEY_CHAT_WINDOW_HEIGHT = "chatHeight"; // Legacy, no longer saved, used for chat window, not chat window content pane
    public static final String KEY_CHAT_WIDTH = "chatPixelWidth";
    public static final String KEY_CHAT_HEIGHT = "chatPixelHeight";
    public static final String KEY_CHAT_CHROMA_ENABLED = "chromaEnabled";
    public static final String KEY_CHAT_INVERT_CHROMA = "invertChroma";
    public static final String KEY_CHAT_REVERSE_SCROLLING = "reverseScrolling";
    public static final String KEY_CHAT_CHROMA_LEFT = "chromaLeft";
    public static final String KEY_CHAT_CHROMA_TOP = "chromaTop";
    public static final String KEY_CHAT_CHROMA_RIGHT = "chromaRight";
    public static final String KEY_CHAT_CHROMA_BOTTOM = "chromaBottom";
    public static final String KEY_CHAT_CHROMA_CORNER = "chromaCornerRadius";
    public static final String KEY_CHAT_ALWAYS_ON_TOP = "chatAlwaysOnTop";
    public static final String KEY_CHAT_ANTIALIAS = "chatAntialias";

    public static final String[] CHAT_KEYS = new String[] { KEY_CHAT_SCROLL, KEY_CHAT_RESIZABLE, KEY_CHAT_POSITION, KEY_CHAT_POSITION_X, KEY_CHAT_POSITION_Y, KEY_CHAT_FROM_BOTTOM, KEY_CHAT_WIDTH, KEY_CHAT_HEIGHT, KEY_CHAT_CHROMA_ENABLED, KEY_CHAT_INVERT_CHROMA, KEY_CHAT_REVERSE_SCROLLING, KEY_CHAT_CHROMA_LEFT, KEY_CHAT_CHROMA_TOP, KEY_CHAT_CHROMA_RIGHT, KEY_CHAT_CHROMA_BOTTOM, KEY_CHAT_CHROMA_CORNER, KEY_CHAT_ALWAYS_ON_TOP, KEY_CHAT_ANTIALIAS };

    public static final String[] CHAT_KEYS_EXCEPT_WINDOW_POSITION = new String[] { KEY_CHAT_SCROLL, KEY_CHAT_RESIZABLE, KEY_CHAT_POSITION, KEY_CHAT_FROM_BOTTOM, KEY_CHAT_WIDTH, KEY_CHAT_HEIGHT, KEY_CHAT_CHROMA_ENABLED, KEY_CHAT_INVERT_CHROMA, KEY_CHAT_REVERSE_SCROLLING, KEY_CHAT_CHROMA_LEFT, KEY_CHAT_CHROMA_TOP, KEY_CHAT_CHROMA_RIGHT, KEY_CHAT_CHROMA_BOTTOM, KEY_CHAT_CHROMA_CORNER, KEY_CHAT_ALWAYS_ON_TOP, KEY_CHAT_ANTIALIAS };

    public static final String KEY_COLOR_BG = "colorBackground";
    public static final String KEY_COLOR_FG = "colorForeground";
    public static final String KEY_COLOR_BORDER = "colorBorder";
    public static final String KEY_COLOR_HIGHLIGHT = "colorHighlight";
    public static final String KEY_COLOR_CHROMA_KEY = "chromaKey";
    public static final String KEY_COLOR_PALETTE = "colorPalette";
    public static final String KEY_COLOR_USERNAME = "colorUsername";
    public static final String KEY_COLOR_TIMESTAMP = "colorTimestamp";
    public static final String KEY_COLOR_MESSAGE = "colorMessage";
    public static final String KEY_COLOR_JOIN = "colorJoin";
    public static final String KEY_COLOR_TWITCH = "colorUseTwitch";

    public static final String[] COLOR_KEYS = new String[] { KEY_COLOR_BG, KEY_COLOR_FG, KEY_COLOR_BORDER, KEY_COLOR_HIGHLIGHT, KEY_COLOR_CHROMA_KEY, KEY_COLOR_PALETTE, KEY_COLOR_USERNAME, KEY_COLOR_TIMESTAMP, KEY_COLOR_MESSAGE, KEY_COLOR_JOIN, KEY_COLOR_TWITCH };

    public static final String[] COLOR_KEYS_WITHOUT_PALETTE = new String[] { KEY_COLOR_BG, KEY_COLOR_FG, KEY_COLOR_BORDER, KEY_COLOR_HIGHLIGHT, KEY_COLOR_CHROMA_KEY, KEY_COLOR_USERNAME, KEY_COLOR_TIMESTAMP, KEY_COLOR_MESSAGE, KEY_COLOR_JOIN, KEY_COLOR_TWITCH };

    public static final String KEY_MESSAGE_JOIN = "messageShowJoin";
    public static final String KEY_MESSAGE_USERNAME = "messageShowUsername";
    public static final String KEY_MESSAGE_TIMESTAMP = "messageShowTimestamp";
    public static final String KEY_MESSAGE_USERFORMAT = "messageUsernameFormat";
    public static final String KEY_MESSAGE_TIMEFORMAT = "messageTimestampFormat";
    public static final String KEY_MESSAGE_CONTENT_BREAK = "messageContentBreak";
    public static final String KEY_MESSAGE_QUEUE_SIZE = "messageQueueSize";
    public static final String KEY_MESSAGE_SPEED = "messageSpeed";
    public static final String KEY_MESSAGE_EXPIRATION_TIME = "messageExpirationTime";
    public static final String KEY_MESSAGE_HIDE_EMPTY_BORDER = "messageHideEmptyBorder";
    public static final String KEY_MESSAGE_HIDE_EMPTY_BACKGROUND = "messageHideEmptyBackground";
    public static final String KEY_MESSAGE_CASE_TYPE = "messageUserCase";
    public static final String KEY_MESSAGE_CASE_SPECIFY = "messageUserCaseSpecify";
    public static final String KEY_MESSAGE_CASING = "messageCasing";

    public static final String[] MESSAGE_KEYS = new String[] { KEY_MESSAGE_JOIN, KEY_MESSAGE_USERNAME, KEY_MESSAGE_TIMESTAMP, KEY_MESSAGE_USERFORMAT, KEY_MESSAGE_TIMEFORMAT, KEY_MESSAGE_CONTENT_BREAK, KEY_MESSAGE_QUEUE_SIZE, KEY_MESSAGE_SPEED, KEY_MESSAGE_EXPIRATION_TIME, KEY_MESSAGE_HIDE_EMPTY_BORDER, KEY_MESSAGE_HIDE_EMPTY_BACKGROUND, KEY_MESSAGE_CASE_TYPE, KEY_MESSAGE_CASE_SPECIFY, KEY_MESSAGE_CASING };

    public static final String KEY_EMOJI_ENABLED = "emojiEnabled";
    public static final String KEY_EMOJI_ANIMATION = "emojiAnimationEnabled";
    public static final String KEY_EMOJI_TWITCH_BADGES = "badgesEnabled";
    public static final String KEY_EMOJI_FFZ_BADGES = "badgesFfzEnabled";
    public static final String KEY_EMOJI_SCALE_TO_LINE = "emojiScaleToLine";
    public static final String KEY_EMOJI_BADGE_SCALE_TO_LINE = "badgeScaleToLine";
    public static final String KEY_EMOJI_BADGE_HEIGHT_OFFSET = "badgeHeightOffset";
    public static final String KEY_EMOJI_SCALE = "emojiScale";
    public static final String KEY_EMOJI_BADGE_SCALE = "badgeScale";
    public static final String KEY_EMOJI_DISPLAY_STRAT = "emojiDisplayStrat";
    public static final String KEY_EMOJI_TWITCH_ENABLE = "emojiTwitchEnabled";
    public static final String KEY_EMOJI_TWITCH_CACHE = "emojiTwitchCached";
    public static final String KEY_EMOJI_FFZ_ENABLE = "emojiFfzEnabled";
    public static final String KEY_EMOJI_FFZ_CACHE = "emojiFfzCached";
    public static final String KEY_EMOJI_BTTV_ENABLE = "emojiBttvEnabled";
    public static final String KEY_EMOJI_BTTV_CACHE = "emojiBttvCached";
    public static final String KEY_EMOJI_TWITTER_ENABLE = "emojiTwitterEnabled";

    public static final String[] EMOJI_KEYS = new String[] { KEY_EMOJI_ENABLED, KEY_EMOJI_ANIMATION, KEY_EMOJI_TWITCH_BADGES, KEY_EMOJI_FFZ_BADGES, KEY_EMOJI_SCALE_TO_LINE, KEY_EMOJI_BADGE_SCALE_TO_LINE, KEY_EMOJI_BADGE_HEIGHT_OFFSET, KEY_EMOJI_SCALE, KEY_EMOJI_BADGE_SCALE, KEY_EMOJI_DISPLAY_STRAT, KEY_EMOJI_TWITCH_ENABLE, KEY_EMOJI_TWITCH_CACHE, KEY_EMOJI_FFZ_ENABLE, KEY_EMOJI_FFZ_CACHE, KEY_EMOJI_BTTV_ENABLE, KEY_EMOJI_BTTV_CACHE, KEY_EMOJI_TWITTER_ENABLE };

    public static final String KEY_CENSOR_ENABLED = "censorEnabled";
    public static final String KEY_CENSOR_PURGE_ON_TWITCH_BAN = "censorPurgeOnTwitchBan";
    public static final String KEY_CENSOR_URL = "censorUrl";
    public static final String KEY_CENSOR_FIRST_URL = "censorFirstUrl";
    public static final String KEY_CENSOR_UNKNOWN_CHARS = "censorUnknownChars";
    public static final String KEY_CENSOR_UNKNOWN_CHARS_PERCENT = "censorUnknownCharsPercent";
    public static final String KEY_CENSOR_WHITE = "censorWhitelist";
    public static final String KEY_CENSOR_BLACK = "censorBlacklist";
    public static final String KEY_CENSOR_BANNED = "censorBannedWords";

    public static final String[] CENSOR_KEYS = new String[] { KEY_CENSOR_ENABLED, KEY_CENSOR_PURGE_ON_TWITCH_BAN, KEY_CENSOR_URL, KEY_CENSOR_FIRST_URL, KEY_CENSOR_UNKNOWN_CHARS, KEY_CENSOR_UNKNOWN_CHARS_PERCENT, KEY_CENSOR_WHITE, KEY_CENSOR_BLACK, KEY_CENSOR_BANNED };

    public static final String[][] ALL_KEYS_EXCEPT_CHAT_WINDOW_POSITION = new String[][] { IRC_KEYS, FONT_KEYS, CHAT_KEYS_EXCEPT_WINDOW_POSITION, COLOR_KEYS, MESSAGE_KEYS, EMOJI_KEYS, CENSOR_KEYS };

    public static final String[][] ALL_KEYS = new String[][] { IRC_KEYS, FONT_KEYS, CHAT_KEYS, COLOR_KEYS, MESSAGE_KEYS, EMOJI_KEYS, CENSOR_KEYS };

    /**
     * Get all the property keys to be tested to determine if two fProps are equal. The parameter FontificatorProperties
     * is to be used as a collection of configuration that is used to select which properties matter. For example, if
     * chat window position should be remembered or not, so if remember position is unchecked, it doesn't ask you to
     * save changes just because you moved the window.
     * 
     * @param fProps
     *            contains configuration to be used to determine which keys are needed
     * @return all keys to be tested for equals
     */
    private static String[][] getKeysForEqualsTest(FontificatorProperties fProps)
    {
        if (fProps != null && fProps.getChatConfig() != null && fProps.getChatConfig().isRememberPosition())
        {
            return ALL_KEYS_EXCEPT_CHAT_WINDOW_POSITION;
        }

        return ALL_KEYS;
    }

    private ConfigIrc ircConfig = new ConfigIrc();

    private ConfigFont fontConfig = new ConfigFont();

    private ConfigChat chatConfig = new ConfigChat();

    private ConfigColor colorConfig = new ConfigColor();

    private ConfigMessage messageConfig = new ConfigMessage();

    private ConfigEmoji emojiConfig = new ConfigEmoji();

    private ConfigCensor censorConfig = new ConfigCensor();

    public FontificatorProperties()
    {
    }

    @Override
    public void clear()
    {
        ircConfig.reset();
        fontConfig.reset();
        chatConfig.reset();
        colorConfig.reset();
        messageConfig.reset();
        emojiConfig.reset();
        censorConfig.reset();
        super.clear();
    }

    public ConfigIrc getIrcConfig()
    {
        return ircConfig;
    }

    public ConfigFont getFontConfig()
    {
        return fontConfig;
    }

    public ConfigChat getChatConfig()
    {
        return chatConfig;
    }

    public ConfigColor getColorConfig()
    {
        return colorConfig;
    }

    public ConfigMessage getMessageConfig()
    {
        return messageConfig;
    }

    public ConfigEmoji getEmojiConfig()
    {
        return emojiConfig;
    }

    public ConfigCensor getCensorConfig()
    {
        return censorConfig;
    }

    /**
     * Called whenever unsaved changes might be lost to let the user have the option to save them
     * 
     * @param ctrlWindow
     * @return okayToContinue
     */
    public boolean checkForUnsavedProps(ControlWindow ctrlWindow, Component parent)
    {
        if (hasUnsavedChanges())
        {
            int response = JOptionPane.showConfirmDialog(parent, "Save configuration changes?", "Unsaved Changes", JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE);
            if (response == JOptionPane.YES_OPTION)
            {
                if (ctrlWindow.saveConfig())
                {
                    return true;
                }
            }
            else if (response == JOptionPane.NO_OPTION)
            {
                return true;
            }

            return false;
        }
        else
        {
            return true;
        }
    }

    /**
     * Load configuration from the file indicated by the specified filename, or a preset file contained in the classpath
     * resource directory
     * 
     * @param filename
     * @return report
     * @throws Exception
     */
    public LoadConfigReport loadFile(String filename) throws Exception
    {
        if (filename.startsWith(ConfigFont.INTERNAL_FILE_PREFIX))
        {
            final String plainFilename = filename.substring(ConfigFont.INTERNAL_FILE_PREFIX.length());

            if (getClass().getClassLoader().getResource(plainFilename) == null)
            {
                LoadConfigReport report = new LoadConfigReport();
                final String errorMsg = "Preset theme " + plainFilename + " not found";
                ChatWindow.popup.handleProblem(errorMsg);
                report.addError(errorMsg, LoadConfigErrorType.FILE_NOT_FOUND);
                return report;
            }

            InputStream is = getClass().getClassLoader().getResourceAsStream(plainFilename);
            LoadConfigReport report = loadFile(is, filename, true);

            if (report.isErrorFree())
            {
                // This is a temporary copy, not to be saved to the last saved file for loading next time, but to avoid
                // having to not save when switching between presets
                this.lastSavedCopy = getCopy();
            }

            is.close();
            return report;
        }
        else
        {
            return loadFile(new File(filename));
        }
    }

    /**
     * Load configuration from the specified file
     * 
     * @param file
     * @return report
     * @throws Exception
     */
    public LoadConfigReport loadFile(File file) throws Exception
    {
        logger.trace("Loading file " + file.getAbsolutePath());
        InputStream is = new FileInputStream(file);
        LoadConfigReport report = loadFile(is, file.getAbsolutePath(), false);
        is.close();
        return report;
    }

    /**
     * Load file from InputStream. Does not close InputStream
     * 
     * @param is
     * @param filename
     * @param isPreset
     * @return report
     * @throws Exception
     */
    private LoadConfigReport loadFile(InputStream is, String filename, boolean isPreset) throws Exception
    {
        final String prevAuth = getProperty(KEY_IRC_AUTH);
        super.load(is);
        final String currAuth = getProperty(KEY_IRC_AUTH);

        // Only decrypt the auth token here if there wasn't a previously loaded one that was already decrypted. This is
        // determined by whether the previous authorization wasn't loaded (null) or if there has been a change. If the
        // inputstream data didn't have an auth token, then the previous and current ones will match, meaning a new one
        // wasn't loaded and there's no need to decrypt.
        if (prevAuth == null || !prevAuth.equals(currAuth))
        {
            decryptProperty(KEY_IRC_AUTH);
        }

        LoadConfigReport report = loadConfigs(!isPreset);

        if (report.isErrorFree() && !isPreset)
        {
            rememberLastConfigFile(filename);
        }

        return report;
    }

    /**
     * Store the specified path to a configuration file in the last config file location conf file
     * 
     * @param path
     *            The path of the configuration file to remember
     */
    public void rememberLastConfigFile(String path)
    {
        logger.trace("Remembering last loaded file " + path);

        // Keep track of the last saved or loaded copy to compare with to see if a save prompt is required when exiting
        this.lastSavedCopy = getCopy();

        BufferedWriter writer = null;
        try
        {
            writer = new BufferedWriter(new FileWriter(CONFIG_FILE_LAST_LOCATION, false));
            writer.write(path);
        }
        catch (Exception e)
        {
            // Don't alert the user, just log behind the scenes, not important enough to warrant a popup
            logger.error("Unable to save last file loaded", e);
        }
        finally
        {
            if (writer != null)
            {
                try
                {
                    writer.close();
                }
                catch (Exception e)
                {
                    logger.error(e.toString(), e);
                }
            }
        }
    }

    /**
     * Delete the last config file location conf file that stored the path to a configuration file. To be used to remove
     * a file that is not found or has errors.
     */
    public void forgetLastConfigFile()
    {
        logger.trace("Forgetting last loaded");

        File f = new File(CONFIG_FILE_LAST_LOCATION);
        f.delete();
    }

    /**
     * Save the configuration to the specified file
     * 
     * @param file
     * @throws Exception
     */
    public void saveFile(File file) throws Exception
    {
        OutputStream os = new FileOutputStream(file);
        encryptProperty(KEY_IRC_AUTH);
        super.store(os, null);
        decryptProperty(KEY_IRC_AUTH);
        rememberLastConfigFile(file.getAbsolutePath());
    }

    public void encryptProperty(String key)
    {
        BasicTextEncryptor textEncryptor = new BasicTextEncryptor();
        textEncryptor.setPassword(ENC_PASSWORD);

        final String decryptedValue = getProperty(key);
        if (decryptedValue != null && !decryptedValue.isEmpty())
        {
            try
            {
                final String encryptedValue = textEncryptor.encrypt(decryptedValue);
                setProperty(key, encryptedValue);
            }
            catch (Exception e)
            {
                final String errorMessage = "Error encrypting value for " + key + " property";
                logger.error(errorMessage, e);
                ChatWindow.popup.handleProblem(errorMessage);
                setProperty(key, "");
            }
        }
    }

    public void decryptProperty(String key)
    {
        BasicTextEncryptor textEncryptor = new BasicTextEncryptor();
        textEncryptor.setPassword(ENC_PASSWORD);
        final String encryptedValue = getProperty(key);
        if (encryptedValue != null && !encryptedValue.isEmpty())
        {
            try
            {
                final String decryptedValue = textEncryptor.decrypt(encryptedValue);
                setProperty(key, decryptedValue);
            }
            catch (Exception e)
            {
                final String errorMessage = "Error decrypting value for " + key + " property";
                logger.error(errorMessage, e);
                ChatWindow.popup.handleProblem(errorMessage);
                setProperty(key, "");
            }
        }
    }

    /**
     * Try to load the configuration file stored i the last config file location conf file.
     * 
     * @return report
     * @throws Exception
     */
    public LoadConfigReport loadLast() throws Exception
    {
        logger.trace("Load last");

        final String previousConfigNotFound = "Previous configuration not found.";
        final String previousConfigError = "Error loading previous configuration.";

        BufferedReader reader = null;
        try
        {
            File lastFile = new File(CONFIG_FILE_LAST_LOCATION);
            if (!lastFile.exists())
            {
                LoadConfigReport errorReport = new LoadConfigReport();
                errorReport.addError(previousConfigNotFound, LoadConfigErrorType.FILE_NOT_FOUND);
                return errorReport;
            }
            reader = new BufferedReader(new FileReader(lastFile));
            final String lastConfigFilename = reader.readLine();
            reader.close();
            LoadConfigReport report = loadFile(lastConfigFilename);
            if (report.isProblem())
            {
                report.setMainMessage(previousConfigError);
            }
            return report;
        }
        finally
        {
            if (reader != null)
            {
                try
                {
                    reader.close();
                }
                catch (Exception e)
                {
                    logger.error(e.toString(), e);
                }
            }
        }
    }

    /**
     * Set a value only if it isn't already set, optionally overriding if specified
     * 
     * @param key
     * @param value
     * @param override
     */
    private void setPropertyOverride(final String key, final String value, final boolean override)
    {
        final boolean valueExists = getProperty(key) != null && !getProperty(key).isEmpty();
        if (override || !valueExists)
        {
            setProperty(key, value);
        }
    }

    /**
     * Load a default configuration, for if something goes wrong, or if no previously used configuration file is stored
     * 
     * @param override
     *            Whether to override values should they already exist
     */
    public void loadDefaultValues(boolean override)
    {
        logger.trace("Loading default values");

        final String trueString = Boolean.toString(true);
        final String falseString = Boolean.toString(false);

        setPropertyOverride(KEY_IRC_HOST, "irc.twitch.tv", override);
        setPropertyOverride(KEY_IRC_PORT, Integer.toString(6667), override);
        setPropertyOverride(KEY_IRC_ANON, trueString, override);
        setPropertyOverride(KEY_IRC_AUTO_RECONNECT, trueString, override);

        setPropertyOverride(KEY_FONT_FILE_BORDER, ConfigFont.INTERNAL_FILE_PREFIX + "borders/dw3_border.png", override);
        setPropertyOverride(KEY_FONT_FILE_FONT, ConfigFont.INTERNAL_FILE_PREFIX + "fonts/dw3_font.png", override);
        setPropertyOverride(KEY_FONT_TYPE, FontType.FIXED_WIDTH.name(), override);
        setPropertyOverride(KEY_FONT_GRID_WIDTH, Integer.toString(8), override);
        setPropertyOverride(KEY_FONT_GRID_HEIGHT, Integer.toString(12), override);
        setPropertyOverride(KEY_FONT_SCALE, Integer.toString(2), override);
        setPropertyOverride(KEY_FONT_BORDER_SCALE, Integer.toString(3), override);
        setPropertyOverride(KEY_FONT_BORDER_INSET_X, Integer.toString(1), override);
        setPropertyOverride(KEY_FONT_BORDER_INSET_Y, Integer.toString(1), override);
        setPropertyOverride(KEY_FONT_SPACE_WIDTH, Integer.toString(25), override);
        setPropertyOverride(KEY_FONT_BASELINE_OFFSET, Integer.toString(0), override);
        setPropertyOverride(KEY_FONT_CHARACTERS, SpriteFont.NORMAL_ASCII_KEY, override);
        setPropertyOverride(KEY_FONT_UNKNOWN_CHAR, Character.toString((char) 127), override);
        setPropertyOverride(KEY_FONT_EXTENDED_CHAR, trueString, override);
        setPropertyOverride(KEY_FONT_SPACING_LINE, Integer.toString(2), override);
        setPropertyOverride(KEY_FONT_SPACING_CHAR, Integer.toString(0), override);
        setPropertyOverride(KEY_FONT_SPACING_MESSAGE, Integer.toString(0), override);

        setPropertyOverride(KEY_CHAT_SCROLL, falseString, override);
        setPropertyOverride(KEY_CHAT_RESIZABLE, trueString, override);
        setPropertyOverride(KEY_CHAT_POSITION, falseString, override);
        setPropertyOverride(KEY_CHAT_POSITION_X, Integer.toString(0), override);
        setPropertyOverride(KEY_CHAT_POSITION_Y, Integer.toString(0), override);
        setPropertyOverride(KEY_CHAT_FROM_BOTTOM, falseString, override);
        setPropertyOverride(KEY_CHAT_WIDTH, Integer.toString(550), override);
        setPropertyOverride(KEY_CHAT_HEIGHT, Integer.toString(450), override);
        setPropertyOverride(KEY_CHAT_CHROMA_ENABLED, falseString, override);
        setPropertyOverride(KEY_CHAT_INVERT_CHROMA, falseString, override);
        setPropertyOverride(KEY_CHAT_REVERSE_SCROLLING, falseString, override);
        setPropertyOverride(KEY_CHAT_CHROMA_LEFT, Integer.toString(10), override);
        setPropertyOverride(KEY_CHAT_CHROMA_TOP, Integer.toString(10), override);
        setPropertyOverride(KEY_CHAT_CHROMA_RIGHT, Integer.toString(10), override);
        setPropertyOverride(KEY_CHAT_CHROMA_BOTTOM, Integer.toString(10), override);
        setPropertyOverride(KEY_CHAT_CHROMA_CORNER, Integer.toString(10), override);
        setPropertyOverride(KEY_CHAT_ALWAYS_ON_TOP, falseString, override);
        setPropertyOverride(KEY_CHAT_ANTIALIAS, falseString, override);

        setPropertyOverride(KEY_COLOR_BG, "000000", override);
        setPropertyOverride(KEY_COLOR_FG, "FFFFFF", override);
        setPropertyOverride(KEY_COLOR_BORDER, "FFFFFF", override);
        setPropertyOverride(KEY_COLOR_HIGHLIGHT, "6699FF", override);
        setPropertyOverride(KEY_COLOR_PALETTE, "F7977A,FDC68A,FFF79A,A2D39C,6ECFF6,A187BE,F6989D", override);
        setPropertyOverride(KEY_COLOR_CHROMA_KEY, "00FF00", override);
        setPropertyOverride(KEY_COLOR_USERNAME, trueString, override);
        setPropertyOverride(KEY_COLOR_TIMESTAMP, falseString, override);
        setPropertyOverride(KEY_COLOR_MESSAGE, falseString, override);
        setPropertyOverride(KEY_COLOR_JOIN, falseString, override);
        setPropertyOverride(KEY_COLOR_TWITCH, falseString, override);

        setPropertyOverride(KEY_MESSAGE_JOIN, falseString, override);
        setPropertyOverride(KEY_MESSAGE_USERNAME, trueString, override);
        setPropertyOverride(KEY_MESSAGE_TIMESTAMP, falseString, override);
        setPropertyOverride(KEY_MESSAGE_USERFORMAT, ConfigMessage.USERNAME_REPLACE, override);
        setPropertyOverride(KEY_MESSAGE_TIMEFORMAT, "[HH:mm:ss]", override);
        setPropertyOverride(KEY_MESSAGE_CONTENT_BREAK, ConfigMessage.DEFAULT_CONTENT_BREAKER, override);
        setPropertyOverride(KEY_MESSAGE_QUEUE_SIZE, Integer.toString(64), override);
        setPropertyOverride(KEY_MESSAGE_SPEED, Integer.toString((int) (ConfigMessage.MAX_MESSAGE_SPEED * 0.25f)), override);
        setPropertyOverride(KEY_MESSAGE_EXPIRATION_TIME, Integer.toString(0), override);
        setPropertyOverride(KEY_MESSAGE_HIDE_EMPTY_BORDER, falseString, override);
        setPropertyOverride(KEY_MESSAGE_HIDE_EMPTY_BACKGROUND, falseString, override);
        setPropertyOverride(KEY_MESSAGE_CASE_TYPE, UsernameCaseResolutionType.NONE.name(), override);
        setPropertyOverride(KEY_MESSAGE_CASE_SPECIFY, falseString, override);
        setPropertyOverride(KEY_MESSAGE_CASING, MessageCasing.MIXED_CASE.name(), override);

        setPropertyOverride(KEY_EMOJI_ENABLED, trueString, override);
        setPropertyOverride(KEY_EMOJI_ANIMATION, falseString, override);
        setPropertyOverride(KEY_EMOJI_TWITCH_BADGES, trueString, override);
        setPropertyOverride(KEY_EMOJI_FFZ_BADGES, falseString, override);
        setPropertyOverride(KEY_EMOJI_SCALE_TO_LINE, trueString, override);
        setPropertyOverride(KEY_EMOJI_BADGE_SCALE_TO_LINE, falseString, override);
        setPropertyOverride(KEY_EMOJI_BADGE_HEIGHT_OFFSET, Integer.toString(0), override);
        setPropertyOverride(KEY_EMOJI_SCALE, Integer.toString(100), override);
        setPropertyOverride(KEY_EMOJI_BADGE_SCALE, Integer.toString(100), override);
        setPropertyOverride(KEY_EMOJI_DISPLAY_STRAT, EmojiLoadingDisplayStragegy.SPACE.name(), override);
        setPropertyOverride(KEY_EMOJI_TWITCH_ENABLE, trueString, override);
        setPropertyOverride(KEY_EMOJI_TWITCH_CACHE, falseString, override);
        setPropertyOverride(KEY_EMOJI_FFZ_ENABLE, falseString, override);
        setPropertyOverride(KEY_EMOJI_FFZ_CACHE, falseString, override);
        setPropertyOverride(KEY_EMOJI_BTTV_ENABLE, falseString, override);
        setPropertyOverride(KEY_EMOJI_BTTV_CACHE, falseString, override);
        setPropertyOverride(KEY_EMOJI_TWITTER_ENABLE, trueString, override);

        setPropertyOverride(KEY_CENSOR_ENABLED, trueString, override);
        setPropertyOverride(KEY_CENSOR_PURGE_ON_TWITCH_BAN, trueString, override);
        setPropertyOverride(KEY_CENSOR_URL, falseString, override);
        setPropertyOverride(KEY_CENSOR_FIRST_URL, falseString, override);
        setPropertyOverride(KEY_CENSOR_UNKNOWN_CHARS, falseString, override);
        setPropertyOverride(KEY_CENSOR_UNKNOWN_CHARS_PERCENT, Integer.toString(20), override);
        setPropertyOverride(KEY_CENSOR_WHITE, "", override);
        setPropertyOverride(KEY_CENSOR_BLACK, "", override);
        setPropertyOverride(KEY_CENSOR_BANNED, "", override);

        loadConfigs(true);
    }

    private FontificatorProperties getCopy()
    {
        FontificatorProperties copy = new FontificatorProperties();
        for (String key : stringPropertyNames())
        {
            copy.setProperty(key, getProperty(key));
        }
        return copy;
    }

    /**
     * Load properties into specialized config objects. This method returns its own report.
     * 
     * @return success
     */
    private LoadConfigReport loadConfigs(boolean loadNonFontConfig)
    {
        LoadConfigReport report = new LoadConfigReport();
        if (loadNonFontConfig)
        {
            ircConfig.load(this, report);
            chatConfig.load(this, report);
            messageConfig.load(this, report);
            emojiConfig.load(this, report);
            censorConfig.load(this, report);
        }
        fontConfig.load(this, report);
        colorConfig.load(this, report);

        if (!report.isErrorFree())
        {
            if (report.isOnlyMissingKeys())
            {
                report.setMainMessage("<center>Please resave the configuration file<br />to fix these issues:</center>");
            }
            ChatWindow.popup.handleProblem(report);
        }

        return report;
    }

    private boolean hasUnsavedChanges()
    {
        return !equals(lastSavedCopy);
    }

    @Override
    public int hashCode()
    {
        final int prime = 31;
        int result = super.hashCode();
        result = prime * result + ((chatConfig == null) ? 0 : chatConfig.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj)
    {
        if (this == obj)
            return true;
        if (!super.equals(obj))
            return false;
        if (getClass() != obj.getClass())
            return false;

        FontificatorProperties otherFp = (FontificatorProperties) obj;

        return FontificatorProperties.propertyBatchMatch(getKeysForEqualsTest(this), this, otherFp);
    }

    private static boolean propertyBatchMatch(String[][] keys, FontificatorProperties a, FontificatorProperties b)
    {
        for (int i = 0; i < keys.length; i++)
        {
            for (int j = 0; j < keys[i].length; j++)
            {
                if (!propertyEquals(a.getProperty(keys[i][j]), b.getProperty(keys[i][j])))
                {
                    return false;
                }
            }
        }
        return true;
    }

    private static boolean propertyEquals(String one, String two)
    {
        if (one == null)
        {
            return two == null;
        }
        else
        {
            return one.equals(two);
        }
    }

} public class LoadConfigReport
{
    private static final int MAX_NUMBER_OF_NON_PROBLEMS = 15;

    private String mainMessage;

    /**
     * A set of types of errors that have been collected in this report
     */
    private Set<LoadConfigErrorType> types;

    /**
     * Human-readable messages describing the errors collected so far
     */
    private List<String> messages;

    /**
     * Instantiates an empty report
     */
    public LoadConfigReport()
    {
        types = new HashSet<LoadConfigErrorType>();
        messages = new ArrayList<String>();
    }

    /**
     * Instantiate this report with only a type, when there is no need for human-readable explanations
     * 
     * @param type
     */
    public LoadConfigReport(LoadConfigErrorType type)
    {
        this();
        types.add(type);
    }

    /**
     * Add an error to this report
     * 
     * @param message
     * @param type
     */
    public void addError(String message, LoadConfigErrorType type)
    {
        messages.add(message);
        types.add(type);
    }

    /**
     * Whether no errors have been reported, whether they are problems or not. This check identifies the load as being capable of handling any subsequent work, no need to supplement the results with defaults.
     * 
     * @return error free
     */
    public boolean isErrorFree()
    {
        return messages.isEmpty();
    }

    /**
     * Get the opposite of whether there is a problem
     * 
     * @return success
     */
    public boolean isSuccess()
    {
        return !isProblem();
    }

    /**
     * Get whether there is a problem. A problem is whenever at least one of the types of errors is marked as a problem-type error.
     * 
     * @return problem
     */
    public boolean isProblem()
    {
        for (LoadConfigErrorType result : types)
        {
            if (result.isProblem())
            {
                return true;
            }
        }
        // No real problems exist, but if there are more than the max number allowed, consider it a problem. For
        // example, someone might be loading a random text file as a config file that has nothing to do with this
        // program. That should be a problem, even though it will register as merely having a bunch of missing values
        return messages.size() > MAX_NUMBER_OF_NON_PROBLEMS;
    }

    /**
     * Get whether all the load problems are just missing keys
     * 
     * @return whether all errors are missing keys
     */
    public boolean isOnlyMissingKeys()
    {
        for (LoadConfigErrorType result : types)
        {
            if (result != LoadConfigErrorType.MISSING_KEY)
            {
                return false;
            }
        }
        return true;
    }

    /**
     * Get the messages
     * 
     * @return messages
     */
    public List<String> getMessages()
    {
        return messages;
    }

    /**
     * Get the types
     * 
     * @return types
     */
    public Set<LoadConfigErrorType> getTypes()
    {
        return types;
    }

    /**
     * Adds all the contents of the specified other report to this report
     * 
     * @param otherReport
     */
    public void addFromReport(LoadConfigReport otherReport)
    {
        for (String msg : otherReport.getMessages())
        {
            messages.add(msg);
        }
        for (LoadConfigErrorType rslt : otherReport.getTypes())
        {
            types.add(rslt);
        }
    }

    public String getMainMessage()
    {
        return mainMessage;
    }

    public void setMainMessage(String mainMessage)
    {
        this.mainMessage = mainMessage;
    }
} public class AssetIndexLoader
{
    private static final Logger logger = Logger.getLogger(AssetIndexLoader.class);

    private static final String INDEX_FONTS_FILENAME = "index_fonts.txt";

    private static final String INDEX_BORDERS_FILENAME = "index_borders.txt";

    private static final String INDEX_PRESETS_FILENAME = "index_presets.txt";

    private static final String PRESET_DIRECTORY = "presets/";

    /**
     * Loads all the font index values into a map
     * 
     * @return
     */
    public static Map<DropdownLabel, DropdownFont> loadFonts()
    {
        Map<DropdownLabel, DropdownFont> fontMap = new LinkedHashMap<DropdownLabel, DropdownFont>();
        fontMap.put(ControlPanelFont.CUSTOM_KEY, null);
        String errorLine = null;
        try
        {
            List<String> fontLines = loadIndexFile(INDEX_FONTS_FILENAME);
            for (String line : fontLines)
            {
                errorLine = line;
                if (line.trim().startsWith("#"))
                {
                    continue;
                }
                String[] splitLine = line.split(",");
                final String category = splitLine[0].trim();
                final String name = splitLine[1].trim();
                final String filename = splitLine[2].trim();
                final FontType type = FontType.valueOf(splitLine[3].trim());
                fontMap.put(new DropdownLabel(category, name), new DropdownFont(filename, type));
            }
        }
        catch (Exception e)
        {
            logger.error("Error loading font index" + (errorLine == null ? "" : "\n" + errorLine), e);
        }
        return fontMap;
    }

    /**
     * Load all the border index values into a map
     * 
     * @return
     */
    public static Map<DropdownLabel, DropdownBorder> loadBorders()
    {
        Map<DropdownLabel, DropdownBorder> borderMap = new LinkedHashMap<DropdownLabel, DropdownBorder>();
        borderMap.put(ControlPanelFont.CUSTOM_KEY, null);
        String errorLine = null;
        try
        {
            List<String> borderLines = loadIndexFile(INDEX_BORDERS_FILENAME);
            for (String line : borderLines)
            {
                errorLine = line;
                if (line.trim().startsWith("#"))
                {
                    continue;
                }
                String[] splitLine = line.split(",");
                final String category = splitLine[0].trim();
                final String name = splitLine[1].trim();
                final String filename = splitLine[2].trim();
                final int color = Integer.parseInt(splitLine[3].trim(), 16);
                borderMap.put(new DropdownLabel(category, name), new DropdownBorder(filename, color));
            }
        }
        catch (Exception e)
        {
            logger.error("Error loading border index" + (errorLine == null ? "" : "\n" + errorLine), e);
        }
        return borderMap;
    }

    public static Map<String, List<String[]>> loadPresets()
    {
        final Map<String, List<String[]>> presetMapSubmenuToItem = new LinkedHashMap<String, List<String[]>>();

        String errorLine = null;
        try
        {
            List<String> presetLines = loadIndexFile(INDEX_PRESETS_FILENAME);
            for (String line : presetLines)
            {
                errorLine = line;
                if (line.trim().startsWith("#"))
                {
                    continue;
                }
                String[] splitLine = line.split(",");
                final String category = splitLine[0].trim();
                final String item = splitLine[1].trim();
                final String filename = splitLine[2].trim();
                List<String[]> catArray = presetMapSubmenuToItem.get(category);
                if (catArray == null)
                {
                    catArray = new ArrayList<String[]>();
                    presetMapSubmenuToItem.put(category, catArray);
                }
                catArray.add(new String[] { item, ConfigFont.INTERNAL_FILE_PREFIX + PRESET_DIRECTORY + filename });
            }
        }
        catch (Exception e)
        {
            logger.error("Error loading preset index" + (errorLine == null ? "" : "\n" + errorLine), e);
        }

        return presetMapSubmenuToItem;
    }

    private static List<String> loadIndexFile(String indexFilename) throws IOException
    {
        InputStream is = AssetIndexLoader.class.getClassLoader().getResourceAsStream(indexFilename);
        BufferedReader reader = new BufferedReader(new InputStreamReader(is));
        List<String> lines = new ArrayList<String>();
        String line;
        while ((line = reader.readLine()) != null)
        {
            lines.add(line);
        }
        return lines;
    }

} public class ChatWindow extends JFrame
{
    private static final long serialVersionUID = 1L;

    /**
     * A stored reference to the chat panel, used whenever some part of the system has a reference to this ChatWindow,
     * but needs to modify something on the panel. This is in lieu of putting a bunch of accessor methods here to do
     * pass through the function to the panel in an encapsulated way.
     */
    private ChatPanel chatPanel;

    /**
     * A static copy of this ChatWindow for accessing it globally
     */
    public static ChatWindow me;

    /**
     * The popup for submitting errors that the user needs to see
     */
    public static FontificatorError popup;

    /**
     * Mouse listeners for dragging the Chat Window around when dragging the mouse inside the chat
     */
    private ChatMouseListeners mouseListeners;

    /**
     * Escape stroke to close popups
     */
    private static final KeyStroke escapeStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);

    /**
     * Construct the Chat Window
     */
    public ChatWindow()
    {
        me = this;
        setTitle("Fontificator Chat");
        popup = new FontificatorError(null);
    }

    /**
     * Sets the properties to get hooks into the properties' configuration models; Sets the ControlWindow to get hooks
     * back into the controls; Sets the loaded member Boolean to indicate it has everything it needs to begin rendering
     * the visualization
     * 
     * @param fProps
     * @param ctrlWindow
     * @throws IOException
     */
    public void initChat(final FontificatorProperties fProps, final ControlWindow ctrlWindow) throws IOException
    {
        chatPanel = new ChatPanel();
        add(chatPanel);

        mouseListeners = new ChatMouseListeners(this, ctrlWindow);
        addMouseListener(mouseListeners);
        addMouseMotionListener(mouseListeners);
        addMouseWheelListener(chatPanel);
        setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);

        setChatSize(fProps.getChatConfig());

        setResizable(fProps.getChatConfig().isResizable());
        setAlwaysOnTop(fProps.getChatConfig().isAlwaysOnTop());

        chatPanel.setConfig(fProps);
        chatPanel.initExpirationTimer();

        addWindowListener(new WindowListener()
        {
            @Override
            public void windowOpened(WindowEvent e)
            {
            }

            @Override
            public void windowClosing(WindowEvent e)
            {
                callExit(e.getComponent());
            }

            @Override
            public void windowClosed(WindowEvent e)
            {
            }

            @Override
            public void windowIconified(WindowEvent e)
            {
            }

            @Override
            public void windowDeiconified(WindowEvent e)
            {
            }

            @Override
            public void windowActivated(WindowEvent e)
            {
            }

            @Override
            public void windowDeactivated(WindowEvent e)
            {
            }

            /**
             * Calls exit from the control window
             */
            private void callExit(Component caller)
            {
                ctrlWindow.attemptToExit(caller);
            }
        });

    }

    /**
     * Handles sizing the Chat Window, clearing out old JFrame-based config values in the ConfigChat model if they were
     * the only ones available
     * 
     * @param chatConfig
     */
    public void setChatSize(ConfigChat chatConfig)
    {
        if (chatConfig.getWindowWidth() != null && chatConfig.getWindowHeight() != null)
        {
            setSize(chatConfig.getWindowWidth(), chatConfig.getWindowHeight());
            chatConfig.setWidth(getContentPane().getWidth());
            chatConfig.setHeight(getContentPane().getHeight());
            chatConfig.clearLegacyWindowSize();
        }
        else if (chatConfig.getWidth() > 0 && chatConfig.getHeight() > 0)
        {
            getContentPane().setPreferredSize(new Dimension(chatConfig.getWidth(), chatConfig.getHeight()));
            getContentPane().setSize(chatConfig.getWidth(), chatConfig.getHeight());
            pack();
        }
    }

    /**
     * Does the work required to make the parameter JDialog be hidden when pressing escape
     * 
     * @param popup
     */
    public static void setupHideOnEscape(final JDialog popup)
    {
        Action aa = new AbstractAction()
        {
            private static final long serialVersionUID = 1L;

            public void actionPerformed(ActionEvent event)
            {
                popup.setVisible(false);
            }
        };
        final String mapKey = "escapePressed";
        JRootPane root = popup.getRootPane();
        root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(escapeStroke, mapKey);
        root.getActionMap().put(mapKey, aa);
    }

    /**
     * Access to the chat panel through this window, for any places there is a reference to just the ChatWindow, but
     * that needs to affect the ChatPanel, which has all the actual chat options
     * 
     * @return chatPanel
     */
    public ChatPanel getChatPanel()
    {
        return chatPanel;
    }

    /**
     * This is a small hack to expose the ctrl window to the message control panel so its username case map can be
     * cleared out if the user changes the type of username case resolution
     */
    public void clearUsernameCases()
    {
        mouseListeners.clearUsernameCases();
    }

} public class LabeledInput extends JPanel
{
    private static final long serialVersionUID = 1L;

    private JLabel label;

    private JTextField input;

    private boolean isPassword;

    private final static int DEFAULT_FIELD_SIZE = 6;

    public LabeledInput(String label)
    {
        this(label, DEFAULT_FIELD_SIZE);
    }

    public LabeledInput(String label, int size)
    {
        this(label, false, size);
    }

    public LabeledInput(String label, boolean isPassword, int size)
    {
        this(label, isPassword, isPassword ? new JPasswordField(size) : new JTextField(size));
    }

    private LabeledInput(String label, boolean isPassword, JTextField input)
    {
        this.isPassword = isPassword;
        this.label = new JLabel(label);
        this.input = input;

        setLayout(new GridBagLayout());

        GridBagConstraints gbc = new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, new Insets(2, 4, 2, 4), 0, 0);
        add(this.label, gbc);
        gbc.gridx++;
        gbc.weightx = 1.0;
        gbc.anchor = GridBagConstraints.WEST;
        add(this.input, gbc);
    }

    @Override
    public void setEnabled(boolean enabled)
    {
        input.setEnabled(enabled);
        label.setEnabled(enabled);
    }

    public String getLabel()
    {
        return label.getText();
    }

    public String getText()
    {
        if (isPassword)
        {
            return String.valueOf(((JPasswordField) input).getPassword());
        }
        else
        {
            return input.getText();
        }
    }

    public void setText(String text)
    {
        input.setText(text);
    }

    public void addActionListener(ActionListener al)
    {
        input.addActionListener(al);
    }

    public void addDocumentListener(DocumentListener dl)
    {
        input.getDocument().addDocumentListener(dl);
    }

    @Override
    public void addFocusListener(FocusListener fl)
    {
        input.addFocusListener(fl);
    }

} public class LabeledSlider extends JPanel
{
    private static final long serialVersionUID = 1L;

    private static final float NO_SCALE = 1.0f;

    protected JSlider slider;

    private JLabel label;

    private String unitLabelStr;

    private JLabel unitLabel;

    private int maxValueDigits;

    private float scale;

    public LabeledSlider(String labelStr, String unitLabelStr, int min, int max)
    {
        this(labelStr, unitLabelStr, min, max, Math.max(Integer.toString(min).length(), Integer.toString(max).length()));
    }

    public LabeledSlider(String labelStr, String unitLabelStr, int min, int max, float scale)
    {
        this(labelStr, unitLabelStr, min, max, Math.max(Integer.toString(min).length(), Integer.toString(max).length()), scale);
    }

    public LabeledSlider(String labelStr, String unitLabelStr, int min, int max, int maxValueDigits)
    {
        this(labelStr, unitLabelStr, min, max, min, maxValueDigits);
    }

    public LabeledSlider(String labelStr, String unitLabelStr, int min, int max, int maxValueDigits, float scale)
    {
        this(labelStr, unitLabelStr, min, max, min, maxValueDigits, scale);
    }

    public LabeledSlider(String labelStr, String unitLabelStr, int min, int max, int value, int maxValueDigits)
    {
        this(labelStr, unitLabelStr, min, max, value, maxValueDigits, NO_SCALE);
    }

    public LabeledSlider(String labelStr, String unitLabelStr, int min, int max, int value, int maxValueDigits, float scale)
    {
        this.scale = scale;
        this.unitLabelStr = unitLabelStr;
        this.slider = new JSlider(JSlider.HORIZONTAL, min, max, value);
        this.label = new JLabel(labelStr);
        this.unitLabel = new JLabel(slider.getValue() + " " + unitLabelStr);
        this.maxValueDigits = maxValueDigits;
        setUnitLabel();
        slider.addChangeListener(new ChangeListener()
        {
            public void stateChanged(ChangeEvent e)
            {
                setUnitLabel();
            }
        });
        addComponents();
    }

    protected void addComponents()
    {
        setLayout(new GridBagLayout());
        GridBagConstraints gbc = new GridBagConstraints(0, 0, 1, 1, 1.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0);

        gbc.weightx = 0.0;
        gbc.weighty = 0.0;
        gbc.anchor = GridBagConstraints.EAST;
        gbc.gridx = 0;
        gbc.gridy = 0;

        add(label, gbc);

        gbc.weightx = 1.0;
        gbc.anchor = GridBagConstraints.CENTER;
        gbc.gridx++;

        gbc.fill = GridBagConstraints.HORIZONTAL;
        add(slider, gbc);
        gbc.fill = GridBagConstraints.NONE;

        gbc.weightx = 0.0;
        gbc.anchor = GridBagConstraints.WEST;
        gbc.gridx++;

        add(unitLabel, gbc);
    }

    public void setEnabled(boolean enabled)
    {
        slider.setEnabled(enabled);
        label.setEnabled(enabled);
        unitLabel.setEnabled(enabled);
    }

    public int getValue()
    {
        return slider.getValue();
    }

    public float getScaledValue()
    {
        return getValue() * scale;
    }

    public String getValueString()
    {
        if (scale == NO_SCALE)
        {
            return Integer.toString(getValue());
        }
        else
        {
            return String.format("%.2f", getScaledValue());
        }
    }

    protected void setUnitLabel()
    {
        unitLabel.setText("<html><nobr><tt><b>" + padValue(getValueString()) + " " + getUnitLabelStr() + "</b></tt></nobr></html>");
    }

    private String padValue(String valStr)
    {
        return padValue(valStr, maxValueDigits);
    }

    protected String padValue(String valStr, int length)
    {
        while (valStr.length() < length)
        {
            valStr = " " + valStr;
        }
        valStr = valStr.replaceAll(" ", "&nbsp;");
        return valStr;
    }

    public void addChangeListener(ChangeListener cl)
    {
        slider.addChangeListener(cl);
    }

    public void setValue(int value)
    {
        slider.setValue(value);
    }

    public void setScaledValue(float value)
    {
        setValue((int) (value / scale));
    }

    public JSlider getSlider()
    {
        return slider;
    }

    public void setValueTextColor(Color textColor)
    {
        unitLabel.setForeground(textColor);
    }

    protected String getUnitLabelStr()
    {
        return unitLabelStr;
    }
} public class ComboMenuBar extends JMenuBar
{
    private static final long serialVersionUID = 1L;

    private JMenu mainMenu;

    private MenuScrollItem upItem;

    private MenuScrollItem downItem;

    public static final int SCROLL_COUNT = 12;

    private int firstIndex = 0;

    private Dimension preferredSize;

    private HintTextField filterInput;

    private class MenuVisibilityStatus
    {
        private boolean filtered;

        private boolean outOfScrollBounds;

        public void setFiltered(boolean filtered)
        {
            this.filtered = filtered;
        }

        public void setOutOfScrollBounds(boolean outOfScrollBounds)
        {
            this.outOfScrollBounds = outOfScrollBounds;
        }

        public boolean isFiltered()
        {
            return filtered;
        }

        public boolean isOutOfScrollBounds()
        {
            return outOfScrollBounds;
        }

        public void reset()
        {
            filtered = false;
            outOfScrollBounds = false;
        }

        public String toString()
        {
            return "Filtered=" + filtered + "; OutOfScrollBounds=" + outOfScrollBounds;
        }
    }

    /**
     * All the folders containing actual selectable menu items. These are not GameFontMenuItem objects.
     */
    private Map<JMenuItem, MenuVisibilityStatus> allMenuItems;
    private Map<JMenu, MenuVisibilityStatus> allMenuFolders;

    public HintTextField getFilter()
    {
        return filterInput;
    }

    public ComboMenuBar(Map<String, List<String>> menuTextMap, ActionListener al)
    {
        setPreferredSize(new Dimension(240, 24));
        filterInput = new HintTextField("Filter", 7);
        filterInput.getDocument().addDocumentListener(new DocumentListener()
        {
            @Override
            public void insertUpdate(DocumentEvent e)
            {
                applyFilter(filterInput.getText());
            }

            @Override
            public void removeUpdate(DocumentEvent e)
            {
                applyFilter(filterInput.getText());
            }

            @Override
            public void changedUpdate(DocumentEvent e)
            {
                applyFilter(filterInput.getText());
            }
        });

        Collection<String> menuLabels = menuTextMap.keySet();

        allMenuItems = new LinkedHashMap<JMenuItem, MenuVisibilityStatus>();
        allMenuFolders = new LinkedHashMap<JMenu, MenuVisibilityStatus>();

        /*
         * rootItems is the list of items on the root of the menu along with all the 'allMenuFolders' menus. Essentially
         * just the Custom... item
         */
        List<JMenuItem> rootItems = new ArrayList<JMenuItem>();

        for (String label : menuLabels)
        {
            MenuVisibilityStatus status = new MenuVisibilityStatus();
            // Check if there is no submenu and add that as a root item
            if (menuTextMap.get(label) == null)
            {
                JMenuItem item = new JMenuItem(label);
                // Custom
                item.addActionListener(al);
                rootItems.add(item);
                allMenuItems.put(item, status);
            }
            else
            {
                JMenu item = new JMenu(label);
                allMenuFolders.put(item, status);
                allMenuItems.put(item, status);
            }

        }

        JMenu menu = new ComboMenu(); // The top-level menu

        /*
         * This populates the menu with all the submenus, and also fills each submenu with the selectable items
         */
        for (JMenuItem m : allMenuItems.keySet())
        {
            List<String> submenuText = menuTextMap.get(m.getText());
            if (submenuText != null)
            {
                for (String submenuLabel : submenuText)
                {
                    JMenuItem item = new GameFontMenuItem(submenuLabel);
                    item.addActionListener(al);
                    m.add(item);
                }
                menu.add(m);
            }
        }

        for (JMenuItem item : rootItems)
        {
            menu.add(item);
        }

        setup(menu);
        setupScroll(menu);
        this.mainMenu = menu;
    }

    private void setup(final JMenu menu)
    {
        Color color = UIManager.getColor("Menu.selectionBackground");
        UIManager.put("Menu.selectionBackground", UIManager.getColor("Menu.background"));
        menu.updateUI();
        UIManager.put("Menu.selectionBackground", color);

        ActionListener listener = new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                JMenuItem item = (JMenuItem) e.getSource();
                menu.setText(item.getText());
                menu.requestFocus();
            }
        };
        setListener(menu, listener);

        add(menu);
    }

    private void setupScroll(final JMenu menu)
    {
        upItem = new MenuScrollItem(this, MenuIcon.UP);
        downItem = new MenuScrollItem(this, MenuIcon.DOWN);

        PopupMenuListener menuListener = new PopupMenuListener()
        {
            @Override
            public void popupMenuWillBecomeVisible(PopupMenuEvent e)
            {
                updateScroll();
            }

            @Override
            public void popupMenuWillBecomeInvisible(PopupMenuEvent e)
            {
            }

            @Override
            public void popupMenuCanceled(PopupMenuEvent e)
            {
            }
        };

        MenuSelectionManager.defaultManager().clearSelectedPath();
        menu.getPopupMenu().addPopupMenuListener(menuListener);
    }

    private void setListener(JMenuItem item, ActionListener listener)
    {
        if (item instanceof JMenu)
        {
            JMenu menu = (JMenu) item;
            int n = menu.getItemCount();
            for (int i = 0; i < n; i++)
            {
                setListener(menu.getItem(i), listener);
            }
        }
        else if (item != null)
        {
            item.addActionListener(listener);
        }
    }

    private void applyFilter(String filterText)
    {
        resetMenu();

        for (JMenu menuFolder : allMenuFolders.keySet())
        {
            boolean atLeastOneHit = false;
            boolean matchesMenu = menuFolder.getText().toLowerCase().contains(filterText.toLowerCase().trim());

            for (int i = 0; i < menuFolder.getItemCount(); i++)
            {
                GameFontMenuItem gfmi = (GameFontMenuItem) menuFolder.getItem(i);
                final boolean matchesGame = gfmi.isMatchingFilterGame(filterText);
                final boolean matchesGenre = gfmi.isMatchingFilterGenre(filterText);
                final boolean matchesSystem = gfmi.isMatchingFilterSystem(filterText);

                boolean hit = matchesMenu || matchesGame || matchesGenre || matchesSystem;

                gfmi.setVisible(hit);
                if (hit)
                {
                    atLeastOneHit = true;
                }
            }
            allMenuItems.get(menuFolder).setFiltered(!atLeastOneHit);
        }

        setMenuItemFilterVisibility();
    }

    private void setMenuItemFilterVisibility()
    {
        for (JMenuItem menuFolder : allMenuItems.keySet())
        {
            final boolean menuFolderVisibility = !allMenuItems.get(menuFolder).isFiltered() && !menuFolder.getText().isEmpty();
            menuFolder.setVisible(menuFolderVisibility);
        }
    }

    private List<JMenuItem> getInBoundScrollMenuFolders()
    {
        List<JMenuItem> menuItemKeyList = new ArrayList<JMenuItem>();
        for (JMenuItem menuFolder : allMenuItems.keySet())
        {
            if (!allMenuItems.get(menuFolder).isOutOfScrollBounds())
            {
                menuItemKeyList.add(menuFolder);
            }
        }
        return menuItemKeyList;
    }

    public void updateScroll()
    {
        List<JMenuItem> menuKeyList = getKeyList();

        // Keep the first index less than the total number available in the list (unfiltered) minus the length of the scroll count, and no lower than zero
        firstIndex = Math.min(firstIndex, menuKeyList.size() - SCROLL_COUNT);
        firstIndex = Math.max(firstIndex, 0);
        int lastIndex = firstIndex + Math.min(menuKeyList.size(), SCROLL_COUNT);

        int runningUnfilteredCount = 0;
        for (int i = 0; i < menuKeyList.size(); i++)
        {
            JMenuItem jmi = menuKeyList.get(i);
            MenuVisibilityStatus status = allMenuItems.get(jmi);
            final boolean inScrollBounds = !status.isFiltered() && runningUnfilteredCount >= firstIndex && runningUnfilteredCount < lastIndex;
            status.setOutOfScrollBounds(!inScrollBounds);
            if (!status.isFiltered())
            {
                runningUnfilteredCount++;
            }
        }

        mainMenu.getPopupMenu().removeAll();
        setMenuItemFilterVisibility();

        upItem.setEnabled(firstIndex > 0);
        mainMenu.getPopupMenu().add(upItem);
        List<JMenuItem> inBoundsScrollMenuFolders = getInBoundScrollMenuFolders();

        for (JMenuItem menuFolder : inBoundsScrollMenuFolders)
        {
            mainMenu.getPopupMenu().add(menuFolder);
        }
        downItem.setEnabled(lastIndex < runningUnfilteredCount);
        mainMenu.getPopupMenu().add(downItem);

        JComponent parent = (JComponent) upItem.getParent();
        parent.revalidate();
        parent.repaint();
    }

    private void resetMenu()
    {
        firstIndex = 0;
        // Clear menu
        mainMenu.getPopupMenu().removeAll();
        // Set everything to visible and reset all map values to zero
        for (JMenuItem menuFolder : allMenuItems.keySet())
        {
            allMenuItems.get(menuFolder).reset();
            menuFolder.setVisible(true);
            if (menuFolder instanceof JMenu)
            {
                JMenu jMenuFolder = (JMenu) menuFolder;
                for (int i = 0; i < jMenuFolder.getItemCount(); i++)
                {
                    jMenuFolder.getItem(i).setVisible(true);
                }
            }
        }
        // Add all menu folders to the popup menu
        for (JMenuItem menuFolder : allMenuItems.keySet())
        {
            menuFolder.setVisible(true);
            mainMenu.getPopupMenu().add(menuFolder);
        }
    }

    public String getSelectedItem()
    {
        return mainMenu.getText();
    }

    public void setSelectedText(String label)
    {
        mainMenu.setText(label);
    }

    @Override
    public void setPreferredSize(Dimension size)
    {
        preferredSize = size;
    }

    @Override
    public Dimension getPreferredSize()
    {
        if (preferredSize == null)
        {
            Dimension menuD = getItemSize(mainMenu);
            Insets margin = mainMenu.getMargin();
            Dimension retD = new Dimension(menuD.width, margin.top + margin.bottom + menuD.height);
            mainMenu.setPreferredSize(retD);
            preferredSize = retD;
        }
        return preferredSize;
    }

    private Dimension getItemSize(JMenu menu)
    {
        Dimension d = new Dimension(0, 0);
        int n = menu.getItemCount();
        for (int i = 0; i < n; i++)
        {
            Dimension itemD;
            JMenuItem item = menu.getItem(i);
            if (item instanceof JMenu)
            {
                itemD = getItemSize((JMenu) item);
            }
            else if (item != null)
            {
                itemD = item.getPreferredSize();
            }
            else
            {
                itemD = new Dimension(0, 0);
            }
            d.width = Math.max(d.width, itemD.width);
            d.height = Math.max(d.height, itemD.height);
        }
        return d;
    }

    private List<JMenuItem> getKeyList()
    {
        List<JMenuItem> menuItemKeyList = new ArrayList<JMenuItem>();
        for (JMenuItem menuFolder : allMenuItems.keySet())
        {
            if (!menuFolder.getText().isEmpty())
            {
                menuItemKeyList.add(menuFolder);
            }
        }
        return menuItemKeyList;
    }

    public void incrementFirstIndex(int increment)
    {
        firstIndex += increment;
    }
} public class ControlWindow extends JDialog
{
    private static final Logger logger = Logger.getLogger(ControlWindow.class);

    private static final long serialVersionUID = 1L;

    private final String DEFAULT_CONFIG_FILE_EXTENSION = "cgf";

    private final String DEFAULT_SCREENSHOT_FILE_EXTENSION = "png";

    private ControlTabs controlTabs;

    private ChatViewerBot bot;

    private ChatWindow chatWindow;

    private MessageDialog messageDialog;

    private FontificatorProperties fProps;

    private JEditorPane aboutPane;

    // @formatter:off
    private static String ABOUT_CONTENTS = "<html><table bgcolor=#EEEEEE width=100% border=1><tr><td>" + 
        "<center><font face=\"Arial, Helvetica\"><b>Chat Game Fontificator</b> is a Twitch chat display that makes<br />" + 
        "the chat look like the text boxes from various video games.<br /><br />" + 
        "It is free, open source, and in the public domain to the furthest<br />" + 
        "extent I am permitted to forfeit my copyright over this software.<br /><br />" + 
        "Please enjoy!<br /><br />" + 
        "By Matt Yanos<br /><br />" + 
        "<a href=\"www.github.com/GlitchCog/ChatGameFontificator\">www.github.com/GlitchCog/ChatGameFontificator</a>" + 
        "</font></center>" + 
        "</td></tr></table></html>";
    // @formatter:on

    private String helpText;

    public static ControlWindow me;

    private JDialog help;

    private JFileChooser opener;

    private JFileChooser configSaver;

    private JFileChooser screenshotSaver;

    private ScreenshotOptions screenshotOptions;

    public ControlWindow(JFrame parent, FontificatorProperties fProps, LogBox logBox)
    {
        super(parent);

        BufferedReader br = null;
        try
        {

            InputStream is = getClass().getClassLoader().getResourceAsStream("help.html");
            br = new BufferedReader(new InputStreamReader(is));
            String line;
            StringBuilder helpBuilder = new StringBuilder();
            while ((line = br.readLine()) != null)
            {
                helpBuilder.append(line);
            }
            helpText = helpBuilder.toString();
        }
        catch (Exception e)
        {
            helpText = "Unable to load help file";
            logger.error(helpText, e);
            ChatWindow.popup.handleProblem(helpText);
        }
        finally
        {
            if (br != null)
            {
                try
                {
                    br.close();
                }
                catch (Exception e)
                {
                    logger.error(e.toString(), e);
                }
            }
        }

        ControlWindow.me = this;

        setTitle("Fontificator Configuration");
        setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);

        this.fProps = fProps;

        FileFilter cgfFileFilter = new FileNameExtensionFilter("Chat Game Fontificator Configuration (*." + DEFAULT_CONFIG_FILE_EXTENSION + ")", DEFAULT_CONFIG_FILE_EXTENSION.toLowerCase());
        FileFilter pngFileFilter = new FileNameExtensionFilter("PNG Image (*." + DEFAULT_SCREENSHOT_FILE_EXTENSION + ")", DEFAULT_SCREENSHOT_FILE_EXTENSION.toLowerCase());

        this.opener = new JFileChooser();
        this.opener.setFileFilter(cgfFileFilter);

        this.configSaver = new JFileChooser();
        this.configSaver.setFileFilter(cgfFileFilter);

        this.screenshotOptions = new ScreenshotOptions();

        this.screenshotSaver = new JFileChooser();
        this.screenshotSaver.setFileFilter(pngFileFilter);
        this.screenshotSaver.setAccessory(screenshotOptions);
    }

    public void loadLastData(ChatWindow chatWindow)
    {
        this.chatWindow = chatWindow;

        ChatWindow.setupHideOnEscape(this);

        LoadConfigReport report = new LoadConfigReport();

        fProps.clear();
        try
        {
            report = fProps.loadLast();
        }
        catch (Exception e)
        {
            final String errorMsg = "Unknown error loading last config file";
            logger.error(errorMsg, e);
            report.addError(errorMsg, LoadConfigErrorType.UNKNOWN_ERROR);
        }

        if (!report.isErrorFree())
        {
            final boolean overwriteExistingValues = report.isProblem();
            if (overwriteExistingValues)
            {
                fProps.forgetLastConfigFile();
            }
            fProps.loadDefaultValues(overwriteExistingValues);
        }
    }

    /**
     * Called separately from construction.
     * 
     * @param logBox
     */
    public void build(LogBox logBox)
    {
        constructAboutPopup();

        this.messageDialog = new MessageDialog(fProps, chatWindow, this, logBox);

        this.bot = new ChatViewerBot();
        this.bot.setUsername(fProps.getIrcConfig().getUsername());

        this.controlTabs = new ControlTabs(fProps, bot, messageDialog.getCensorPanel(), logBox);
        this.controlTabs.build(chatWindow, this);

        this.bot.setChatPanel(chatWindow.getChatPanel());

        setLayout(new GridLayout(1, 1));
        add(controlTabs);

        initMenus();

        // This wasn't built before the config was loaded into the chat control
        // tab, so set it here
        setAlwaysOnTopMenu(fProps.getChatConfig().isAlwaysOnTop());
        setRememberPositionMenu(fProps.getChatConfig().isRememberPosition());
        setAntiAliasMenu(fProps.getChatConfig().isAntiAlias());

        setupHelp();

        pack();
        setMinimumSize(getSize());
        setResizable(false);
    }

    private void setupHelp()
    {
        final String helpTitle = "Chat Game Fontificator Help";

        help = new JDialog(this, true);
        help.setTitle(helpTitle);
        help.setSize(640, 480);
        help.setLayout(new GridBagLayout());

        JEditorPane helpPane = new JEditorPane();
        helpPane.setContentType("text/html");
        helpPane.setText(helpText);
        helpPane.setEditable(false);
        JScrollPane scrollHelp = new JScrollPane(helpPane, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);

        JButton ok = new JButton("Close");
        ok.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                help.setVisible(false);
            }
        });

        GridBagConstraints gbc = new GridBagConstraints();

        gbc.gridy = 0;
        gbc.weightx = 1.0;
        gbc.weighty = 0.0;
        gbc.fill = GridBagConstraints.NONE;
        gbc.anchor = GridBagConstraints.NORTH;
        help.add(new JLabel("The function of each option available in the Control Window tabs is explained below"), gbc);

        gbc.fill = GridBagConstraints.BOTH;
        gbc.anchor = GridBagConstraints.CENTER;
        gbc.weightx = 1.0;
        gbc.weighty = 1.0;
        gbc.gridy = 1;
        help.add(scrollHelp, gbc);

        gbc.gridy++;
        gbc.fill = GridBagConstraints.NONE;
        gbc.anchor = GridBagConstraints.SOUTH;
        gbc.weighty = 0.0;
        help.add(ok, gbc);

        help.setResizable(false);
    }

    /**
     * Builds the menus from the static arrays
     */
    private void initMenus()
    {
        JMenuBar menuBar = new JMenuBar();

        final String[] mainMenuText = { "File", "Presets", "View", "Message", "Help" };
        final int[] mainMnomonics = { KeyEvent.VK_F, KeyEvent.VK_P, KeyEvent.VK_V, KeyEvent.VK_M, KeyEvent.VK_H };

        JMenu[] menus = new JMenu[mainMenuText.length];

        for (int i = 0; i < mainMenuText.length; i++)
        {
            menus[i] = new JMenu(mainMenuText[i]);
            menus[i].setMnemonic(mainMnomonics[i]);
        }

        /* File Menu Item Text */
        final String strFileOpen = "Open Configuration";
        final String strFileSave = "Save Configuration";
        final String strFileRestore = "Restore Default Configuration";
        final String strScreenshot = "Screenshot";
        final String strFileExit = "Exit";
        // @formatter:off
        final MenuComponent[] fileComponents = new MenuComponent[] { 
            new MenuComponent(strFileOpen, KeyEvent.VK_O, KeyStroke.getKeyStroke(KeyEvent.VK_O, Event.CTRL_MASK)), 
            new MenuComponent(strFileSave, KeyEvent.VK_S, KeyStroke.getKeyStroke(KeyEvent.VK_S, Event.CTRL_MASK)), 
            new MenuComponent(strFileRestore, KeyEvent.VK_R, null), 
            new MenuComponent(strScreenshot, KeyEvent.VK_C, KeyStroke.getKeyStroke(KeyEvent.VK_F8, 0)), 
            new MenuComponent(strFileExit, KeyEvent.VK_X, KeyStroke.getKeyStroke(KeyEvent.VK_Q, Event.CTRL_MASK)) 
        };
        // @formatter:on

        /* View Menu Item Text */
        final String strAntiAlias = "Anti-Aliased";
        final String strViewTop = "Always On Top";
        final String strRememberPos = "Remember Chat Window Position";
        final String strViewHide = "Hide Control Window";
        final MenuComponent[] viewComponents = new MenuComponent[] { new MenuComponent(strAntiAlias, KeyEvent.VK_A, null, true), null, new MenuComponent(strViewTop, KeyEvent.VK_T, null, true), new MenuComponent(strRememberPos, KeyEvent.VK_P, null, true), new MenuComponent(strViewHide, KeyEvent.VK_H, KeyStroke.getKeyStroke(KeyEvent.VK_H, Event.CTRL_MASK)) };

        /* Message Menu Item Text */
        final String strMsgMsg = "Message Management";
        final MenuComponent[] messageComponents = new MenuComponent[] { new MenuComponent(strMsgMsg, KeyEvent.VK_M, KeyStroke.getKeyStroke(KeyEvent.VK_M, Event.CTRL_MASK)) };

        /* Help Menu Item Text */
        final String strHelpHelp = "Help";
        final String strHelpDebug = "Debug Mode";
        final String strHelpAbout = "About";
        final MenuComponent[] helpComponents = new MenuComponent[] { new MenuComponent(strHelpHelp, KeyEvent.VK_R, null), new MenuComponent(strHelpDebug, KeyEvent.VK_D, null, true), null, new MenuComponent(strHelpAbout, KeyEvent.VK_A, null) };

        /* All menu components, with a placeholder for the Presets menu */
        final MenuComponent[][] allMenuComponents = new MenuComponent[][] { fileComponents, new MenuComponent[] {}, viewComponents, messageComponents, helpComponents };

        ActionListener mal = new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                JMenuItem mi = (JMenuItem) e.getSource();
                if (strFileOpen.equals(mi.getText()))
                {
                    open();
                }
                else if (strFileSave.equals(mi.getText()))
                {
                    saveConfig();
                }
                else if (strFileRestore.equals(mi.getText()))
                {
                    restoreDefaults(true);
                    controlTabs.refreshUiFromConfig(fProps);
                }
                else if (strScreenshot.equals(mi.getText()))
                {
                    saveScreenshot();
                }
                else if (strFileExit.equals(mi.getText()))
                {
                    attemptToExit();
                }
                else if (strAntiAlias.equals(mi.getText()))
                {
                    JCheckBoxMenuItem checkBox = (JCheckBoxMenuItem) e.getSource();
                    controlTabs.setAntiAlias(checkBox.isSelected());
                    chatWindow.getChatPanel().repaint();
                }
                else if (strViewTop.equals(mi.getText()))
                {
                    JCheckBoxMenuItem checkBox = (JCheckBoxMenuItem) e.getSource();
                    ((JFrame) getParent()).setAlwaysOnTop(checkBox.isSelected());
                    controlTabs.setAlwaysOnTopConfig(checkBox.isSelected());
                }
                else if (strRememberPos.equals(mi.getText()))
                {
                    JCheckBoxMenuItem checkBox = (JCheckBoxMenuItem) e.getSource();
                    controlTabs.setRememberChatWindowPosition(checkBox.isSelected());
                    if (checkBox.isSelected())
                    {
                        final int sx = (int) chatWindow.getLocationOnScreen().getX();
                        final int sy = (int) chatWindow.getLocationOnScreen().getY();
                        fProps.getChatConfig().setChatWindowPositionX(sx);
                        fProps.getChatConfig().setChatWindowPositionY(sy);
                    }
                }
                else if (strViewHide.equals(mi.getText()))
                {
                    setVisible(false);
                }
                else if (strMsgMsg.equals(mi.getText()))
                {
                    messageDialog.showDialog();
                }
                else if (strHelpHelp.equals(mi.getText()))
                {
                    help.setVisible(true);
                }
                else if (strHelpDebug.equals(mi.getText()))
                {
                    toggleDebugTab();
                }
                else if (strHelpAbout.equals(mi.getText()))
                {
                    showAboutPane();
                }
            }
        };

        /* Set all menu items but presets */
        JMenuItem item = null;
        for (int i = 0; i < allMenuComponents.length; i++)
        {
            for (int j = 0; j < allMenuComponents[i].length; j++)
            {
                MenuComponent mc = allMenuComponents[i][j];
                if (mc == null)
                {
                    menus[i].add(new JSeparator());
                }
                else
                {
                    item = mc.checkbox ? new JCheckBoxMenuItem(mc.label) : new JMenuItem(mc.label);
                    item.addActionListener(mal);
                    item.setMnemonic(mc.mnemonic);
                    if (mc.accelerator != null)
                    {
                        item.setAccelerator(mc.accelerator);
                    }
                    menus[i].add(item);
                }
            }
            menuBar.add(menus[i]);
        }

        Map<String, List<String[]>> presetMapSubmenuToItem = AssetIndexLoader.loadPresets();

        final List<String[]> allPresets = new ArrayList<String[]>();
        for (String key : presetMapSubmenuToItem.keySet())
        {
            for (String[] value : presetMapSubmenuToItem.get(key))
            {
                allPresets.add(value);
            }
        }

        ActionListener presetListener = new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                String sourceText = ((JMenuItem) e.getSource()).getText();
                for (String[] presets : allPresets)
                {
                    if (presets[0].equals(sourceText))
                    {
                        loadPreset(presets[0], presets[1]);
                        break;
                    }
                }
            }
        };

        for (String submenuKey : presetMapSubmenuToItem.keySet())
        {
            List<String[]> submenuItems = presetMapSubmenuToItem.get(submenuKey);
            if (submenuKey != null)
            {
                JMenu submenu = new JMenu(submenuKey);
                for (String[] itemAndFilename : submenuItems)
                {
                    JMenuItem submenuItem = new JMenuItem(itemAndFilename[0]);
                    submenuItem.addActionListener(presetListener);
                    submenu.add(submenuItem);
                }
                menus[1].add(submenu);
            }
            else
            {
                for (String[] submenuRootItemAndFilename : submenuItems)
                {
                    JMenuItem submenuRootItem = new JMenuItem(submenuRootItemAndFilename[0]);
                    submenuRootItem.addActionListener(presetListener);
                    menus[1].add(submenuRootItem);
                }
            }
        }

        for (int i = 0; i < menus.length; i++)
        {
            menuBar.add(menus[i]);
        }

        setJMenuBar(menuBar); // add the whole menu bar
    }

    /**
     * Restore default values to the configuration
     * 
     * @param overrideExistingValues
     */
    private void restoreDefaults(boolean overrideExistingValues)
    {
        boolean okayToProceed = fProps.checkForUnsavedProps(this, this);

        if (okayToProceed)
        {
            int result = JOptionPane.showConfirmDialog(this, "Reset to default configuration?", "Confirm", JOptionPane.YES_NO_OPTION);
            if (result == JOptionPane.YES_OPTION)
            {
                fProps.loadDefaultValues(overrideExistingValues);
                controlTabs.refreshUiFromConfig(fProps);
                chatWindow.getChatPanel().repaint();
            }
        }
    }

    private void open()
    {
        boolean okayToProceed = fProps.checkForUnsavedProps(this, this);

        if (okayToProceed)
        {
            int result = opener.showOpenDialog(me);
            if (result == JFileChooser.APPROVE_OPTION)
            {
                try
                {
                    LoadConfigReport report = fProps.loadFile(opener.getSelectedFile());
                    if (report.isProblem())
                    {
                        throw new Exception("Configuration file open error");
                    }
                    controlTabs.refreshUiFromConfig(fProps);
                    chatWindow.getChatPanel().repaint();
                }
                catch (Exception ex)
                {
                    final String errorMsg = "Unable to open file " + (opener.getSelectedFile() == null ? "null" : opener.getSelectedFile().getName());
                    logger.error(errorMsg, ex);
                    ChatWindow.popup.handleProblem(errorMsg);
                }
            }
        }
    }

    private void loadPreset(String presetName, String presetFilename)
    {
        boolean okayToProceed = fProps.checkForUnsavedProps(this, this);
        if (okayToProceed)
        {
            try
            {
                LoadConfigReport report = fProps.loadFile(presetFilename);
                if (report.isProblem())
                {
                    logger.error("Unsuccessful call to FontificatorProperties.loadFile(String)");
                    throw new Exception();
                }
            }
            catch (Exception ex)
            {
                logger.error(ex.toString(), ex);
                ChatWindow.popup.handleProblem("Unable to load preset " + presetName + " (" + presetFilename + ")");
            }
            controlTabs.refreshUiFromConfig(fProps);
            chatWindow.getChatPanel().repaint();
        }
    }

    /**
     * Gets a file to save to from the given file chooser, including options to overwrite
     * 
     * @param chooser
     *            the chooser to use to get the file
     * @param extension
     *            the extension of the type of file being saved, or null if there is no default extension
     * @return file or null if selection is canceled
     */
    public static File getTargetSaveFile(JFileChooser chooser, String extension)
    {
        int overwrite = JOptionPane.YES_OPTION;
        // Do while overwrite is no, so it loops back around to try again if someone says they don't want to
        // overwrite an existing file, but if they select cancel it just breaks out of the loop
        do
        {
            int result = chooser.showSaveDialog(me);

            // Default to yes, so it writes even if there's no existing file
            overwrite = JOptionPane.YES_OPTION;

            if (result == JFileChooser.APPROVE_OPTION)
            {
                File saveFile = chooser.getSelectedFile();

                if (chooser.getFileFilter() instanceof FileNameExtensionFilter)
                {
                    String[] exts = ((FileNameExtensionFilter) (chooser.getFileFilter())).getExtensions();
                    boolean endsInExt = false;
                    for (String ext : exts)
                    {
                        if (saveFile.getName().toLowerCase().endsWith(ext.toLowerCase()))
                        {
                            endsInExt = true;
                            break;
                        }
                    }
                    if (extension != null && !endsInExt)
                    {
                        saveFile = new File(saveFile.getPath() + "." + extension);
                    }
                }

                if (saveFile.exists())
                {
                    overwrite = JOptionPane.showConfirmDialog(me, "File " + saveFile.getName() + " already exists. Overwrite?", "Overwrite?", JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE);
                }

                if (overwrite == JOptionPane.YES_OPTION)
                {
                    return saveFile;
                }
            }
        } while (overwrite == JOptionPane.NO_OPTION);
        return null;
    }

    /**
     * Save configuration to a file
     * 
     * @return whether the file was saved
     */
    public boolean saveConfig()
    {
        final boolean configReadyToSave = controlTabs.refreshConfigFromUi();
        File saveFile = null;
        if (configReadyToSave)
        {
            saveFile = getTargetSaveFile(configSaver, DEFAULT_CONFIG_FILE_EXTENSION);
        }
        if (saveFile != null)
        {
            try
            {
                fProps.saveFile(saveFile);
                return true;
            }
            catch (Exception ex)
            {
                logger.error("Configuration file save error", ex);
                return false;
            }
        }
        return false;
    }

    /**
     * Takes and saves a screenshot of the current chat window
     * 
     * @return whether the screenshot was saved
     */
    private boolean saveScreenshot()
    {
        // Take the screenshot before the save file chooser is shown
        ChatPanel chat = chatWindow.getChatPanel();
        BufferedImage chatImage = new BufferedImage(chat.getWidth(), chat.getHeight(), screenshotOptions.isTransparencyEnabled() ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB);
        Graphics chatGraphics = chatImage.getGraphics();
        chat.paint(chatGraphics);

        final boolean chromaEnabled = Boolean.toString(true).equalsIgnoreCase(fProps.getProperty(FontificatorProperties.KEY_CHAT_CHROMA_ENABLED));
        if (screenshotOptions.isTransparencyEnabled() && chromaEnabled)
        {
            final int chromaKey = new Color(Integer.parseInt(fProps.getProperty(FontificatorProperties.KEY_COLOR_CHROMA_KEY), 16)).getRGB();
            final int transparentPixel = new Color(0, true).getRGB();

            for (int r = 0; r < chatImage.getHeight(); r++)
            {
                for (int c = 0; c < chatImage.getWidth(); c++)
                {
                    if (chatImage.getRGB(c, r) == chromaKey)
                    {
                        chatImage.setRGB(c, r, transparentPixel);
                    }
                }
            }
        }

        File saveFile = getTargetSaveFile(screenshotSaver, DEFAULT_SCREENSHOT_FILE_EXTENSION);
        if (saveFile != null)
        {
            try
            {
                if (screenshotOptions.isMetadataEnabled())
                {
                    ImageWriter writer = ImageIO.getImageWritersByFormatName("PNG").next();
                    ImageOutputStream stream = ImageIO.createImageOutputStream(saveFile);
                    writer.setOutput(stream);

                    IIOMetadata metadata = writer.getDefaultImageMetadata(ImageTypeSpecifier.createFromRenderedImage(chatImage), writer.getDefaultWriteParam());

                    IIOMetadataNode title = generateMetadataNode("Title", "CGF Screenshot");
                    IIOMetadataNode software = generateMetadataNode("Software", "Chat Game Fontificator");
                    final String fontGameName = ControlPanelFont.getFontGameName(fProps.getProperty(FontificatorProperties.KEY_FONT_FILE_FONT));
                    final String borderGameName = ControlPanelFont.getBorderGameName(fProps.getProperty(FontificatorProperties.KEY_FONT_FILE_BORDER));
                    IIOMetadataNode description = generateMetadataNode("Description", fontGameName + " Font / " + borderGameName + " Border");

                    IIOMetadataNode text = new IIOMetadataNode("tEXt");
                    text.appendChild(title);
                    text.appendChild(software);
                    text.appendChild(description);

                    final String metadataFormatStr = "javax_imageio_png_1.0";
                    IIOMetadataNode root = new IIOMetadataNode(metadataFormatStr);
                    root.appendChild(text);
                    metadata.mergeTree(metadataFormatStr, root);

                    writer.write(metadata, new IIOImage(chatImage, null, metadata), writer.getDefaultWriteParam());

                    stream.close();
                }
                else
                {
                    ImageIO.write(chatImage, "png", saveFile);
                }

                return true;
            }
            catch (Exception e)
            {
                logger.error("Unable to save screenshot", e);
                return false;
            }
        }
        return false;
    }

    private IIOMetadataNode generateMetadataNode(String key, String value)
    {
        IIOMetadataNode node = new IIOMetadataNode("tEXtEntry");
        node.setAttribute("keyword", key);
        node.setAttribute("value", value);
        return node;
    }

    public void setAlwaysOnTopMenu(boolean alwaysOnTop)
    {
        ((JCheckBoxMenuItem) (getJMenuBar().getMenu(2).getItem(2))).setSelected(alwaysOnTop);
    }

    public void setRememberPositionMenu(boolean rememberPosition)
    {
        ((JCheckBoxMenuItem) (getJMenuBar().getMenu(2).getItem(3))).setSelected(rememberPosition);
    }

    public void setAntiAliasMenu(boolean antiAlias)
    {
        ((JCheckBoxMenuItem) (getJMenuBar().getMenu(2).getItem(0))).setSelected(antiAlias);
    }

    public void clearUsernameCases()
    {
        bot.clearUsernameCases();
    }

    public void addManualMessage(String username, String message)
    {
        bot.sendMessageToChat(MessageType.MANUAL, message, new TwitchPrivmsg(username));
    }

    public void disconnect()
    {
        bot.disconnect();
    }

    public void attemptToExit()
    {
        attemptToExit(this);
    }

    /**
     * Any program exit should call this method to do so
     */
    public void attemptToExit(Component parent)
    {
        boolean okayToProceed = fProps.checkForUnsavedProps(this, parent);
        if (okayToProceed)
        {
            disconnect();
            System.exit(0);
        }
    }

    /**
     * Construct the popup dialog containing the About message
     */
    private void constructAboutPopup()
    {
        aboutPane = new JEditorPane("text/html", ABOUT_CONTENTS);
        aboutPane.addHyperlinkListener(new HyperlinkListener()
        {
            public void hyperlinkUpdate(HyperlinkEvent e)
            {
                if (EventType.ACTIVATED.equals(e.getEventType()))
                {
                    if (Desktop.isDesktopSupported())
                    {
                        try
                        {
                            Desktop.getDesktop().browse(URI.create("https://" + e.getDescription()));
                        }
                        catch (IOException e1)
                        {
                            e1.printStackTrace();
                        }
                    }
                }
            }
        });
        aboutPane.setEditable(false);
    }

    private void toggleDebugTab()
    {
        controlTabs.toggleDebugTab();
    }

    private void showAboutPane()
    {
        JOptionPane.showMessageDialog(this, aboutPane, "About", JOptionPane.PLAIN_MESSAGE);
    }

    public MessageDialog getMessageDialog()
    {
        return messageDialog;
    }

    public void loadAfterInit()
    {
        controlTabs.setChatWindowPosition();
    }

    public ControlPanelDebug getDebugPanel()
    {
        return controlTabs.getDebugTab();
    }

} public class DropdownBorder
{
    private final String borderFilename;

    /**
     * The default color to use to tint this background
     */
    private final Color defaultTint;

    public DropdownBorder(String borderFilename, int defaultTint)
    {
        this(borderFilename, new Color(defaultTint));
    }

    public DropdownBorder(String borderFilename, Color defaultTint)
    {
        this.borderFilename = borderFilename;
        this.defaultTint = defaultTint;
    }

    public String getBorderFilename()
    {
        return ConfigFont.INTERNAL_FILE_PREFIX + ConfigFont.INTERNAL_BORDER_DIR + borderFilename;
    }

    public Color getDefaultTint()
    {
        return defaultTint;
    }
} public class DropdownFont
{
    private final String fontFilename;

    private final FontType defaultType;

    public DropdownFont(String fontFilename, FontType defaultType)
    {
        this.fontFilename = fontFilename;
        this.defaultType = defaultType;
    }

    public String getFontFilename()
    {
        return ConfigFont.INTERNAL_FILE_PREFIX + ConfigFont.INTERNAL_FONT_DIR + fontFilename;
    }

    public FontType getDefaultType()
    {
        return defaultType;
    }

}
import java.awt.Color;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.border.TitledBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.filechooser.FileFilter;
import org.apache.log4j.Logger;
import com.glitchcog.fontificator.config.ConfigFont;
import com.glitchcog.fontificator.config.FontType;
import com.glitchcog.fontificator.config.FontificatorProperties;
import com.glitchcog.fontificator.config.loadreport.LoadConfigReport;
import com.glitchcog.fontificator.gui.AssetIndexLoader;
import com.glitchcog.fontificator.gui.chat.ChatWindow;
import com.glitchcog.fontificator.gui.component.CharacterPicker;
import com.glitchcog.fontificator.gui.component.LabeledInput;
import com.glitchcog.fontificator.gui.component.LabeledSlider;
import com.glitchcog.fontificator.gui.component.combomenu.ComboMenuBar;
import com.glitchcog.fontificator.gui.controls.ControlWindow;
import com.glitchcog.fontificator.gui.controls.panel.model.DropdownBorder;
import com.glitchcog.fontificator.gui.controls.panel.model.DropdownFont;
import com.glitchcog.fontificator.gui.controls.panel.model.DropdownLabel;
package com.glitchcog.fontificator.gui.controls.panel;





/**
 * Control Panel containing all the Font and Border options
 * 
 * @author Matt Yanos
 */
public class ControlPanelFont extends ControlPanelBase
{
    private static final Logger logger = Logger.getLogger(ControlPanelFont.class);

    private static final long serialVersionUID = 1L;

    /**
     * Text of the selection option in the font and border preset dropdown menus to prompt the user to specify her own
     * file
     */
    public static final DropdownLabel CUSTOM_KEY = new DropdownLabel(null, "Custom...");

    private static final Color SCALE_EVEN = Color.BLACK;

    private static final Color SCALE_UNEVEN = new Color(0x661033);

    private static final Map<DropdownLabel, DropdownFont> PRESET_FONT_FILE_MAP = AssetIndexLoader.loadFonts();

    /**
     * A reverse lookup through the map to get the actual name of a game for a font
     * 
     * @param font
     *            filename
     * @return game name
     */
    public static String getFontGameName(String filename)
    {
        for (Entry<DropdownLabel, DropdownFont> e : PRESET_FONT_FILE_MAP.entrySet())
        {
            if (CUSTOM_KEY.equals(e.getKey()))
            {
                continue;
            }

            final String fontFilename = e.getValue().getFontFilename();

            if (fontFilename.equals(filename))
            {
                return e.getKey().getLabel();
            }
        }
        return "Unknown";
    }

    public static List<DropdownFont> getAllFonts()
    {
        List<DropdownFont> allFonts = new ArrayList<DropdownFont>();
        for (DropdownLabel ddfKey : PRESET_FONT_FILE_MAP.keySet())
        {
            if (CUSTOM_KEY.equals(ddfKey))
            {
                continue;
            }
            allFonts.add(PRESET_FONT_FILE_MAP.get(ddfKey));
        }
        return allFonts;
    }

    private static Map<DropdownLabel, DropdownBorder> PRESET_BORDER_FILE_MAP = AssetIndexLoader.loadBorders();

    /**
     * A reverse lookup through the map to get the actual name of a game for a border
     * 
     * @param border
     *            filename
     * @return game name
     */
    public static String getBorderGameName(String filename)
    {
        for (Entry<DropdownLabel, DropdownBorder> e : PRESET_BORDER_FILE_MAP.entrySet())
        {
            if (CUSTOM_KEY.equals(e.getKey()))
            {
                continue;
            }

            final String borderFilename = e.getValue().getBorderFilename();

            if (borderFilename.equals(filename))
            {
                return e.getKey().getLabel();
            }
        }
        return "Unknown";
    }

    private LabeledInput fontFilenameInput;

    private ComboMenuBar fontPresetDropdown;