/*
 * 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.sink;

import org.gradle.api.logging.LogLevel;
import org.gradle.internal.SystemProperties;
import org.gradle.internal.logging.events.BatchOutputEventListener;
import org.gradle.internal.logging.events.OperationIdentifier;
import org.gradle.internal.logging.events.OutputEvent;
import org.gradle.internal.logging.events.OutputEventListener;
import org.gradle.internal.logging.events.ProgressCompleteEvent;
import org.gradle.internal.logging.events.ProgressEvent;
import org.gradle.internal.logging.events.ProgressStartEvent;
import org.gradle.internal.logging.events.RenderableOutputEvent;
import org.gradle.internal.logging.events.StyledTextOutputEvent;
import org.gradle.util.GUtil;

import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;

import static org.gradle.internal.logging.text.StyledTextOutput.Style;

/**
 * An {@code org.gradle.logging.internal.OutputEventListener} implementation which generates output events to log the
 * progress of operations.
 */
public class ProgressLogEventGenerator extends BatchOutputEventListener {
    private static final String EOL = SystemProperties.getInstance().getLineSeparator();

    private final OutputEventListener listener;
    private final boolean deferHeader;
    private final Map<OperationIdentifier, Operation> operations = new LinkedHashMap<OperationIdentifier, Operation>();

    public ProgressLogEventGenerator(OutputEventListener listener, boolean deferHeader) {
        this.listener = listener;
        this.deferHeader = deferHeader;
    }

    public void onOutput(OutputEvent event) {
        if (event instanceof ProgressStartEvent) {
            onStart((ProgressStartEvent) event);
        } else if (event instanceof ProgressCompleteEvent) {
            onComplete((ProgressCompleteEvent) event);
        } else if (event instanceof RenderableOutputEvent) {
            doOutput((RenderableOutputEvent) event);
        } else if (!(event instanceof ProgressEvent)) {
            listener.onOutput(event);
        }
    }

    private void doOutput(RenderableOutputEvent event) {
        for (Operation operation : operations.values()) {
            operation.completeHeader();
        }
        listener.onOutput(event);
    }

    private void onComplete(ProgressCompleteEvent progressCompleteEvent) {
        assert !operations.isEmpty();
        Operation operation = operations.remove(progressCompleteEvent.getProgressOperationId());

        // Didn't find an operation with that id in the map
        if (operation == null) {
            // Remove last operation and complete that
            Iterator<Map.Entry<OperationIdentifier, Operation>> entryIterator = operations.entrySet().iterator();
            Map.Entry<OperationIdentifier, Operation> lastEntry = entryIterator.next();
            while (entryIterator.hasNext()) {
                lastEntry = entryIterator.next();
            }
            entryIterator.remove();
            operation = lastEntry.getValue();
            // TODO: Do we actually run into this case anymore?
        }
        completeOperation(progressCompleteEvent, operation);
    }

    private void completeOperation(ProgressCompleteEvent progressCompleteEvent, Operation operation) {
        operation.status = progressCompleteEvent.getStatus();
        operation.completeTime = progressCompleteEvent.getTimestamp();
        operation.complete();
    }

    private void onStart(ProgressStartEvent progressStartEvent) {
        Operation operation = new Operation(progressStartEvent.getCategory(), progressStartEvent.getLoggingHeader(), progressStartEvent.getTimestamp(), progressStartEvent.getBuildOperationId());
        operations.put(progressStartEvent.getProgressOperationId(), operation);

        if (!deferHeader || !(progressStartEvent.getLoggingHeader() != null && progressStartEvent.getLoggingHeader().equals(progressStartEvent.getShortDescription()))) {
            operation.startHeader();
        }
    }

    enum State {None, HeaderStarted, HeaderCompleted, Completed}

    private class Operation {
        private final Object buildOperationIdentifier;
        private final String category;
        private final String loggingHeader;
        private final long startTime;
        private final boolean hasLoggingHeader;
        private String status = "";
        private State state = State.None;
        private long completeTime;

        private Operation(String category, String loggingHeader, long startTime, Object buildOperationIdentifier) {
            this.category = category;
            this.loggingHeader = loggingHeader;
            this.startTime = startTime;
            this.hasLoggingHeader = GUtil.isTrue(loggingHeader);
            this.buildOperationIdentifier = buildOperationIdentifier;
        }

        private StyledTextOutputEvent plainTextEvent(long timestamp, String text) {
            return new StyledTextOutputEvent(timestamp, category, LogLevel.LIFECYCLE, buildOperationIdentifier, Collections.singletonList(new StyledTextOutputEvent.Span(text)));
        }

        private StyledTextOutputEvent styledTextEvent(long timestamp, StyledTextOutputEvent.Span... spans) {
            return new StyledTextOutputEvent(timestamp, category, LogLevel.LIFECYCLE, buildOperationIdentifier, Arrays.asList(spans));
        }

        private void doOutput(RenderableOutputEvent event) {
            for (Operation pending : operations.values()) {
                if (pending == this) {
                    break;
                }
                pending.completeHeader();
            }
            listener.onOutput(event);
        }

        public void startHeader() {
            assert state == State.None;
            if (hasLoggingHeader) {
                state = State.HeaderStarted;
                doOutput(plainTextEvent(startTime, loggingHeader));
            } else {
                state = State.HeaderCompleted;
            }
        }

        public void completeHeader() {
            switch (state) {
                case None:
                    if (hasLoggingHeader) {
                        listener.onOutput(plainTextEvent(startTime, loggingHeader + EOL));
                    }
                    break;
                case HeaderStarted:
                    listener.onOutput(plainTextEvent(startTime, EOL));
                    break;
                case HeaderCompleted:
                    return;
                default:
                    throw new IllegalStateException("state is " + state);
            }
            state = State.HeaderCompleted;
        }

        public void complete() {
            boolean hasStatus = GUtil.isTrue(status);
            switch (state) {
                case None:
                    if (hasLoggingHeader && hasStatus) {
                        doOutput(styledTextEvent(completeTime,
                            new StyledTextOutputEvent.Span(loggingHeader + ' '),
                            new StyledTextOutputEvent.Span(Style.ProgressStatus, status),
                            new StyledTextOutputEvent.Span(EOL)));
                    } else if (hasLoggingHeader) {
                        doOutput(plainTextEvent(completeTime, loggingHeader + EOL));
                    }
                    break;
                case HeaderStarted:
                    assert hasLoggingHeader;
                    if (hasStatus) {
                        doOutput(styledTextEvent(completeTime,
                            new StyledTextOutputEvent.Span(" "),
                            new StyledTextOutputEvent.Span(Style.ProgressStatus, status),
                            new StyledTextOutputEvent.Span(EOL)));
                    } else {
                        doOutput(plainTextEvent(completeTime, EOL));
                    }
                    break;
                case HeaderCompleted:
                    if (hasLoggingHeader && hasStatus) {
                        doOutput(styledTextEvent(completeTime,
                            new StyledTextOutputEvent.Span(loggingHeader + ' '),
                            new StyledTextOutputEvent.Span(Style.ProgressStatus, status),
                            new StyledTextOutputEvent.Span(EOL)));
                    }
                    break;
                default:
                    throw new IllegalStateException("state is " + state);
            }
            state = State.Completed;
        }
    }
}
