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

package net.minecrell.terminalconsole;

import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.layout.PatternLayout;
import org.apache.logging.log4j.core.pattern.ConverterKeys;
import org.apache.logging.log4j.core.pattern.LogEventPatternConverter;
import org.apache.logging.log4j.core.pattern.PatternConverter;
import org.apache.logging.log4j.core.pattern.PatternFormatter;
import org.apache.logging.log4j.core.pattern.PatternParser;
import org.apache.logging.log4j.util.PerformanceSensitive;
import org.apache.logging.log4j.util.PropertiesUtil;
import org.checkerframework.checker.nullness.qual.Nullable;

import java.util.List;

/**
 * Replaces Minecraft formatting codes in the result of a pattern with
 * appropriate ANSI escape codes. The implementation will only replace valid
 * color codes using the section sign (§).
 *
 * <p>The {@link MinecraftFormattingConverter} can be only used together with
 * {@link TerminalConsoleAppender} to detect if the current console supports
 * color output. When running in an unsupported environment, it will
 * automatically strip all formatting codes instead.</p>
 *
 * <p>{@link TerminalConsoleAppender#ANSI_OVERRIDE_PROPERTY} may be used
 * to force the use of ANSI colors even in unsupported environments. As an
 * alternative, {@link #KEEP_FORMATTING_PROPERTY} may be used to keep the
 * raw Minecraft formatting codes.</p>
 *
 * <p><b>Example usage:</b> {@code %minecraftFormatting{%message}}<br>
 * It can be configured to always strip formatting codes from the message:
 * {@code %minecraftFormatting{%message}{strip}}</p>
 *
 * @see <a href="http://minecraft.gamepedia.com/Formatting_codes">
 *     Formatting Codes</a>
 */
@Plugin(name = "minecraftFormatting", category = PatternConverter.CATEGORY)
@ConverterKeys({ "minecraftFormatting" })
@PerformanceSensitive("allocation")
public final class MinecraftFormattingConverter extends LogEventPatternConverter {

    /**
     * System property that allows disabling the replacement of Minecraft
     * formatting codes entirely, keeping them in the console output. For
     * some applications they might be easier and more accurate for parsing
     * in applications like certain control panels.
     *
     * <p>If this system property is not set, or set to any value except
     * {@code true}, all Minecraft formatting codes will be replaced
     * or stripped from the console output.</p>
     */
    public static final String KEEP_FORMATTING_PROPERTY = TerminalConsoleAppender.PROPERTY_PREFIX + ".keepMinecraftFormatting";

    private static final boolean KEEP_FORMATTING = PropertiesUtil.getProperties().getBooleanProperty(KEEP_FORMATTING_PROPERTY);

    static final String ANSI_RESET = "\u001B[m";

    private static final char COLOR_CHAR = '§';
    private static final String LOOKUP = "0123456789abcdefklmnor";

    private static final String[] ansiCodes = new String[] {
            "\u001B[0;30m", // Black §0
            "\u001B[0;34m", // Dark Blue §1
            "\u001B[0;32m", // Dark Green §2
            "\u001B[0;36m", // Dark Aqua §3
            "\u001B[0;31m", // Dark Red §4
            "\u001B[0;35m", // Dark Purple §5
            "\u001B[0;33m", // Gold §6
            "\u001B[0;37m", // Gray §7
            "\u001B[0;30;1m",  // Dark Gray §8
            "\u001B[0;34;1m",  // Blue §9
            "\u001B[0;32;1m",  // Green §a
            "\u001B[0;36;1m",  // Aqua §b
            "\u001B[0;31;1m",  // Red §c
            "\u001B[0;35;1m",  // Light Purple §d
            "\u001B[0;33;1m",  // Yellow §e
            "\u001B[0;37;1m",  // White §f
            "\u001B[5m",       // Obfuscated §k
            "\u001B[21m",      // Bold §l
            "\u001B[9m",       // Strikethrough §m
            "\u001B[4m",       // Underline §n
            "\u001B[3m",       // Italic §o
            ANSI_RESET,        // Reset §r
    };

    private final boolean ansi;
    private final List<PatternFormatter> formatters;

    /**
     * Construct the converter.
     *
     * @param formatters The pattern formatters to generate the text to manipulate
     * @param strip If true, the converter will strip all formatting codes
     */
    protected MinecraftFormattingConverter(List<PatternFormatter> formatters, boolean strip) {
        super("minecraftFormatting", null);
        this.formatters = formatters;
        this.ansi = !strip;
    }

    @Override
    public void format(LogEvent event, StringBuilder toAppendTo) {
        int start = toAppendTo.length();
        //noinspection ForLoopReplaceableByForEach
        for (int i = 0, size = formatters.size(); i < size; i++) {
            formatters.get(i).format(event, toAppendTo);
        }

        if (KEEP_FORMATTING || toAppendTo.length() == start) {
            // Skip replacement if disabled or if the content is empty
            return;
        }

        String content = toAppendTo.substring(start);
        format(content, toAppendTo, start, ansi && TerminalConsoleAppender.isAnsiSupported());
    }

    static void format(String s, StringBuilder result, int start, boolean ansi) {
        int next = s.indexOf(COLOR_CHAR);
        int last = s.length() - 1;
        if (next == -1 || next == last) {
            return;
        }

        result.setLength(start + next);

        int pos = next;
        do {
            int format = LOOKUP.indexOf(Character.toLowerCase(s.charAt(next + 1)));
            if (format != -1) {
                if (pos != next) {
                    result.append(s, pos, next);
                }
                if (ansi) {
                    result.append(ansiCodes[format]);
                }
                pos = next += 2;
            } else {
                next++;
            }

            next = s.indexOf(COLOR_CHAR, next);
        } while (next != -1 && next < last);

        result.append(s, pos, s.length());
        if (ansi) {
            result.append(ANSI_RESET);
        }
    }

    /**
     * Gets a new instance of the {@link MinecraftFormattingConverter} with the
     * specified options.
     *
     * @param config The current configuration
     * @param options The pattern options
     * @return The new instance
     *
     * @see MinecraftFormattingConverter
     */
    public static @Nullable MinecraftFormattingConverter newInstance(Configuration config, String[] options) {
        if (options.length < 1 || options.length > 2) {
            LOGGER.error("Incorrect number of options on minecraftFormatting. Expected at least 1, max 2 received " + options.length);
            return null;
        }
        if (options[0] == null) {
            LOGGER.error("No pattern supplied on minecraftFormatting");
            return null;
        }

        PatternParser parser = PatternLayout.createPatternParser(config);
        List<PatternFormatter> formatters = parser.parse(options[0]);
        boolean strip = options.length > 1 && "strip".equals(options[1]);
        return new MinecraftFormattingConverter(formatters, strip);
    }

}