package com.github.triceo.splitlog;

import com.github.triceo.splitlog.api.*;
import com.github.triceo.splitlog.logging.SplitlogLoggerFactory;
import com.github.triceo.splitlog.util.SplitlogTailer;
import com.github.triceo.splitlog.util.SplitlogThreadFactory;
import org.apache.commons.io.input.Tailer;
import org.slf4j.Logger;

import java.lang.ref.WeakReference;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;

/**
 * Has a sole responsibility of starting and stopping {@link Tailer} thread when
 * told so by the {@link DefaultLogWatch}.
 */
final class LogWatchTailingManager {

    private static final ExecutorService EXECUTOR = Executors.newCachedThreadPool(new SplitlogThreadFactory("tails"));

    private static final Logger LOGGER = SplitlogLoggerFactory.getLogger(LogWatchTailingManager.class);
    private final int bufferSize;
    private MessageBuilder currentlyProcessedMessage;
    private final long delayBetweenReads;
    private final AtomicBoolean isReading = new AtomicBoolean(false);
    private final AtomicBoolean isTailing = new AtomicBoolean(false);
    private final AtomicLong numberOfTimesThatTailerWasStarted = new AtomicLong(0);
    private WeakReference<Message> previousAcceptedMessage;
    private final boolean reopenBetweenReads, ignoreExistingContent;
    private final TailSplitter splitter;
    private SplitlogTailer tailer;

    private final DefaultLogWatch watch;

    public LogWatchTailingManager(final DefaultLogWatch watch, final LogWatchBuilder builder,
        final TailSplitter splitter) {
        this.watch = watch;
        this.splitter = splitter;
        this.delayBetweenReads = builder.getDelayBetweenReads();
        this.bufferSize = builder.getReadingBufferSize();
        this.reopenBetweenReads = builder.isClosingBetweenReads();
        this.ignoreExistingContent = !builder.isReadingFromBeginning();
    }

    public DefaultLogWatch getWatch() {
        return this.watch;
    }

    protected void readingFinished() {
        if (!this.isReading.compareAndSet(true, false)) {
            return;
        }
        LogWatchTailingManager.LOGGER.info("Tailing stopped submitting lines.");
        if (this.currentlyProcessedMessage != null) {
            /*
             * there will be no more lines from the current reading burst; the
             * currently processed message must be marked as INCOMING with the
             * possibility of being finished in the subsequent reading burst(s).
             */
            this.getWatch().messageIncoming(this.currentlyProcessedMessage.buildIntermediate(this.splitter));
        }
    }

    protected void readingStarted() {
        if (!this.isReading.compareAndSet(false, true)) {
            return;
        }
        LogWatchTailingManager.LOGGER.info("Tailing will now start submitting lines.");
    }

    protected void readLine(final String line) {
        if (!this.isReading.get()) {
            LogWatchTailingManager.LOGGER.warn("Line '{}' received when the tailer shouldn't have been sending: {}.",
                    line, this);
            return;
        }
        final boolean isMessageBeingProcessed = this.currentlyProcessedMessage != null;
        if (this.splitter.isStartingLine(line)) {
            // new message begins
            if (isMessageBeingProcessed) { // finish old message
                LogWatchTailingManager.LOGGER.debug("Existing message will be finished.");
                final Message completeMessage = this.currentlyProcessedMessage.buildFinal(this.splitter);
                final MessageDeliveryStatus accepted = this.getWatch().messageArrived(completeMessage);
                this.currentlyProcessedMessage = null;
                if (accepted == null) {
                    LogWatchTailingManager.LOGGER.info("Message {} rejected at the gate to {}.", completeMessage, this);
                } else if (accepted == MessageDeliveryStatus.ACCEPTED) {
                    this.previousAcceptedMessage = new WeakReference<>(completeMessage);
                } else {
                    LogWatchTailingManager.LOGGER
                    .info("Message {} rejected from storage in {}.", completeMessage, this);
                }
            }
            // prepare for new message
            LogWatchTailingManager.LOGGER.debug("New message is being prepared.");
            this.currentlyProcessedMessage = new MessageBuilder(line);
            if (this.previousAcceptedMessage != null) {
                this.currentlyProcessedMessage.setPreviousMessage(this.previousAcceptedMessage.get());
            }
        } else {
            // continue present message
            if (!isMessageBeingProcessed) {
                LogWatchTailingManager.LOGGER.debug("Disregarding line as trash.");
                // most likely just a garbage immediately after start
                return;
            }
            LogWatchTailingManager.LOGGER.debug("Existing message is being updated.");
            this.currentlyProcessedMessage.add(line);
        }
        LogWatchTailingManager.LOGGER.debug("Line processing over.");
    }

    /**
     * Start the tailer on a separate thread. Only when a tailer is running can
     * {@link Follower}s be notified of new {@link Message}s from the log.
     *
     * @return True if the start was scheduled, false if scheduled already.
     */
    public boolean start() {
        if (!this.isTailing.compareAndSet(false, true)) {
            return false;
        }
        final boolean willReadFromEnd = this.willReadFromEnd();
        LogWatchTailingManager.LOGGER.debug("Tailer {} ignore existing file contents.", willReadFromEnd ? "will"
                : "won't");
        this.tailer = new SplitlogTailer(this.watch.getWatchedFile(), new LogWatchTailerListener(this),
                this.delayBetweenReads, this.willReadFromEnd(), this.reopenBetweenReads, this.bufferSize);
        LogWatchTailingManager.EXECUTOR.submit(this.tailer);
        final long start = System.nanoTime();
        this.tailer.waitUntilStarted();
        final long duration = System.nanoTime() - start;
        LogWatchTailingManager.LOGGER.debug("It took {} ms for the tailing to actually start.",
                TimeUnit.NANOSECONDS.toMillis(duration));
        final long iterationNum = this.numberOfTimesThatTailerWasStarted.incrementAndGet();
        LogWatchTailingManager.LOGGER.info("Tailing #{} started for {}.", iterationNum, this.watch);
        return true;
    }

    /**
     * Stop the tailer thread, preventing any {@link Follower}s from receiving
     * {@link Message}s.
     *
     * @return True if stopped, false if never running.
     */
    public boolean stop() {
        if (!this.isTailing.get()) {
            LogWatchTailingManager.LOGGER.debug("Tailer not running, therefore not terminating.");
            return false;
        }
        /*
         * terminate tailer; we stop the scheduler and the task will therefore
         * never be started again
         */
        this.tailer.stop();
        LogWatchTailingManager.LOGGER.info("Terminated tailing #{} for {}.",
                this.numberOfTimesThatTailerWasStarted.get(), this.watch);
        return true;
    }

    protected void tailingFinished() {
        this.isReading.set(false);
        LogWatchTailingManager.LOGGER.info("Tailing terminated.");
        if (this.currentlyProcessedMessage != null) {
            /*
             * there will be no more lines. the last message must be accepted or
             * rejected as well.
             */
            this.getWatch().messageArrived(this.currentlyProcessedMessage.buildFinal(this.splitter));
            this.currentlyProcessedMessage = null;
            this.previousAcceptedMessage = null;
        }
    }

    private boolean willReadFromEnd() {
        if (this.numberOfTimesThatTailerWasStarted.get() > 0) {
            return true;
        } else {
            return this.ignoreExistingContent;
        }
    }

}
