/*
 * 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.commons.io.input;

import static org.apache.commons.io.IOUtils.CR;
import static org.apache.commons.io.IOUtils.EOF;
import static org.apache.commons.io.IOUtils.LF;

import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.attribute.FileTime;
import java.time.Duration;
import java.util.Arrays;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import org.apache.commons.io.IOUtils;
import org.apache.commons.io.ThreadUtils;
import org.apache.commons.io.build.AbstractOrigin;
import org.apache.commons.io.build.AbstractStreamBuilder;
import org.apache.commons.io.file.PathUtils;
import org.apache.commons.io.file.attribute.FileTimes;

/**
 * Simple implementation of the UNIX "tail -f" functionality.
 * <p>
 * To build an instance, see {@link Builder}.
 * </p>
 * <h2>1. Create a TailerListener implementation</h2>
 * <p>
 * First you need to create a {@link TailerListener} implementation; ({@link TailerListenerAdapter} is provided for
 * convenience so that you don't have to implement every method).
 * </p>
 *
 * <p>
 * For example:
 * </p>
 *
 * <pre>
 * public class MyTailerListener extends TailerListenerAdapter {
 *     public void handle(String line) {
 *         System.out.println(line);
 *     }
 * }
 * </pre>
 *
 * <h2>2. Using a Tailer</h2>
 *
 * <p>
 * You can create and use a Tailer in one of three ways:
 * </p>
 * <ul>
 * <li>Using a {@link Builder}</li>
 * <li>Using an {@link java.util.concurrent.Executor}</li>
 * <li>Using a {@link Thread}</li>
 * </ul>
 *
 * <p>
 * An example of each is shown below.
 * </p>
 *
 * <h3>2.1 Using a Builder</h3>
 *
 * <pre>
 * TailerListener listener = new MyTailerListener();
 * Tailer tailer = Tailer.builder()
 *   .setFile(file)
 *   .setTailerListener(listener)
 *   .setDelayDuration(delay)
 *   .get();
 * </pre>
 *
 * <h3>2.2 Using an Executor</h3>
 *
 * <pre>
 * TailerListener listener = new MyTailerListener();
 * Tailer tailer = new Tailer(file, listener, delay);
 *
 * // stupid executor impl. for demo purposes
 * Executor executor = new Executor() {
 *     public void execute(Runnable command) {
 *         command.run();
 *     }
 * };
 *
 * executor.execute(tailer);
 * </pre>
 *
 *
 * <h3>2.3 Using a Thread</h3>
 *
 * <pre>
 * TailerListener listener = new MyTailerListener();
 * Tailer tailer = new Tailer(file, listener, delay);
 * Thread thread = new Thread(tailer);
 * thread.setDaemon(true); // optional
 * thread.start();
 * </pre>
 *
 * <h2>3. Stopping a Tailer</h2>
 * <p>
 * Remember to stop the tailer when you have done with it:
 * </p>
 *
 * <pre>
 * tailer.stop();
 * </pre>
 *
 * <h2>4. Interrupting a Tailer</h2>
 * <p>
 * You can interrupt the thread a tailer is running on by calling {@link Thread#interrupt()}.
 * </p>
 *
 * <pre>
 * thread.interrupt();
 * </pre>
 * <p>
 * If you interrupt a tailer, the tailer listener is called with the {@link InterruptedException}.
 * </p>
 * <p>
 * The file is read using the default Charset; this can be overridden if necessary.
 * </p>
 *
 * @see TailerListener
 * @see TailerListenerAdapter
 * @since 2.0
 * @since 2.5 Updated behavior and documentation for {@link Thread#interrupt()}.
 * @since 2.12.0 Add {@link Tailable} and {@link RandomAccessResourceBridge} interfaces to tail of files accessed using
 *        alternative libraries such as jCIFS or <a href="https://commons.apache.org/proper/commons-vfs/">Apache Commons
 *        VFS</a>.
 */
public class Tailer implements Runnable, AutoCloseable {

    /**
     * Builds a {@link Tailer} with default values.
     * <p>
     * For example:
     * </p>
     * <pre>{@code
     * Tailer t = Tailer.builder()
     *   .setPath(path)
     *   .setCharset(StandardCharsets.UTF_8)
     *   .setDelayDuration(Duration.ofSeconds(1))
     *   .setExecutorService(Executors.newSingleThreadExecutor(Builder::newDaemonThread))
     *   .setReOpen(false)
     *   .setStartThread(true)
     *   .setTailable(tailable)
     *   .setTailerListener(tailerListener)
     *   .setTailFromEnd(false)
     *   .get();}
     * </pre>
     *
     * @since 2.12.0
     */
    public static class Builder extends AbstractStreamBuilder<Tailer, Builder> {

        private static final Duration DEFAULT_DELAY_DURATION = Duration.ofMillis(DEFAULT_DELAY_MILLIS);

        /**
         * Creates a new daemon thread.
         *
         * @param runnable the thread's runnable.
         * @return a new daemon thread.
         */
        private static Thread newDaemonThread(final Runnable runnable) {
            final Thread thread = new Thread(runnable, "commons-io-tailer");
            thread.setDaemon(true);
            return thread;
        }

        private Tailable tailable;
        private TailerListener tailerListener;
        private Duration delayDuration = DEFAULT_DELAY_DURATION;
        private boolean end;
        private boolean reOpen;
        private boolean startThread = true;
        private ExecutorService executorService = Executors.newSingleThreadExecutor(Builder::newDaemonThread);

        /**
         * Constructs a new instance.
         * <p>
         * This builder use the aspects tailable, Charset, TailerListener, delayDuration, end, reOpen, buffer size.
         * </p>
         *
         * @return a new instance.
         */
        @Override
        public Tailer get() {
            final Tailer tailer = new Tailer(tailable, getCharset(), tailerListener, delayDuration, end, reOpen, getBufferSize());
            if (startThread) {
                executorService.submit(tailer);
            }
            return tailer;
        }

        /**
         * Sets the delay duration. null resets to the default delay of one second.
         *
         * @param delayDuration the delay between checks of the file for new content.
         * @return this
         */
        public Builder setDelayDuration(final Duration delayDuration) {
            this.delayDuration = delayDuration != null ? delayDuration : DEFAULT_DELAY_DURATION;
            return this;
        }

        /**
         * Sets the executor service to use when startThread is true.
         *
         * @param executorService the executor service to use when startThread is true.
         * @return this
         */
        public Builder setExecutorService(final ExecutorService executorService) {
            this.executorService = Objects.requireNonNull(executorService, "executorService");
            return this;
        }

        /**
         * Sets the origin.
         *
         * @throws UnsupportedOperationException if the origin cannot be converted to a Path.
         */
        @Override
        protected Builder setOrigin(final AbstractOrigin<?, ?> origin) {
            setTailable(new TailablePath(origin.getPath()));
            return super.setOrigin(origin);
        }

        /**
         * Sets the re-open behavior.
         *
         * @param reOpen whether to close/reopen the file between chunks
         * @return this
         */
        public Builder setReOpen(final boolean reOpen) {
            this.reOpen = reOpen;
            return this;
        }

        /**
         * Sets the daemon thread startup behavior.
         *
         * @param startThread whether to create a daemon thread automatically.
         * @return this
         */
        public Builder setStartThread(final boolean startThread) {
            this.startThread = startThread;
            return this;
        }

        /**
         * Sets the tailable.
         *
         * @param tailable the tailable.
         * @return this.
         */
        public Builder setTailable(final Tailable tailable) {
            this.tailable = Objects.requireNonNull(tailable, "tailable");
            return this;
        }

        /**
         * Sets the listener.
         *
         * @param tailerListener the listener.
         * @return this
         */
        public Builder setTailerListener(final TailerListener tailerListener) {
            this.tailerListener = Objects.requireNonNull(tailerListener, "tailerListener");
            return this;
        }

        /**
         * Sets the tail start behavior.
         *
         * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
         * @return this
         */
        public Builder setTailFromEnd(final boolean end) {
            this.end = end;
            return this;
        }
    }

    /**
     * Bridges random access to a {@link RandomAccessFile}.
     */
    private static final class RandomAccessFileBridge implements RandomAccessResourceBridge {

        private final RandomAccessFile randomAccessFile;

        private RandomAccessFileBridge(final File file, final String mode) throws FileNotFoundException {
            randomAccessFile = new RandomAccessFile(file, mode);
        }

        @Override
        public void close() throws IOException {
            randomAccessFile.close();
        }

        @Override
        public long getPointer() throws IOException {
            return randomAccessFile.getFilePointer();
        }

        @Override
        public int read(final byte[] b) throws IOException {
            return randomAccessFile.read(b);
        }

        @Override
        public void seek(final long position) throws IOException {
            randomAccessFile.seek(position);
        }

    }

    /**
     * Bridges access to a resource for random access, normally a file. Allows substitution of remote files for example
     * using jCIFS.
     *
     * @since 2.12.0
     */
    public interface RandomAccessResourceBridge extends Closeable {

        /**
         * Gets the current offset in this tailable.
         *
         * @return the offset from the beginning of the tailable, in bytes, at which the next read or write occurs.
         * @throws IOException if an I/O error occurs.
         */
        long getPointer() throws IOException;

        /**
         * Reads up to {@code b.length} bytes of data from this tailable into an array of bytes. This method blocks until at
         * least one byte of input is available.
         *
         * @param b the buffer into which the data is read.
         * @return the total number of bytes read into the buffer, or {@code -1} if there is no more data because the end of
         *         this tailable has been reached.
         * @throws IOException If the first byte cannot be read for any reason other than end of tailable, or if the random
         *         access tailable has been closed, or if some other I/O error occurs.
         */
        int read(final byte[] b) throws IOException;

        /**
         * Sets the file-pointer offset, measured from the beginning of this tailable, at which the next read or write occurs.
         * The offset may be set beyond the end of the tailable. Setting the offset beyond the end of the tailable does not
         * change the tailable length. The tailable length will change only by writing after the offset has been set beyond the
         * end of the tailable.
         *
         * @param pos the offset position, measured in bytes from the beginning of the tailable, at which to set the tailable
         *        pointer.
         * @throws IOException if {@code pos} is less than {@code 0} or if an I/O error occurs.
         */
        void seek(final long pos) throws IOException;
    }

    /**
     * A tailable resource like a file.
     *
     * @since 2.12.0
     */
    public interface Tailable {

        /**
         * Creates a random access file stream to read.
         *
         * @param mode the access mode, by default this is for {@link RandomAccessFile}.
         * @return a random access file stream to read.
         * @throws FileNotFoundException if the tailable object does not exist.
         */
        RandomAccessResourceBridge getRandomAccess(final String mode) throws FileNotFoundException;

        /**
         * Tests if this tailable is newer than the specified {@link FileTime}.
         *
         * @param fileTime the file time reference.
         * @return true if the {@link File} exists and has been modified after the given {@link FileTime}.
         * @throws IOException if an I/O error occurs.
         */
        boolean isNewer(final FileTime fileTime) throws IOException;

        /**
         * Gets the last modification {@link FileTime}.
         *
         * @return See {@link java.nio.file.Files#getLastModifiedTime(Path, LinkOption...)}.
         * @throws IOException if an I/O error occurs.
         */
        FileTime lastModifiedFileTime() throws IOException;

        /**
         * Gets the size of this tailable.
         *
         * @return The size, in bytes, of this tailable, or {@code 0} if the file does not exist. Some operating systems may
         *         return {@code 0} for path names denoting system-dependent entities such as devices or pipes.
         * @throws IOException if an I/O error occurs.
         */
        long size() throws IOException;
    }

    /**
     * A tailable for a file {@link Path}.
     */
    private static final class TailablePath implements Tailable {

        private final Path path;
        private final LinkOption[] linkOptions;

        private TailablePath(final Path path, final LinkOption... linkOptions) {
            this.path = Objects.requireNonNull(path, "path");
            this.linkOptions = linkOptions;
        }

        Path getPath() {
            return path;
        }

        @Override
        public RandomAccessResourceBridge getRandomAccess(final String mode) throws FileNotFoundException {
            return new RandomAccessFileBridge(path.toFile(), mode);
        }

        @Override
        public boolean isNewer(final FileTime fileTime) throws IOException {
            return PathUtils.isNewer(path, fileTime, linkOptions);
        }

        @Override
        public FileTime lastModifiedFileTime() throws IOException {
            return Files.getLastModifiedTime(path, linkOptions);
        }

        @Override
        public long size() throws IOException {
            return Files.size(path);
        }

        @Override
        public String toString() {
            return "TailablePath [file=" + path + ", linkOptions=" + Arrays.toString(linkOptions) + "]";
        }
    }

    private static final int DEFAULT_DELAY_MILLIS = 1000;

    private static final String RAF_READ_ONLY_MODE = "r";

    // The default charset used for reading files
    private static final Charset DEFAULT_CHARSET = Charset.defaultCharset();

    /**
     * Constructs a new {@link Builder}.
     *
     * @return Creates a new {@link Builder}.
     * @since 2.12.0
     */
    public static Builder builder() {
        return new Builder();
    }

    /**
     * Creates and starts a Tailer for the given file.
     *
     * @param file the file to follow.
     * @param charset the character set to use for reading the file.
     * @param listener the TailerListener to use.
     * @param delayMillis the delay between checks of the file for new content in milliseconds.
     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
     * @param reOpen whether to close/reopen the file between chunks.
     * @param bufferSize buffer size.
     * @return The new tailer.
     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}.
     */
    @Deprecated
    public static Tailer create(final File file, final Charset charset, final TailerListener listener, final long delayMillis, final boolean end,
        final boolean reOpen, final int bufferSize) {
        //@formatter:off
        return builder()
                .setFile(file)
                .setTailerListener(listener)
                .setCharset(charset)
                .setDelayDuration(Duration.ofMillis(delayMillis))
                .setTailFromEnd(end)
                .setReOpen(reOpen)
                .setBufferSize(bufferSize)
                .get();
        //@formatter:on
    }

    /**
     * Creates and starts a Tailer for the given file, starting at the beginning of the file with the default delay of 1.0s
     *
     * @param file the file to follow.
     * @param listener the TailerListener to use.
     * @return The new tailer.
     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}.
     */
    @Deprecated
    public static Tailer create(final File file, final TailerListener listener) {
        //@formatter:off
        return builder()
                .setFile(file)
                .setTailerListener(listener)
                .get();
        //@formatter:on
    }

    /**
     * Creates and starts a Tailer for the given file, starting at the beginning of the file
     *
     * @param file the file to follow.
     * @param listener the TailerListener to use.
     * @param delayMillis the delay between checks of the file for new content in milliseconds.
     * @return The new tailer.
     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}.
     */
    @Deprecated
    public static Tailer create(final File file, final TailerListener listener, final long delayMillis) {
        //@formatter:off
        return builder()
                .setFile(file)
                .setTailerListener(listener)
                .setDelayDuration(Duration.ofMillis(delayMillis))
                .get();
        //@formatter:on
    }

    /**
     * Creates and starts a Tailer for the given file with default buffer size.
     *
     * @param file the file to follow.
     * @param listener the TailerListener to use.
     * @param delayMillis the delay between checks of the file for new content in milliseconds.
     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
     * @return The new tailer.
     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}.
     */
    @Deprecated
    public static Tailer create(final File file, final TailerListener listener, final long delayMillis, final boolean end) {
        //@formatter:off
        return builder()
                .setFile(file)
                .setTailerListener(listener)
                .setDelayDuration(Duration.ofMillis(delayMillis))
                .setTailFromEnd(end)
                .get();
        //@formatter:on
    }

    /**
     * Creates and starts a Tailer for the given file with default buffer size.
     *
     * @param file the file to follow.
     * @param listener the TailerListener to use.
     * @param delayMillis the delay between checks of the file for new content in milliseconds.
     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
     * @param reOpen whether to close/reopen the file between chunks.
     * @return The new tailer.
     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}.
     */
    @Deprecated
    public static Tailer create(final File file, final TailerListener listener, final long delayMillis, final boolean end, final boolean reOpen) {
        //@formatter:off
        return builder()
                .setFile(file)
                .setTailerListener(listener)
                .setDelayDuration(Duration.ofMillis(delayMillis))
                .setTailFromEnd(end)
                .setReOpen(reOpen)
                .get();
        //@formatter:on
    }

    /**
     * Creates and starts a Tailer for the given file.
     *
     * @param file the file to follow.
     * @param listener the TailerListener to use.
     * @param delayMillis the delay between checks of the file for new content in milliseconds.
     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
     * @param reOpen whether to close/reopen the file between chunks.
     * @param bufferSize buffer size.
     * @return The new tailer.
     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}.
     */
    @Deprecated
    public static Tailer create(final File file, final TailerListener listener, final long delayMillis, final boolean end, final boolean reOpen,
        final int bufferSize) {
        //@formatter:off
        return builder()
                .setFile(file)
                .setTailerListener(listener)
                .setDelayDuration(Duration.ofMillis(delayMillis))
                .setTailFromEnd(end)
                .setReOpen(reOpen)
                .setBufferSize(bufferSize)
                .get();
        //@formatter:on
    }

    /**
     * Creates and starts a Tailer for the given file.
     *
     * @param file the file to follow.
     * @param listener the TailerListener to use.
     * @param delayMillis the delay between checks of the file for new content in milliseconds.
     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
     * @param bufferSize buffer size.
     * @return The new tailer.
     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}.
     */
    @Deprecated
    public static Tailer create(final File file, final TailerListener listener, final long delayMillis, final boolean end, final int bufferSize) {
        //@formatter:off
        return builder()
                .setFile(file)
                .setTailerListener(listener)
                .setDelayDuration(Duration.ofMillis(delayMillis))
                .setTailFromEnd(end)
                .setBufferSize(bufferSize)
                .get();
        //@formatter:on
    }

    /**
     * Buffer on top of RandomAccessResourceBridge.
     */
    private final byte[] inbuf;

    /**
     * The file which will be tailed.
     */
    private final Tailable tailable;

    /**
     * The character set that will be used to read the file.
     */
    private final Charset charset;

    /**
     * The amount of time to wait for the file to be updated.
     */
    private final Duration delayDuration;

    /**
     * Whether to tail from the end or start of file
     */
    private final boolean tailAtEnd;

    /**
     * The listener to notify of events when tailing.
     */
    private final TailerListener listener;

    /**
     * Whether to close and reopen the file whilst waiting for more input.
     */
    private final boolean reOpen;

    /**
     * The tailer will run as long as this value is true.
     */
    private volatile boolean run = true;

    /**
     * Creates a Tailer for the given file, with a specified buffer size.
     *
     * @param file the file to follow.
     * @param charset the Charset to be used for reading the file
     * @param listener the TailerListener to use.
     * @param delayMillis the delay between checks of the file for new content in milliseconds.
     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
     * @param reOpen if true, close and reopen the file between reading chunks
     * @param bufSize Buffer size
     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}.
     */
    @Deprecated
    public Tailer(final File file, final Charset charset, final TailerListener listener, final long delayMillis, final boolean end, final boolean reOpen,
        final int bufSize) {
        this(new TailablePath(file.toPath()), charset, listener, Duration.ofMillis(delayMillis), end, reOpen, bufSize);
    }

    /**
     * Creates a Tailer for the given file, starting from the beginning, with the default delay of 1.0s.
     *
     * @param file The file to follow.
     * @param listener the TailerListener to use.
     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}.
     */
    @Deprecated
    public Tailer(final File file, final TailerListener listener) {
        this(file, listener, DEFAULT_DELAY_MILLIS);
    }

    /**
     * Creates a Tailer for the given file, starting from the beginning.
     *
     * @param file the file to follow.
     * @param listener the TailerListener to use.
     * @param delayMillis the delay between checks of the file for new content in milliseconds.
     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}.
     */
    @Deprecated
    public Tailer(final File file, final TailerListener listener, final long delayMillis) {
        this(file, listener, delayMillis, false);
    }

    /**
     * Creates a Tailer for the given file, with a delay other than the default 1.0s.
     *
     * @param file the file to follow.
     * @param listener the TailerListener to use.
     * @param delayMillis the delay between checks of the file for new content in milliseconds.
     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}.
     */
    @Deprecated
    public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end) {
        this(file, listener, delayMillis, end, IOUtils.DEFAULT_BUFFER_SIZE);
    }

    /**
     * Creates a Tailer for the given file, with a delay other than the default 1.0s.
     *
     * @param file the file to follow.
     * @param listener the TailerListener to use.
     * @param delayMillis the delay between checks of the file for new content in milliseconds.
     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
     * @param reOpen if true, close and reopen the file between reading chunks
     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}.
     */
    @Deprecated
    public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end, final boolean reOpen) {
        this(file, listener, delayMillis, end, reOpen, IOUtils.DEFAULT_BUFFER_SIZE);
    }

    /**
     * Creates a Tailer for the given file, with a specified buffer size.
     *
     * @param file the file to follow.
     * @param listener the TailerListener to use.
     * @param delayMillis the delay between checks of the file for new content in milliseconds.
     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
     * @param reOpen if true, close and reopen the file between reading chunks
     * @param bufferSize Buffer size
     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}.
     */
    @Deprecated
    public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end, final boolean reOpen, final int bufferSize) {
        this(file, DEFAULT_CHARSET, listener, delayMillis, end, reOpen, bufferSize);
    }

    /**
     * Creates a Tailer for the given file, with a specified buffer size.
     *
     * @param file the file to follow.
     * @param listener the TailerListener to use.
     * @param delayMillis the delay between checks of the file for new content in milliseconds.
     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
     * @param bufferSize Buffer size
     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}.
     */
    @Deprecated
    public Tailer(final File file, final TailerListener listener, final long delayMillis, final boolean end, final int bufferSize) {
        this(file, listener, delayMillis, end, false, bufferSize);
    }

    /**
     * Creates a Tailer for the given file, with a specified buffer size.
     *
     * @param tailable the file to follow.
     * @param charset the Charset to be used for reading the file
     * @param listener the TailerListener to use.
     * @param delayDuration the delay between checks of the file for new content in milliseconds.
     * @param end Set to true to tail from the end of the file, false to tail from the beginning of the file.
     * @param reOpen if true, close and reopen the file between reading chunks
     * @param bufferSize Buffer size
     */
    private Tailer(final Tailable tailable, final Charset charset, final TailerListener listener, final Duration delayDuration, final boolean end,
        final boolean reOpen, final int bufferSize) {
        this.tailable = Objects.requireNonNull(tailable, "tailable");
        this.listener = Objects.requireNonNull(listener, "listener");
        this.delayDuration = delayDuration;
        this.tailAtEnd = end;
        this.inbuf = IOUtils.byteArray(bufferSize);

        // Save and prepare the listener
        listener.init(this);
        this.reOpen = reOpen;
        this.charset = charset;
    }

    /**
     * Requests the tailer to complete its current loop and return.
     */
    @Override
    public void close() {
        this.run = false;
    }

    /**
     * Gets the delay in milliseconds.
     *
     * @return the delay in milliseconds.
     * @deprecated Use {@link #getDelayDuration()}.
     */
    @Deprecated
    public long getDelay() {
        return delayDuration.toMillis();
    }

    /**
     * Gets the delay Duration.
     *
     * @return the delay Duration.
     * @since 2.12.0
     */
    public Duration getDelayDuration() {
        return delayDuration;
    }

    /**
     * Gets the file.
     *
     * @return the file
     * @throws IllegalStateException if constructed using a user provided {@link Tailable} implementation
     */
    public File getFile() {
        if (tailable instanceof TailablePath) {
            return ((TailablePath) tailable).getPath().toFile();
        }
        throw new IllegalStateException("Cannot extract java.io.File from " + tailable.getClass().getName());
    }

    /**
     * Gets whether to keep on running.
     *
     * @return whether to keep on running.
     * @since 2.5
     */
    protected boolean getRun() {
        return run;
    }

    /**
     * Gets the Tailable.
     *
     * @return the Tailable
     * @since 2.12.0
     */
    public Tailable getTailable() {
        return tailable;
    }

    /**
     * Reads new lines.
     *
     * @param reader The file to read
     * @return The new position after the lines have been read
     * @throws IOException if an I/O error occurs.
     */
    private long readLines(final RandomAccessResourceBridge reader) throws IOException {
        try (ByteArrayOutputStream lineBuf = new ByteArrayOutputStream(64)) {
            long pos = reader.getPointer();
            long rePos = pos; // position to re-read
            int num;
            boolean seenCR = false;
            while (getRun() && (num = reader.read(inbuf)) != EOF) {
                for (int i = 0; i < num; i++) {
                    final byte ch = inbuf[i];
                    switch (ch) {
                    case LF:
                        seenCR = false; // swallow CR before LF
                        listener.handle(new String(lineBuf.toByteArray(), charset));
                        lineBuf.reset();
                        rePos = pos + i + 1;
                        break;
                    case CR:
                        if (seenCR) {
                            lineBuf.write(CR);
                        }
                        seenCR = true;
                        break;
                    default:
                        if (seenCR) {
                            seenCR = false; // swallow final CR
                            listener.handle(new String(lineBuf.toByteArray(), charset));
                            lineBuf.reset();
                            rePos = pos + i + 1;
                        }
                        lineBuf.write(ch);
                    }
                }
                pos = reader.getPointer();
            }

            reader.seek(rePos); // Ensure we can re-read if necessary

            if (listener instanceof TailerListenerAdapter) {
                ((TailerListenerAdapter) listener).endOfFileReached();
            }

            return rePos;
        }
    }

    /**
     * Follows changes in the file, calling {@link TailerListener#handle(String)} with each new line.
     */
    @Override
    public void run() {
        RandomAccessResourceBridge reader = null;
        try {
            FileTime last = FileTimes.EPOCH; // The last time the file was checked for changes
            long position = 0; // position within the file
            // Open the file
            while (getRun() && reader == null) {
                try {
                    reader = tailable.getRandomAccess(RAF_READ_ONLY_MODE);
                } catch (final FileNotFoundException e) {
                    listener.fileNotFound();
                }
                if (reader == null) {
                    ThreadUtils.sleep(delayDuration);
                } else {
                    // The current position in the file
                    position = tailAtEnd ? tailable.size() : 0;
                    last = tailable.lastModifiedFileTime();
                    reader.seek(position);
                }
            }
            while (getRun()) {
                final boolean newer = tailable.isNewer(last); // IO-279, must be done first
                // Check the file length to see if it was rotated
                final long length = tailable.size();
                if (length < position) {
                    // File was rotated
                    listener.fileRotated();
                    // Reopen the reader after rotation ensuring that the old file is closed iff we re-open it
                    // successfully
                    try (RandomAccessResourceBridge save = reader) {
                        reader = tailable.getRandomAccess(RAF_READ_ONLY_MODE);
                        // At this point, we're sure that the old file is rotated
                        // Finish scanning the old file and then we'll start with the new one
                        try {
                            readLines(save);
                        } catch (final IOException ioe) {
                            listener.handle(ioe);
                        }
                        position = 0;
                    } catch (final FileNotFoundException e) {
                        // in this case we continue to use the previous reader and position values
                        listener.fileNotFound();
                        ThreadUtils.sleep(delayDuration);
                    }
                    continue;
                }
                // File was not rotated
                // See if the file needs to be read again
                if (length > position) {
                    // The file has more content than it did last time
                    position = readLines(reader);
                    last = tailable.lastModifiedFileTime();
                } else if (newer) {
                    /*
                     * This can happen if the file is truncated or overwritten with the exact same length of information. In cases like
                     * this, the file position needs to be reset
                     */
                    position = 0;
                    reader.seek(position); // cannot be null here

                    // Now we can read new lines
                    position = readLines(reader);
                    last = tailable.lastModifiedFileTime();
                }
                if (reOpen && reader != null) {
                    reader.close();
                }
                ThreadUtils.sleep(delayDuration);
                if (getRun() && reOpen) {
                    reader = tailable.getRandomAccess(RAF_READ_ONLY_MODE);
                    reader.seek(position);
                }
            }
        } catch (final InterruptedException e) {
            Thread.currentThread().interrupt();
            listener.handle(e);
        } catch (final Exception e) {
            listener.handle(e);
        } finally {
            try {
                IOUtils.close(reader);
            } catch (final IOException e) {
                listener.handle(e);
            }
            close();
        }
    }

    /**
     * Requests the tailer to complete its current loop and return.
     *
     * @deprecated Use {@link #close()}.
     */
    @Deprecated
    public void stop() {
        close();
    }
}
