package org.xbib.logging.ext.handlers;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.util.logging.ErrorManager;
import org.xbib.logging.ExtLogRecord;
import org.xbib.logging.handlers.FileHandler;
import org.xbib.logging.ext.io.CountingOutputStream;
import org.xbib.logging.ext.util.SuffixRotator;

public class SizeRotatingFileHandler extends FileHandler {

    // by default, rotate at 10MB
    private long rotateSize = 0xa00000L;
    private int maxBackupIndex = 1;
    private CountingOutputStream outputStream;
    private boolean rotateOnBoot;
    private SuffixRotator suffixRotator = SuffixRotator.EMPTY;

    /**
     * Construct a new instance with no formatter and no output file.
     */
    public SizeRotatingFileHandler() {
    }

    /**
     * Construct a new instance with the given output file.
     *
     * @param file the file
     * @throws FileNotFoundException if the file could not be found on open
     */
    public SizeRotatingFileHandler(final File file) throws FileNotFoundException {
        super(file);
    }

    /**
     * Construct a new instance with the given output file and append setting.
     *
     * @param file   the file
     * @param append {@code true} to append, {@code false} to overwrite
     * @throws FileNotFoundException if the file could not be found on open
     */
    public SizeRotatingFileHandler(final File file, final boolean append) throws FileNotFoundException {
        super(file, append);
    }

    /**
     * Construct a new instance with the given output file.
     *
     * @param fileName the file name
     * @throws FileNotFoundException if the file could not be found on open
     */
    public SizeRotatingFileHandler(final String fileName) throws FileNotFoundException {
        super(fileName);
    }

    /**
     * Construct a new instance with the given output file and append setting.
     *
     * @param fileName the file name
     * @param append   {@code true} to append, {@code false} to overwrite
     * @throws FileNotFoundException if the file could not be found on open
     */
    public SizeRotatingFileHandler(final String fileName, final boolean append) throws FileNotFoundException {
        super(fileName, append);
    }

    /**
     * Construct a new instance with no formatter and no output file.
     */
    public SizeRotatingFileHandler(final long rotateSize, final int maxBackupIndex) {
        this.rotateSize = rotateSize;
        this.maxBackupIndex = maxBackupIndex;
    }

    /**
     * Construct a new instance with the given output file.
     *
     * @param file the file
     * @throws FileNotFoundException if the file could not be found on open
     */
    public SizeRotatingFileHandler(final File file, final long rotateSize, final int maxBackupIndex)
            throws FileNotFoundException {
        super(file);
        this.rotateSize = rotateSize;
        this.maxBackupIndex = maxBackupIndex;
    }

    /**
     * Construct a new instance with the given output file and append setting.
     *
     * @param file   the file
     * @param append {@code true} to append, {@code false} to overwrite
     * @throws FileNotFoundException if the file could not be found on open
     */
    public SizeRotatingFileHandler(final File file, final boolean append, final long rotateSize, final int maxBackupIndex)
            throws FileNotFoundException {
        super(file, append);
        this.rotateSize = rotateSize;
        this.maxBackupIndex = maxBackupIndex;
    }

    /**
     * {@inheritDoc}
     */
    public void setOutputStream(final OutputStream outputStream) {
        lock.lock();
        try {
            this.outputStream = outputStream == null ? null : new CountingOutputStream(outputStream);
            super.setOutputStream(this.outputStream);
        } finally {
            lock.unlock();
        }
    }

    /**
     * {@inheritDoc}
     *
     * @throws RuntimeException if there is an attempt to rotate file and the rotation fails
     */
    public void setFile(final File file) throws FileNotFoundException {
        lock.lock();
        try {
            // Check for a rotate
            if (rotateOnBoot && maxBackupIndex > 0 && file != null && file.exists() && file.length() > 0L) {
                // Make sure any previous files are closed before we attempt to rotate
                setFileInternal(null);
                suffixRotator.rotate(getErrorManager(), file.toPath(), maxBackupIndex);
            }
            setFileInternal(file);
        } finally {
            lock.unlock();
        }
    }

    /**
     * Indicates whether or a not the handler should rotate the file before the first log record is written.
     *
     * @return {@code true} if file should rotate on boot, otherwise {@code false}/
     */
    public boolean isRotateOnBoot() {
        lock.lock();
        try {
            return rotateOnBoot;
        } finally {
            lock.unlock();
        }
    }

    /**
     * Set to a value of {@code true} if the file should be rotated before the a new file is set. The rotation only
     * happens if the file names are the same and the file has a {@link File#length() length} greater than 0.
     *
     * @param rotateOnBoot {@code true} to rotate on boot, otherwise {@code false}
     */
    public void setRotateOnBoot(final boolean rotateOnBoot) {
        lock.lock();
        try {
            this.rotateOnBoot = rotateOnBoot;
        } finally {
            lock.unlock();
        }
    }

    /**
     * Set the rotation size, in bytes.
     *
     * @param rotateSize the number of bytes before the log is rotated
     */
    public void setRotateSize(final long rotateSize) {
        lock.lock();
        try {
            this.rotateSize = rotateSize;
        } finally {
            lock.unlock();
        }
    }

    /**
     * Set the maximum backup index (the number of log files to keep around).
     *
     * @param maxBackupIndex the maximum backup index
     */
    public void setMaxBackupIndex(final int maxBackupIndex) {
        lock.lock();
        try {
            this.maxBackupIndex = maxBackupIndex;
        } finally {
            lock.unlock();
        }
    }

    /**
     * Returns the suffix set to be appended to files during rotation.
     *
     * @return the suffix or {@code null} if no suffix should be used
     */
    public String getSuffix() {
        if (suffixRotator == SuffixRotator.EMPTY) {
            return null;
        }
        return suffixRotator.toString();
    }

    /**
     * Sets the suffix to be appended to the file name during the file rotation. The suffix does not play a role in
     * determining when the file should be rotated.
     * <p/>
     * The suffix must be a string understood by the {@link java.text.SimpleDateFormat}. Optionally the suffix can end
     * with {@code .gz} or {@code .zip} which will compress the file on rotation.
     * <p/>
     * <p>
     * If the suffix ends with {@code .gz} or {@code .zip} the file will be compressed on rotation.
     * </p>
     * <p>
     * <b>Note:</b> The {@link #setMaxBackupIndex(int) maxBackupIndex} only takes into account files rotated with same
     * suffix. For example if the suffix pattern is {@code .yyyy-MM-dd} and the size rotation is reached only files
     * with the same date suffix will be purged. A file the day before or after will not be purged.
     * </p>
     *
     * @param suffix the suffix to place after the filename when the file is rotated
     */
    public void setSuffix(final String suffix) {
        lock.lock();
        try {
            this.suffixRotator = SuffixRotator.parse(suffix);
        } finally {
            lock.unlock();
        }
    }

    /**
     * {@inheritDoc}
     */
    protected void preWrite(final ExtLogRecord record) {
        final int maxBackupIndex = this.maxBackupIndex;
        final long currentSize = (outputStream == null ? Long.MIN_VALUE : outputStream.currentSize);
        if (currentSize > rotateSize && maxBackupIndex > 0) {
            try {
                final File file = getFile();
                if (file == null) {
                    // no file is set; a direct output stream or writer was specified
                    return;
                }
                // close the old file.
                setFileInternal(null);
                suffixRotator.rotate(getErrorManager(), file.toPath(), maxBackupIndex);
                // start with new file.
                setFileInternal(file);
            } catch (IOException e) {
                reportError("Unable to rotate log file", e, ErrorManager.OPEN_FAILURE);
            }
        }
    }

    private void setFileInternal(final File file) throws FileNotFoundException {
        super.setFile(file);
        if (outputStream != null) {
            outputStream.currentSize = file == null ? 0L : file.length();
        }
    }
}
