/*
 * Bitronix Transaction Manager
 *
 * Copyright (c) 2010, Bitronix Software.
 *
 * This copyrighted material is made available to anyone wishing to use, modify,
 * copy, or redistribute it subject to the terms and conditions of the GNU
 * Lesser General Public License, as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
 * for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this distribution; if not, write to:
 * Free Software Foundation, Inc.
 * 51 Franklin Street, Fifth Floor
 * Boston, MA 02110-1301 USA
 */
package bitronix.tm.journal;

import bitronix.tm.utils.Decoder;
import org.slf4j.LoggerFactory;
import org.slf4j.Logger;

import java.io.IOException;
import java.io.RandomAccessFile;

/**
 * Used to control a log file's header.
 * <p>The physical data is read when this object is created then cached. Calling setter methods sets the header field
 * then moves the file pointer back to the previous location.</p>
 *
 * @author lorban
 */
public class TransactionLogHeader {

    private final static Logger log = LoggerFactory.getLogger(TransactionLogHeader.class);

    /**
     * Position of the format ID in the header (see {@link bitronix.tm.BitronixXid#FORMAT_ID}).
     */
    public final static int FORMAT_ID_HEADER = 0;

    /**
     * Position of the timestamp in the header.
     */
    public final static int TIMESTAMP_HEADER = FORMAT_ID_HEADER + 4;

    /**
     * Position of the log file state in the header.
     */
    public final static int STATE_HEADER = TIMESTAMP_HEADER + 8;

    /**
     * Position of the current log position in the header.
     */
    public final static int CURRENT_POSITION_HEADER = STATE_HEADER + 1;

    /**
     * Total length of the header.
     */
    public final static int HEADER_LENGTH = CURRENT_POSITION_HEADER + 8;

    /**
     * State of the log file when it has been closed properly.
     */
    public final static byte CLEAN_LOG_STATE = 0;

    /**
     * State of the log file when it hasn't been closed properly or it is still open.
     */
    public final static byte UNCLEAN_LOG_STATE = -1;


    private final RandomAccessFile randomAccessFile;
    private int formatId;
    private long timestamp;
    private byte state;
    private long position;
    private long maxFileLength;

    /**
     * TransactionLogHeader are used to control headers of the specified RandomAccessFile.
     * All calls to setters are synchronized on the passed-in RandomAccessFile.
     * @param randomAccessFile the random access file to read from.
     * @param maxFileLength the max file length.
     * @throws IOException if an I/O error occurs.
     */
    public TransactionLogHeader(RandomAccessFile randomAccessFile, long maxFileLength) throws IOException {
        this.randomAccessFile = randomAccessFile;
        this.maxFileLength = maxFileLength;

        randomAccessFile.seek(FORMAT_ID_HEADER);
        formatId = randomAccessFile.readInt();
        timestamp = randomAccessFile.readLong();
        state = randomAccessFile.readByte();
        position = randomAccessFile.readLong();
        randomAccessFile.seek(position);

        if (log.isDebugEnabled()) log.debug("read header " + this);
    }

    /**
     * Get FORMAT_ID_HEADER.
     * @see #FORMAT_ID_HEADER
     * @return the FORMAT_ID_HEADER value.
     */
    public int getFormatId() {
        return formatId;
    }

    /**
     * Get TIMESTAMP_HEADER.
     * @see #TIMESTAMP_HEADER
     * @return the TIMESTAMP_HEADER value.
     */
    public long getTimestamp() {
        return timestamp;
    }

    /**
     * Get STATE_HEADER.
     * @see #STATE_HEADER
     * @return the STATE_HEADER value.
     */
    public byte getState() {
        return state;
    }

    /**
     * Get CURRENT_POSITION_HEADER.
     * @see #CURRENT_POSITION_HEADER
     * @return the CURRENT_POSITION_HEADER value.
     */
    public long getPosition() {
        return position;
    }

    /**
     * Set FORMAT_ID_HEADER.
     * @see #FORMAT_ID_HEADER
     * @param formatId the FORMAT_ID_HEADER value.
     * @throws IOException if an I/O error occurs.
     */
    public void setFormatId(int formatId) throws IOException {
        synchronized (randomAccessFile) {
            long currentPos = randomAccessFile.getFilePointer();
            randomAccessFile.seek(FORMAT_ID_HEADER);
            randomAccessFile.writeInt(formatId);
            randomAccessFile.seek(currentPos);
        }

        this.formatId = formatId;
    }

    /**
     * Set TIMESTAMP_HEADER.
     * @see #TIMESTAMP_HEADER
     * @param timestamp the TIMESTAMP_HEADER value.
     * @throws IOException if an I/O error occurs.
     */
    public void setTimestamp(long timestamp) throws IOException {
        synchronized (randomAccessFile) {
            long currentPos = randomAccessFile.getFilePointer();
            randomAccessFile.seek(TIMESTAMP_HEADER);
            randomAccessFile.writeLong(timestamp);
            randomAccessFile.seek(currentPos);
        }

        this.timestamp = timestamp;
    }

    /**
     * Set STATE_HEADER.
     * @see #STATE_HEADER
     * @param state the STATE_HEADER value.
     * @throws IOException if an I/O error occurs.
     */
    public void setState(byte state) throws IOException {
        synchronized (randomAccessFile) {
            long currentPos = randomAccessFile.getFilePointer();
            randomAccessFile.seek(STATE_HEADER);
            randomAccessFile.writeByte(state);
            randomAccessFile.seek(currentPos);
        }

        this.state = state;
    }

    /**
     * Set CURRENT_POSITION_HEADER.
     * @see #CURRENT_POSITION_HEADER
     * @param position the CURRENT_POSITION_HEADER value.
     * @throws IOException if an I/O error occurs.
     */
    public void setPosition(long position) throws IOException {
        if (position < HEADER_LENGTH)
            throw new IOException("invalid position " + position + " (too low)");
        if (position >= maxFileLength)
            throw new IOException("invalid position " + position + " (too high)");

        synchronized (randomAccessFile) {
            randomAccessFile.seek(CURRENT_POSITION_HEADER);
            randomAccessFile.writeLong(position);
            randomAccessFile.seek(position);
        }

        this.position = position;
    }

    /**
     * Advance CURRENT_POSITION_HEADER.
     * @see #setPosition
     * @param distance the value to add to the current position.
     * @throws IOException if an I/O error occurs.
     */
    public void goAhead(long distance) throws IOException {
        setPosition(getPosition() + distance);
    }

    /**
     * Rewind CURRENT_POSITION_HEADER back to the beginning of the file.
     * @see #setPosition
     * @throws IOException if an I/O error occurs.
     */
    public void rewind() throws IOException {
        setPosition(HEADER_LENGTH);
    }

    /**
     * Create human-readable String representation.
     * @return a human-readable String representing this object's state.
     */
    public String toString() {
        return "a Bitronix TransactionLogHeader with timestamp=" + timestamp +
                ", state=" + Decoder.decodeHeaderState(state) +
                ", position=" + position;
    }

}
