/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.maven.surefire.booter.spi;

import javax.annotation.Nonnull;

import java.io.IOException;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.charset.CharsetEncoder;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicBoolean;

import org.apache.maven.plugin.surefire.log.api.ConsoleLoggerUtils;
import org.apache.maven.surefire.api.booter.DumpErrorSingleton;
import org.apache.maven.surefire.api.booter.ForkedProcessEventType;
import org.apache.maven.surefire.api.booter.MasterProcessChannelEncoder;
import org.apache.maven.surefire.api.report.ReportEntry;
import org.apache.maven.surefire.api.report.RunMode;
import org.apache.maven.surefire.api.report.SafeThrowable;
import org.apache.maven.surefire.api.report.StackTraceWriter;
import org.apache.maven.surefire.api.report.TestOutputReportEntry;
import org.apache.maven.surefire.api.report.TestSetReportEntry;
import org.apache.maven.surefire.api.util.internal.WritableBufferedByteChannel;
import org.apache.maven.surefire.booter.stream.EventEncoder;

import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_BYE;
import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_CONSOLE_DEBUG;
import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_CONSOLE_ERROR;
import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_CONSOLE_INFO;
import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_CONSOLE_WARNING;
import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_JVM_EXIT_ERROR;
import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_NEXT_TEST;
import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_STDERR;
import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_STDERR_NEW_LINE;
import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_STDOUT;
import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_STDOUT_NEW_LINE;
import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_STOP_ON_NEXT_TEST;
import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_SYSPROPS;
import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_TESTSET_COMPLETED;
import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_TESTSET_STARTING;
import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_TEST_ASSUMPTIONFAILURE;
import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_TEST_ERROR;
import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_TEST_FAILED;
import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_TEST_SKIPPED;
import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_TEST_STARTING;
import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.BOOTERCODE_TEST_SUCCEEDED;

/**
 * magic number : opcode : run mode [: opcode specific data]*
 * <br>
 *
 * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
 * @since 3.0.0-M4
 */
@SuppressWarnings("checkstyle:linelength")
public class EventChannelEncoder extends EventEncoder implements MasterProcessChannelEncoder {
    private final AtomicBoolean trouble = new AtomicBoolean();
    private volatile boolean onExit;

    /**
     * The encoder for events.
     *
     * @param out     the channel available for writing the events
     */
    public EventChannelEncoder(@Nonnull WritableBufferedByteChannel out) {
        super(out);
    }

    @Override
    public boolean checkError() {
        return trouble.get();
    }

    @Override
    public void onJvmExit() {
        onExit = true;
        write(ByteBuffer.wrap(new byte[] {'\n'}), true);
    }

    void encodeSystemProperties(Map<String, String> sysProps, RunMode runMode, Long testRunId) {
        CharsetEncoder encoder = newCharsetEncoder();
        ByteBuffer result = null;
        for (Iterator<Entry<String, String>> it = sysProps.entrySet().iterator(); it.hasNext(); ) {
            Entry<String, String> entry = it.next();
            String key = entry.getKey();
            String value = entry.getValue();

            int bufferLength =
                    estimateBufferLength(BOOTERCODE_SYSPROPS.getOpcode().length(), runMode, encoder, 0, 1, key, value);
            result = result != null && result.capacity() >= bufferLength ? result : ByteBuffer.allocate(bufferLength);
            ((Buffer) result).clear();
            // :maven-surefire-event:sys-prop:<runMode>:<testRunId>:UTF-8:<integer>:<key>:<integer>:<value>:
            encode(encoder, result, BOOTERCODE_SYSPROPS, runMode, testRunId, key, value);
            boolean sync = !it.hasNext();
            write(result, sync);
        }
    }

    @Override
    public void testSetStarting(TestSetReportEntry reportEntry, boolean trimStackTraces) {
        encode(BOOTERCODE_TESTSET_STARTING, reportEntry, trimStackTraces, true);
    }

    @Override
    public void testSetCompleted(TestSetReportEntry reportEntry, boolean trimStackTraces) {
        encodeSystemProperties(reportEntry.getSystemProperties(), reportEntry.getRunMode(), reportEntry.getTestRunId());
        encode(BOOTERCODE_TESTSET_COMPLETED, reportEntry, trimStackTraces, true);
    }

    @Override
    public void testStarting(ReportEntry reportEntry, boolean trimStackTraces) {
        encode(BOOTERCODE_TEST_STARTING, reportEntry, trimStackTraces, true);
    }

    @Override
    public void testSucceeded(ReportEntry reportEntry, boolean trimStackTraces) {
        encode(BOOTERCODE_TEST_SUCCEEDED, reportEntry, trimStackTraces, true);
    }

    @Override
    public void testFailed(ReportEntry reportEntry, boolean trimStackTraces) {
        encode(BOOTERCODE_TEST_FAILED, reportEntry, trimStackTraces, true);
    }

    @Override
    public void testSkipped(ReportEntry reportEntry, boolean trimStackTraces) {
        encode(BOOTERCODE_TEST_SKIPPED, reportEntry, trimStackTraces, true);
    }

    @Override
    public void testError(ReportEntry reportEntry, boolean trimStackTraces) {
        encode(BOOTERCODE_TEST_ERROR, reportEntry, trimStackTraces, true);
    }

    @Override
    public void testAssumptionFailure(ReportEntry reportEntry, boolean trimStackTraces) {
        encode(BOOTERCODE_TEST_ASSUMPTIONFAILURE, reportEntry, trimStackTraces, true);
    }

    @Override
    public void testOutput(TestOutputReportEntry reportEntry) {
        boolean stdout = reportEntry.isStdOut();
        boolean newLine = reportEntry.isNewLine();
        String msg = reportEntry.getLog();
        ForkedProcessEventType event = stdout
                ? (newLine ? BOOTERCODE_STDOUT_NEW_LINE : BOOTERCODE_STDOUT)
                : (newLine ? BOOTERCODE_STDERR_NEW_LINE : BOOTERCODE_STDERR);
        setOutErr(event, reportEntry.getRunMode(), reportEntry.getTestRunId(), msg);
    }

    private void setOutErr(ForkedProcessEventType eventType, RunMode runMode, Long testRunId, String message) {
        ByteBuffer result = encodeMessage(eventType, runMode, testRunId, message);
        write(result, false);
    }

    @Override
    public void consoleInfoLog(String message) {
        ByteBuffer result = encodeMessage(BOOTERCODE_CONSOLE_INFO, message);
        write(result, true);
    }

    @Override
    public void consoleErrorLog(String message) {
        consoleErrorLog(message, null);
    }

    @Override
    public void consoleErrorLog(Throwable t) {
        consoleErrorLog(t.getLocalizedMessage(), t);
    }

    @Override
    public void consoleErrorLog(String message, Throwable t) {
        CharsetEncoder encoder = newCharsetEncoder();
        String stackTrace = t == null ? null : ConsoleLoggerUtils.toString(t);
        int bufferMaxLength = estimateBufferLength(
                BOOTERCODE_CONSOLE_ERROR.getOpcode().length(), null, encoder, 0, 0, message, null, stackTrace);
        ByteBuffer result = ByteBuffer.allocate(bufferMaxLength);
        encodeHeader(result, BOOTERCODE_CONSOLE_ERROR);
        encodeCharset(result);
        encode(encoder, result, message, null, stackTrace);
        write(result, true);
    }

    @Override
    public void consoleErrorLog(StackTraceWriter stackTraceWriter, boolean trimStackTraces) {
        error(stackTraceWriter, trimStackTraces, BOOTERCODE_CONSOLE_ERROR, true);
    }

    @Override
    public void consoleDebugLog(String message) {
        ByteBuffer result = encodeMessage(BOOTERCODE_CONSOLE_DEBUG, message);
        write(result, true);
    }

    @Override
    public void consoleWarningLog(String message) {
        ByteBuffer result = encodeMessage(BOOTERCODE_CONSOLE_WARNING, message);
        write(result, true);
    }

    @Override
    public void bye() {
        encodeOpcode(BOOTERCODE_BYE, true);
    }

    @Override
    public void stopOnNextTest() {
        encodeOpcode(BOOTERCODE_STOP_ON_NEXT_TEST, true);
    }

    @Override
    public void acquireNextTest() {
        encodeOpcode(BOOTERCODE_NEXT_TEST, true);
    }

    @Override
    public void sendExitError(StackTraceWriter stackTraceWriter, boolean trimStackTraces) {
        error(stackTraceWriter, trimStackTraces, BOOTERCODE_JVM_EXIT_ERROR, true);
    }

    private void error(
            StackTraceWriter stackTraceWriter,
            boolean trimStackTraces,
            ForkedProcessEventType eventType,
            @SuppressWarnings("SameParameterValue") boolean sync) {
        CharsetEncoder encoder = newCharsetEncoder();
        StackTrace stackTraceWrapper = new StackTrace(stackTraceWriter, trimStackTraces);
        int bufferMaxLength = estimateBufferLength(
                eventType.getOpcode().length(),
                null,
                encoder,
                0,
                0,
                stackTraceWrapper.message,
                stackTraceWrapper.smartTrimmedStackTrace,
                stackTraceWrapper.stackTrace);
        ByteBuffer result = ByteBuffer.allocate(bufferMaxLength);

        encodeHeader(result, eventType);
        encodeCharset(result);
        encode(encoder, result, stackTraceWrapper);
        write(result, sync);
    }

    // example
    // :maven-surefire-event:testset-starting:rerun-test-after-failure:1:5:UTF-8:<integer>:SourceName:<integer>:SourceText:<integer>:Name:<integer>:NameText:<integer>:Group:<integer>:Message:<integer>:ElapsedTime:<integer>:LocalizedMessage:<integer>:SmartTrimmedStackTrace:<integer>:toStackTrace( stw, trimStackTraces ):<integer>:
    private void encode(
            ForkedProcessEventType operation,
            ReportEntry reportEntry,
            boolean trimStackTraces,
            @SuppressWarnings("SameParameterValue") boolean sync) {
        ByteBuffer result = encode(operation, reportEntry, trimStackTraces);
        write(result, sync);
    }

    private void encodeOpcode(ForkedProcessEventType eventType, boolean sync) {
        int bufferMaxLength = estimateBufferLength(eventType.getOpcode().length(), null, null, 0, 0);
        ByteBuffer result = ByteBuffer.allocate(bufferMaxLength);
        encodeHeader(result, eventType);
        write(result, sync);
    }

    @Override
    protected void write(ByteBuffer frame, boolean sync) {
        final boolean wasInterrupted = Thread.interrupted();
        try {
            super.write(frame, sync);
        } catch (ClosedChannelException e) {
            if (!onExit) {
                String event = new String(
                        frame.array(),
                        frame.arrayOffset() + ((Buffer) frame).position(),
                        frame.remaining(),
                        getCharset());

                DumpErrorSingleton.getSingleton()
                        .dumpException(e, "Channel closed while writing the event '" + event + "'.");
            }
        } catch (IOException e) {
            if (trouble.compareAndSet(false, true)) {
                DumpErrorSingleton.getSingleton().dumpException(e);
            }
        } finally {
            if (wasInterrupted) {
                Thread.currentThread().interrupt();
            }
        }
    }

    private void encode(CharsetEncoder encoder, ByteBuffer result, StackTrace stw) {
        encode(encoder, result, stw.message, stw.smartTrimmedStackTrace, stw.stackTrace);
    }

    private void encode(
            CharsetEncoder encoder, ByteBuffer result, String message, String smartStackTrace, String stackTrace) {
        encodeString(encoder, result, message);
        encodeString(encoder, result, smartStackTrace);
        encodeString(encoder, result, stackTrace);
    }

    /**
     * Used operations:<br>
     * <ul>
     * <li>{@link ForkedProcessEventType#BOOTERCODE_TESTSET_STARTING},</li>
     * <li>{@link ForkedProcessEventType#BOOTERCODE_TESTSET_COMPLETED},</li>
     * <li>{@link ForkedProcessEventType#BOOTERCODE_TEST_STARTING},</li>
     * <li>{@link ForkedProcessEventType#BOOTERCODE_TEST_SUCCEEDED},</li>
     * <li>{@link ForkedProcessEventType#BOOTERCODE_TEST_FAILED},</li>
     * <li>{@link ForkedProcessEventType#BOOTERCODE_TEST_ERROR},</li>
     * <li>{@link ForkedProcessEventType#BOOTERCODE_TEST_SKIPPED},</li>
     * <li>{@link ForkedProcessEventType#BOOTERCODE_TEST_ASSUMPTIONFAILURE}.</li>
     * </ul>
     */
    ByteBuffer encode(ForkedProcessEventType operation, ReportEntry reportEntry, boolean trimStackTraces) {
        StackTrace stackTraceWrapper = new StackTrace(reportEntry.getStackTraceWriter(), trimStackTraces);

        CharsetEncoder encoder = newCharsetEncoder();

        int bufferMaxLength = estimateBufferLength(
                operation.getOpcode().length(),
                reportEntry.getRunMode(),
                encoder,
                1,
                1,
                reportEntry.getSourceName(),
                reportEntry.getSourceText(),
                reportEntry.getName(),
                reportEntry.getNameText(),
                reportEntry.getGroup(),
                reportEntry.getMessage(),
                stackTraceWrapper.message,
                stackTraceWrapper.smartTrimmedStackTrace,
                stackTraceWrapper.stackTrace);

        ByteBuffer result = ByteBuffer.allocate(bufferMaxLength);

        encodeHeader(result, operation, reportEntry.getRunMode(), reportEntry.getTestRunId());
        encodeCharset(result);

        encodeString(encoder, result, reportEntry.getSourceName());
        encodeString(encoder, result, reportEntry.getSourceText());
        encodeString(encoder, result, reportEntry.getName());
        encodeString(encoder, result, reportEntry.getNameText());
        encodeString(encoder, result, reportEntry.getGroup());
        encodeString(encoder, result, reportEntry.getMessage());
        encodeInteger(result, reportEntry.getElapsed());

        encode(encoder, result, stackTraceWrapper);

        return result;
    }

    ByteBuffer encodeMessage(ForkedProcessEventType eventType, RunMode runMode, Long testRunId, String message) {
        CharsetEncoder encoder = newCharsetEncoder();
        int bufferMaxLength = estimateBufferLength(eventType.getOpcode().length(), runMode, encoder, 0, 1, message);
        ByteBuffer result = ByteBuffer.allocate(bufferMaxLength);
        encode(encoder, result, eventType, runMode, testRunId, message);
        return result;
    }

    ByteBuffer encodeMessage(ForkedProcessEventType eventType, String message) {
        CharsetEncoder encoder = newCharsetEncoder();
        int bufferMaxLength = estimateBufferLength(eventType.getOpcode().length(), null, encoder, 0, 0, message);
        ByteBuffer result = ByteBuffer.allocate(bufferMaxLength);
        encode(encoder, result, eventType, message);
        return result;
    }

    private static String toStackTrace(StackTraceWriter stw, boolean trimStackTraces) {
        if (stw == null) {
            return null;
        }

        return trimStackTraces ? stw.writeTrimmedTraceToString() : stw.writeTraceToString();
    }

    private static final class StackTrace {
        final String message;
        final String smartTrimmedStackTrace;
        final String stackTrace;

        StackTrace(StackTraceWriter stackTraceWriter, boolean trimStackTraces) {
            SafeThrowable throwable = stackTraceWriter == null ? null : stackTraceWriter.getThrowable();
            message = throwable == null ? null : throwable.getLocalizedMessage();
            smartTrimmedStackTrace = stackTraceWriter == null ? null : stackTraceWriter.smartTrimmedStackTrace();
            stackTrace = stackTraceWriter == null ? null : toStackTrace(stackTraceWriter, trimStackTraces);
        }
    }
}
