/*
 * Copyright 2016 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.gradle.internal.logging.services;

import org.gradle.api.logging.LogLevel;
import org.gradle.api.logging.StandardOutputListener;
import org.gradle.api.logging.configuration.ConsoleOutput;
import org.gradle.internal.concurrent.CompositeStoppable;
import org.gradle.internal.concurrent.Stoppable;
import org.gradle.internal.logging.LoggingManagerInternal;
import org.gradle.internal.logging.LoggingOutputInternal;
import org.gradle.internal.logging.config.LoggingRouter;
import org.gradle.internal.logging.config.LoggingSourceSystem;
import org.gradle.internal.logging.config.LoggingSystem;
import org.gradle.internal.logging.events.OutputEventListener;
import org.gradle.internal.logging.text.StreamBackedStandardOutputListener;

import java.io.Closeable;
import java.io.OutputStream;
import java.util.LinkedHashSet;
import java.util.Set;

public class DefaultLoggingManager implements LoggingManagerInternal, Closeable {
    private boolean started;
    private final StartableLoggingSystem slf4jLoggingSystem;
    private final StartableLoggingSystem stdOutLoggingSystem;
    private final StartableLoggingSystem stdErrLoggingSystem;
    private final StartableLoggingSystem javaUtilLoggingSystem;
    private final StartableLoggingRouter loggingRouter;
    private final LoggingOutputInternal loggingOutput;
    private final Set<StandardOutputListener> stdoutListeners = new LinkedHashSet<StandardOutputListener>();
    private final Set<StandardOutputListener> stderrListeners = new LinkedHashSet<StandardOutputListener>();
    private final Set<OutputEventListener> outputEventListeners = new LinkedHashSet<OutputEventListener>();

    public DefaultLoggingManager(LoggingSourceSystem slf4jLoggingSystem, LoggingSourceSystem javaUtilLoggingSystem, LoggingSourceSystem stdOutLoggingSystem,
                                 LoggingSourceSystem stdErrLoggingSystem, LoggingRouter loggingRouter) {
        this.loggingOutput = loggingRouter;
        this.loggingRouter = new StartableLoggingRouter(loggingRouter);
        this.slf4jLoggingSystem = new StartableLoggingSystem(slf4jLoggingSystem, null);
        this.stdOutLoggingSystem = new StartableLoggingSystem(stdOutLoggingSystem, null);
        this.stdErrLoggingSystem = new StartableLoggingSystem(stdErrLoggingSystem, null);
        this.javaUtilLoggingSystem = new StartableLoggingSystem(javaUtilLoggingSystem, null);
    }

    public DefaultLoggingManager start() {
        started = true;
        for (StandardOutputListener stdoutListener : stdoutListeners) {
            loggingOutput.addStandardOutputListener(stdoutListener);
        }
        for (StandardOutputListener stderrListener : stderrListeners) {
            loggingOutput.addStandardErrorListener(stderrListener);
        }
        for (OutputEventListener outputEventListener : outputEventListeners) {
            loggingOutput.addOutputEventListener(outputEventListener);
        }
        loggingRouter.start();

        slf4jLoggingSystem.enableCapture();
        slf4jLoggingSystem.start();

        javaUtilLoggingSystem.start();
        stdOutLoggingSystem.start();
        stdErrLoggingSystem.start();

        return this;
    }

    public DefaultLoggingManager stop() {
        try {
            CompositeStoppable.stoppable(slf4jLoggingSystem, javaUtilLoggingSystem, stdOutLoggingSystem, stdErrLoggingSystem).stop();
            for (StandardOutputListener stdoutListener : stdoutListeners) {
                loggingOutput.removeStandardOutputListener(stdoutListener);
            }
            for (StandardOutputListener stderrListener : stderrListeners) {
                loggingOutput.removeStandardErrorListener(stderrListener);
            }
            for (OutputEventListener listener : outputEventListeners) {
                loggingOutput.removeOutputEventListener(listener);
            }
            loggingRouter.stop();
        } finally {
            started = false;
        }
        return this;
    }

    public void close() {
        stop();
    }

    @Override
    public DefaultLoggingManager setLevelInternal(LogLevel logLevel) {
        slf4jLoggingSystem.setLevel(logLevel);
        javaUtilLoggingSystem.setLevel(logLevel);
        loggingRouter.setLevel(logLevel);
        return this;
    }

    public LogLevel getLevel() {
        return slf4jLoggingSystem.level;
    }

    @Override
    public DefaultLoggingManager setMaxWorkerCount(int maxWorkerCount) {
        loggingRouter.setMaxWorkerCount(maxWorkerCount);
        return this;
    }

    @Override
    public DefaultLoggingManager captureSystemSources() {
        stdOutLoggingSystem.enableCapture();
        stdErrLoggingSystem.enableCapture();
        javaUtilLoggingSystem.enableCapture();
        return this;
    }

    public LogLevel getStandardOutputCaptureLevel() {
        return stdOutLoggingSystem.level;
    }

    public DefaultLoggingManager captureStandardOutput(LogLevel level) {
        stdOutLoggingSystem.setLevel(level);
        return this;
    }

    public DefaultLoggingManager captureStandardError(LogLevel level) {
        stdErrLoggingSystem.setLevel(level);
        return this;
    }

    public LogLevel getStandardErrorCaptureLevel() {
        return stdErrLoggingSystem.level;
    }

    public void addStandardOutputListener(StandardOutputListener listener) {
        if (stdoutListeners.add(listener) && started) {
            loggingOutput.addStandardOutputListener(listener);
        }
    }

    public void addStandardErrorListener(StandardOutputListener listener) {
        if (stderrListeners.add(listener) && started) {
            loggingOutput.addStandardErrorListener(listener);
        }
    }

    public void addStandardOutputListener(OutputStream outputStream) {
        addStandardOutputListener(new StreamBackedStandardOutputListener(outputStream));
    }

    public void addStandardErrorListener(OutputStream outputStream) {
        addStandardErrorListener(new StreamBackedStandardOutputListener(outputStream));
    }

    public void removeStandardOutputListener(StandardOutputListener listener) {
        if (stdoutListeners.remove(listener) && started) {
            loggingOutput.removeStandardOutputListener(listener);
        }
    }

    public void removeStandardErrorListener(StandardOutputListener listener) {
        if (stderrListeners.remove(listener) && started) {
            loggingOutput.removeStandardErrorListener(listener);
        }
    }

    public void addOutputEventListener(OutputEventListener listener) {
        if (outputEventListeners.add(listener) && started) {
            loggingOutput.addOutputEventListener(listener);
        }
    }

    public void removeOutputEventListener(OutputEventListener listener) {
        if (outputEventListeners.remove(listener) && started) {
            loggingOutput.removeOutputEventListener(listener);
        }
    }

    public void attachProcessConsole(ConsoleOutput consoleOutput) {
        loggingRouter.attachProcessConsole(consoleOutput);
    }

    public void attachAnsiConsole(OutputStream outputStream) {
        loggingRouter.attachAnsiConsole(outputStream);
    }

    public void attachSystemOutAndErr() {
        loggingOutput.attachSystemOutAndErr();
    }

    private static class StartableLoggingRouter implements Stoppable {
        private final LoggingRouter loggingRouter;
        private LogLevel level;
        private Integer maxWorkerCount;
        private LoggingSystem.Snapshot originalState;
        private ConsoleOutput consoleOutput;
        private OutputStream consoleOutputStream;

        public StartableLoggingRouter(LoggingRouter loggingRouter) {
            this.loggingRouter = loggingRouter;
        }

        public void start() {
            originalState = loggingRouter.snapshot();
            if (level != null) {
                loggingRouter.configure(level);
            }
            if (maxWorkerCount != null) {
                loggingRouter.configureMaxWorkerCount(maxWorkerCount.intValue());
            }
            if (consoleOutput != null) {
                loggingRouter.attachProcessConsole(consoleOutput);
            }
            if (consoleOutputStream != null) {
                loggingRouter.attachAnsiConsole(consoleOutputStream);
            }
        }

        public void attachProcessConsole(ConsoleOutput consoleOutput) {
            if (this.consoleOutput == consoleOutput) {
                return;
            }
            if (consoleOutputStream != null) {
                throw new UnsupportedOperationException("Not implemented yet.");
            }

            if (originalState != null) {
                // Already started
                loggingRouter.attachProcessConsole(consoleOutput);
            }
            this.consoleOutput = consoleOutput;
        }

        public void attachAnsiConsole(OutputStream outputStream) {
            if (this.consoleOutputStream == outputStream) {
                return;
            }
            if (consoleOutput != null) {
                throw new UnsupportedOperationException("Not implemented yet.");
            }

            if (originalState != null) {
                // Already started
                loggingRouter.attachAnsiConsole(outputStream);
            }
            this.consoleOutputStream = outputStream;
        }

        public void setLevel(LogLevel logLevel) {
            if (this.level == logLevel) {
                return;
            }

            if (originalState != null) {
                // Already started
                loggingRouter.configure(logLevel);
            }
            level = logLevel;
        }

        public void setMaxWorkerCount(int maxWorkerCount) {
            if (this.maxWorkerCount != null && this.maxWorkerCount == maxWorkerCount) {
                return;
            }

            if (originalState != null) {
                // Already started
                loggingRouter.configureMaxWorkerCount(maxWorkerCount);
            }
            this.maxWorkerCount = Integer.valueOf(maxWorkerCount);
        }

        @Override
        public void stop() {
            try {
                if (originalState != null) {
                    loggingRouter.restore(originalState);
                }
            } finally {
                originalState = null;
            }
        }
    }

    private static class StartableLoggingSystem implements Stoppable {
        private final LoggingSourceSystem loggingSystem;
        private boolean enabled;
        private LogLevel level;
        private LoggingSystem.Snapshot originalState;

        private StartableLoggingSystem(LoggingSourceSystem loggingSystem, LogLevel level) {
            this.loggingSystem = loggingSystem;
            this.level = level;
        }

        /**
         * Start this logging system: take a snapshot of the current state and start capturing events if enabled.
         */
        public void start() {
            originalState = loggingSystem.snapshot();
            if (level != null) {
                loggingSystem.setLevel(level);
            }
            if (enabled) {
                loggingSystem.startCapture();
            }
        }

        /**
         * Start capturing events from this logging system. Does not take effect until started.
         */
        public void enableCapture() {
            if (enabled) {
                return;
            }

            enabled = true;
            if (originalState != null) {
                //started, enable
                loggingSystem.startCapture();
            }
        }

        /**
         * Sets the logging level for this log system. Does not take effect until started .
         */
        public void setLevel(LogLevel logLevel) {
            if (this.level == logLevel) {
                return;
            }

            this.level = logLevel;
            if (originalState != null) {
                // started, update the log level
                loggingSystem.setLevel(logLevel);
            }
        }

        /**
         * Stops this logging system. Restores state from when started.
         */
        public void stop() {
            try {
                if (originalState != null) {
                    loggingSystem.restore(originalState);
                }
            } finally {
                enabled = false;
                originalState = null;
            }
        }
    }
}
