/*
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */
package net.ranides.assira.trace;

import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.experimental.Accessors;

import lombok.experimental.UtilityClass;
import net.ranides.assira.functional.Functions;
import net.ranides.assira.functional.special.AnyFunction;
import net.ranides.assira.functional.Compositor;
import net.ranides.assira.functional.Compositor.CompositorBuilder;
import net.ranides.assira.functional.Functions.Function0;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.math.BigDecimal;
import java.text.MessageFormat;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;

/**
 * PreparedMessage encapsulates in one object log level, log message and optional log arguments.
 * It is useful if we want to simplify
 * complex logging statements inside method bodies.
 *
 * It uses {@link MessageFormat#format}
 * Message patterns should use 0-based argument indexes.
 *
 * PreparedMessage must be constructed using builder.
 *
 * Functions.Function3<Integer, String, BigDecimal, Boolean> message =
 *      PreparedMessage
 *          .forClass("MyClass")
 *          .warn("Hello {0} world {1} from {2}")
 *          .params()
 *             .withParam(Integer.class)
 *             .withParam(String.class)
 *             .withNullable((BigDecimal v) -> v.intValue(), BigDecimal.class)
 *          .function();
 *
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
@UtilityClass public class PreparedMessage {

    /**
     * Starts building new message with "current class" specified as a tag.
     * Can be invoked from static context.
     *
     * @return builder
     */
    public static MessageBuilder forThis() {
        return forClass(TraceUtils.getCallerName());
    }

    /**
     * Starts building new message with specified tag.
     *
     * @param type tag used by logger
     * @return builder
     */
    public static MessageBuilder forClass(Class<?> type) {
        return new MessageBuilder(LoggerFactory.getLogger(type));
    }

    /**
     * Starts building new message with specified tag.
     *
     * @param type tag used by logger
     * @return builder
     */
    public static MessageBuilder forClass(String type) {
        return new MessageBuilder(LoggerFactory.getLogger(type));
    }

    private static BooleanSupplier initEnabled(Logger logger, LoggerLevel level) {
        switch (level) {
            case ERROR:
                return logger::isErrorEnabled;
            case WARN:
                return logger::isWarnEnabled;
            case INFO:
                return logger::isInfoEnabled;
            case DEBUG:
                return logger::isDebugEnabled;
            case TRACE:
                return logger::isTraceEnabled;
            default:
                return () -> false;
        }
    }

    private static Consumer<String> initWriter(Logger logger, LoggerLevel level) {
        switch (level) {
            case ERROR:
                return logger::error;
            case WARN:
                return logger::warn;
            case INFO:
                return logger::info;
            case DEBUG:
                return logger::debug;
            case TRACE:
                return logger::trace;
            default:
                throw new UnsupportedOperationException();
        }
    }

    /**
     * Builder pattern for PreparedMessage.
     */
    @Setter
    @Accessors(fluent = true)
    @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
    public static class MessageBuilder {
        private final Logger logger;
        private String message;
        private LoggerLevel level;

        /**
         * Prepare message with {@link LoggerLevel#ERROR}
         *
         * @param text text
         * @return builder
         */
        public MessageBuilder error(String text) {
            return level(LoggerLevel.ERROR).message(text);
        }

        /**
         * Prepare message with {@link LoggerLevel#WARN}
         *
         * @param text text
         * @return builder
         */
        public MessageBuilder warn(String text) {
            return level(LoggerLevel.WARN).message(text);
        }

        /**
         * Prepare message with {@link LoggerLevel#INFO}
         *
         * @param text text
         * @return builder
         */
        public MessageBuilder info(String text) {
            return level(LoggerLevel.INFO).message(text);
        }

        /**
         * Prepare message with {@link LoggerLevel#DEBUG}
         *
         * @param text text
         * @return builder
         */
        public MessageBuilder debug(String text) {
            return level(LoggerLevel.DEBUG).message(text);
        }

        /**
         * Prepare message with {@link LoggerLevel#TRACE}
         *
         * @param text text
         * @return builder
         */
        public MessageBuilder trace(String text) {
            return level(LoggerLevel.TRACE).message(text);
        }

        /**
         * Configuration of arguments is delegated to functional compositor
         * Composition should be terminated using "function" method.
         *
         * @return compositor
         */
        public Compositor.CompositorScope<MessageBuilder, Object, Boolean>.Compositor0<Object, Object, Object, Object, Boolean>
            params() {
            return Compositor.compose(new Decorator());
        }

        private class Decorator extends Compositor.CompositorDecorator<MessageBuilder, Object, Boolean> {
            @Override
            public Function0<Boolean> before(Function0<Boolean> action) {
                BooleanSupplier enabled = initEnabled(logger, level);
                return () -> enabled.getAsBoolean() && action.apply();
            }

            @Override
            public AnyFunction<Boolean> before(CompositorBuilder<Object, Boolean> function) {
                BooleanSupplier enabled = initEnabled(logger, level);
                return args -> enabled.getAsBoolean() && function.body().apply(args);
            }

            @Override
            public AnyFunction<Boolean> after(CompositorBuilder<Object, Boolean> function) {
                Consumer<String> writer = initWriter(logger, level);
                return args -> {
                    writer.accept(MessageFormat.format(message, args));
                    return true;
                };
            }

            @Override
            public Function0<Boolean> after(Function0<Boolean> action) {
                Consumer<String> writer = initWriter(logger, level);
                return () -> {
                    writer.accept(message);
                    return true;
                };
            }
        }
    }
}
