/**
 *
 * Copyright 2004 Hiram Chirino
 *
 * 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.codehaus.activemq.journal.impl;

import org.codehaus.activemq.journal.InvalidRecordLocationException;

import java.io.File;
import java.io.IOException;
import java.text.NumberFormat;
import java.util.Iterator;
import java.util.LinkedList;

/**
 * Provides a logical view of many seperate files as one single long
 * log file.  The seperate files that compose the LogFile are Segements
 * of the LogFile.
 * <p/>
 * This class is not thread safe.
 *
 * @version $Revision: 1.1 $
 */
final public class LogFile {

    static final public byte DATA_RECORD_TYPE = 1;
    static final public byte MARK_RECORD_TYPE = 2;
    static final private NumberFormat onlineLogNameFormat = NumberFormat.getNumberInstance();

    static {
        onlineLogNameFormat.setMinimumIntegerDigits(3);
        onlineLogNameFormat.setMaximumIntegerDigits(3);
        onlineLogNameFormat.setGroupingUsed(false);
        onlineLogNameFormat.setParseIntegerOnly(true);
        onlineLogNameFormat.setMaximumFractionDigits(0);
    }

    // Configuration variables.
    private final File logDirectory;
    private final int initialSegmentSize;
    private boolean closed;

    // Keeps track of the online segments.
    private final Segment segments[];
    private final LinkedList activeSegments = new LinkedList();
    private final LinkedList inactiveSegments = new LinkedList();;

    private byte markSegment = -1;
    private final Mark lastMark = new Mark();
    private byte appendSegment = -1;
    private int lastSegmentId = -1;

    public LogFile(File logDirectory) throws IOException {
        this(logDirectory, 4, 1024 * 1024 * 5);
    }

    public LogFile(File logDirectory, int onlineSegmentCount, int initialSegmentSize) throws IOException {
        this.logDirectory = logDirectory;
        this.segments = new Segment[onlineSegmentCount];
        this.initialSegmentSize = initialSegmentSize;
        initialize();
    }

    /**
     * Creates/Loads the log segments.
     *
     * @throws IOException
     */
    private void initialize() throws IOException {

// Create the log directory if it does not exist.
        if (!logDirectory.exists()) {
            if (!logDirectory.mkdirs()) {
                throw new IOException("Could not create directory: " + logDirectory);
            }
        }

        byte lastIndex = (byte) (segments.length - 1);
        int lastSegmentId = 0;
        Mark mark = null;
        for (byte i = 0; i < segments.length; i++) {
            // Initialize the segments..
            segments[i] = new Segment(new File(logDirectory, "log-" + onlineLogNameFormat.format(i) + ".dat"), initialSegmentSize, i);
            // Find the last segment out of the online segments.
            if (segments[i].isActive()) {
                if (segments[i].getLastMark().sequenceId >= 0) {
                	markSegment = i;
                    mark = segments[i].getLastMark();
                }
                if (segments[i].getId() > lastSegmentId) {
                    lastSegmentId = segments[i].getId();
                    lastIndex = i;
                }
            }
        }

// Add all the segments to the activeSegments list.  Last segment will be
// the segment with the largest segment id.
        byte i = nextSegmentIndex(lastIndex);
        while (inactiveSegments.size() + activeSegments.size() < segments.length) {
            Segment segment = segments[i];
            if (segment.isActive()) {
                activeSegments.add(segment);
            }
            else {
                inactiveSegments.add(segment);
            }
            i = nextSegmentIndex(i);
        }
        if (mark != null) {
            setMark(mark);
        }

        if (activeSegments.size() == 0) {
            // At least one segment needs to be active.
            activateNextSegment();
        }
        else {
// Now make everything readonly except the last/append segment.
            Segment lastSegment = (Segment) activeSegments.getLast();
            lastSegment.setReadOnly(false);
            appendSegment = lastSegment.getIndex();
            for (Iterator iter = activeSegments.iterator(); iter.hasNext();) {
                Segment s = (Segment) iter.next();
                if (s != lastSegment) {
                    s.setReadOnly(true);
                }
            }
        }
        if (mark == null) {
            // No mark had been set?  Make the mark the first active record.
            Segment segment = (Segment) activeSegments.getFirst();
            mark = new Mark();
            mark.sequenceId = segment.getIndex();
            mark.offsetId = Segment.SEGMENT_HEADER_SIZE;
        }
        lastMark.copy(mark);
    }


    /**
     * @return
     */
    private int getNextSegmentId() {
        return ++lastSegmentId;
    }


    public void close() throws IOException {
        if (closed) {
            return;
        }
        this.closed = true;
        for (int i = 0; i < segments.length; i++) {
            segments[i].close();
        }
    }

    private void setMark(Mark mark) throws IOException {
        lastMark.copy(mark);
        
        // Find and deactivate active segments before the mark.
        for (Iterator i = activeSegments.iterator(); i.hasNext();) {
            Segment segment = (Segment) i.next();
            if (segment.getLastSequenceId() < lastMark.sequenceId) {
                segment.reinitialize();
                i.remove();
                inactiveSegments.add(segment);
            } else {
            	markSegment = segment.getIndex();
            	break;
            }
        }
    }

    /**
     * @param write
     * @throws IOException
     */
    public void appendAndForce(BatchedWrite write) throws IOException {
        Segment segment = segments[appendSegment];
        segment.seek(segment.getAppendOffset());
        segment.write(write);
        if (write.getMark() != null) {
            setMark(write.getMark());
        }
        segment.force();
    }


    public static class RecordInfo {
        private final RecordLocationImpl location;
        private final RecordHeader header;

        public RecordInfo(RecordLocationImpl location, RecordHeader header) {
            this.location = location;
            this.header = header;
        }
        
        int getNextLocation() {
        	return location.getSegmentOffset()+header.length+Record.RECORD_BASE_SIZE;
        }
        
    }

    private RecordInfo readRecordInfo(RecordLocationImpl location) throws IOException, InvalidRecordLocationException {
        if (0 > location.getSegmentIndex() || location.getSegmentIndex() > segments.length) {
            throw new InvalidRecordLocationException("Invalid segment id.");
        }

        Segment segment = segments[location.getSegmentIndex()];
        segment.seek(location.getSegmentOffset());
        
        // There can be no record at the append offset.
        if (segment.isAtAppendOffset()) {
            throw new InvalidRecordLocationException("No record at end of log.");
        }
        
        // Is there a record header at the seeked location?
        try {
            RecordHeader header = new RecordHeader();
            segment.readRecordHeader(header);
            return new RecordInfo(location, header);
        }
        catch (IOException e) {
            throw new InvalidRecordLocationException("No record at found.");
        }
    }

    /**
     * @param location
     * @return
     * @throws InvalidRecordLocationException
     * @throws IOException
     */
    public RecordLocationImpl readRecordLocation(RecordLocationImpl location) throws IOException, InvalidRecordLocationException {
        RecordInfo info = readRecordInfo(location);
        return info.location.setSequence(info.header.sequenceId);        
    }

    /**
     * @param lastLocation
     * @return
     */
    public RecordLocationImpl getNextDataRecordLocation(RecordLocationImpl lastLocation) throws IOException, InvalidRecordLocationException {
        RecordInfo ri = readRecordInfo(lastLocation);
        while (true) {
            byte segmentIndex = ri.location.getSegmentIndex();
            int offset = ri.getNextLocation();

            // Are we overflowing into next segment?
            if (offset >= segments[segmentIndex].getAppendOffset()) {
                segmentIndex = nextActiveSegmentIndex(segmentIndex);
                if (segmentIndex < 0) {
                    return null;
                }
                offset = Segment.SEGMENT_HEADER_SIZE;
            }

            try {
                ri = readRecordInfo(ri.location.setSegmentIndexAndOffset(segmentIndex, offset));
            }
            catch (InvalidRecordLocationException e) {
                return null;
            }

            // Is the next record the right record type?
            if (ri.header.recordType == DATA_RECORD_TYPE) {
                return ri.location.setSequence(ri.header.sequenceId);
            }
            // No? go onto the next record.
        }
    }

    /**
     * @param i
     * @return -1 if no next segment is available.
     */
    private byte nextActiveSegmentIndex(byte i) {
        byte rc = nextSegmentIndex(i);
        return segments[rc].isActive() ? rc : -1;
    }

    private byte nextSegmentIndex(byte i) {
        i++;
        if (i < segments.length) {
            return i;
        }
        return 0;
    }

    /**
     * @param segmentIndex
     * @param segmentOffset
     * @return
     * @throws IOException
     */
    public byte[] readData(int segmentIndex, int segmentOffset) throws IOException {
        if (0 > segmentIndex || segmentIndex > segments.length) {
            return null;
        }

        Segment segment = segments[segmentIndex];
        segment.seek(segmentOffset);

// There can be no record at the append offset.
        if (segment.isAtAppendOffset()) {
            return null;
        }

// Is there a record header at the seeked location?
        RecordHeader header = new RecordHeader();
        segment.readRecordHeader(header);
        byte data[] = new byte[header.length];
        segment.read(data);

        return data;
    }

    public int getInitialSegmentSize() {
        return initialSegmentSize;
    }

    public boolean isSegmentIndexActive(byte i) {
        synchronized (segments[i]) {
            return segments[i].isActive();
        }
    }

    public long getFirstSequenceIdOfSegementIndex(byte i) {
        synchronized (segments[i]) {
            return segments[i].getFirstSequenceId();
        }
    }

    synchronized public boolean canActivateNextSegment() {
        return inactiveSegments.size() > 0;
    }

    public byte getFirstActiveSegmentIndex() {
        return ((Segment) activeSegments.getFirst()).getIndex();
    }

    void activateNextSegment() throws IOException {
        // The current append segment becomes readonly
        if (appendSegment >= 0) {
            segments[appendSegment].setReadOnly(true);
        }
        Segment next = (Segment) inactiveSegments.removeFirst();
        activeSegments.addLast(next);
        next.activate(getNextSegmentId());
        appendSegment = next.getIndex();
    }

    /**
     * @return
     */
    public byte getAppendSegmentIndex() {
        return appendSegment;
    }

    /**
     * @return
     */
    public int getAppendSegmentOffset() {
        return segments[appendSegment].getAppendOffset();
    }

    int getTotalSegements() {
        return segments.length;
    }

    /**
     *
     */
    public long getLastSequenceId() {
        return segments[appendSegment].getLastSequenceId();
    }

    /**
     * @return
     */
    synchronized public RecordLocationImpl getFirstRecordLocationOfSecondActiveSegment(byte fm) {
        return ((Segment) activeSegments.get(1)).getFirstRecordLocation(fm);
    }

    /**
     * @return Returns the logDirectory.
     */
    public File getLogDirectory() {
        return logDirectory;
    }
	/**
	 * @return Returns the lastMark.
	 */
	public RecordLocationImpl getLastMarkedRecordLocation(byte fm) {
		if( markSegment==-1 )
			return null;
		return new RecordLocationImpl(fm, markSegment, lastMark.offsetId, lastMark.sequenceId);
	}
}
