/**
 *
 * 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 java.io.DataInput;
import java.io.DataOutput;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

/**
 * Allows read/append access to a segment of a LogFile. 
 *  
 * @version $Revision: 1.1 $
 */
public class Segment {
    static final public int SEGMENT_HEADER_SIZE=0x0100;
    
    /** The File that holds the log data. */
    private File file;     				    
    /** Prefered size.  The size that the segment file is set to when initilaized. */
    private final int initialSize;
    /** The index of this Segment */
    private final byte index;
    /** The id of the log file. */
    private int id;   				    
    /** The sequenceId of the first record */
    private long firstSequenceId;		
    /** The sequenceId of the last record */
    private long lastSequenceId;  		
    /** Does it have live records in it? */
    private boolean active=false;       
    /** The location of the mark */
    private final Mark lastMark = new Mark();
    /** The next free offset in the file. */
    private int appendOffset=0;   		
    /** Used to access the file */
    private RandomAccessFile randomAccessFile;
    /** Used to know where we seeked to last */
    //private int seekOffset;
    /** Is the segment in readonly mode */
    private boolean readOnly;
	private FileChannel fileChannel;


    public Segment(File file, int initialSize, byte index) throws IOException {
        this.file = file;
        this.initialSize = initialSize;
        this.index = index;
        boolean initializationNeeeded = !file.exists();
        randomAccessFile = new RandomAccessFile(file, "rw");
        randomAccessFile.seek(0);
        fileChannel = randomAccessFile.getChannel();
        if( initializationNeeeded ) {
            reinitialize();
        } 
        loadState();
    }
    
    public void seek( int newPos ) throws IOException {
    	randomAccessFile.seek(newPos);
    }

    private void loadState() throws IOException {
        seek(0);
        try {
            // It's quicker to load that state from the header..
            // but it might not be consistent.  Detect for failure.
            read(randomAccessFile);
            int firstCode = hashCode();
            read(randomAccessFile);
                        
            if( firstCode==hashCode() && ( readOnly || !active ) ) {
                // Since the header is consistent,
                // and this is inactive or read only segement,
                // there is no need to restore state from the log.
                return;
            }                
        } catch (IOException e) {
        }
        
        // We need to build the segment state by reading the log.
        firstSequenceId=-1;
        lastSequenceId=-1;
        readOnly=false;
        active=true; 
        
        int offset=SEGMENT_HEADER_SIZE;
        long fileSize = randomAccessFile.length();
        RecordHeader header = new RecordHeader();
        RecordFooter footer = new RecordFooter();
        while( (offset+Record.RECORD_BASE_SIZE) < fileSize) {
            
        	// Read the next header
        	seek(offset);
            try { 
                header.readRecordHeader(randomAccessFile);
            } catch (IOException e) {
                break;
            }
            
            // Validate the header..
            if( !header.isValid() )
                break;
            if( firstSequenceId==-1 ) {
                firstSequenceId = header.sequenceId;
            } else {
                // Make sure the sequence is incrementing.
                if( lastSequenceId >= header.sequenceId ) {
                    break;
                }
            }            
            // Validate the whole record is in the segment.
            if( offset+header.length+Record.RECORD_BASE_SIZE > fileSize) {
            	break;
            }
            
            // Should we load the record's data?
            byte data[]=null;
            if( RecordFooter.isChecksumingEnabled() || header.recordType==LogFile.MARK_RECORD_TYPE ) {
            	data = new byte[header.length];
            	randomAccessFile.readFully(data);
            }
            
            // Load the footer.
            seek(offset+header.length+RecordHeader.RECORD_HEADER_SIZE);
            try { 
                footer.readRecordFooter(randomAccessFile);
            } catch (IOException e) {
                break;
            }
            
            // Validate the footer..
            if( !footer.matches(header) )
                break;            
            if( RecordFooter.isChecksumingEnabled() ) {
                if( !footer.matches(data) )
                    break;          	
            }

            // read the mark data in if it was a mark record.
            if(header.recordType==LogFile.MARK_RECORD_TYPE) {
                lastMark.readExternal(data);
            }
                
            lastSequenceId = header.sequenceId;	  
            offset += header.length+Record.RECORD_BASE_SIZE;
            appendOffset = offset;
        }
        
        // Now that we have the log state, store it so that next time we come up 
        // we don't have to read the log to get a consistent state.        
        storeState();            
    }

    private void resize() throws IOException {
        randomAccessFile.setLength(initialSize);
    }
    
    private void storeState() throws IOException {
        int restorePos=getSeekPos();
        // We store the data 2 times just in case we die in the
        // middle of one of the writes.  The hashCode will
        // help us figure out if the written data is valid.
        seek(0);               
        writeHeader(randomAccessFile);
        writeHeader(randomAccessFile);
        force();
        seek(restorePos);
    }
    
    private int getSeekPos() throws IOException {
		return (int) randomAccessFile.getFilePointer();
	}

	public void force() throws IOException {
        fileChannel.force(false);
    }
    
    public void reinitialize() throws IOException {
        this.readOnly=true;
        this.active=false;
        this.firstSequenceId=-1;
        this.appendOffset=-1;
        this.id=-1;
        this.lastSequenceId=-1;
        this.lastMark.offsetId=-1;
        this.lastMark.sequenceId=-1;
        this.appendOffset=SEGMENT_HEADER_SIZE;
        resize();
        storeState();
    }

    public void setReadOnly(boolean enable) throws IOException {
        if( !active )
            throw new IllegalStateException("Not Active.");
        
        this.readOnly=enable;
        storeState();
    }

    public void activate(int id) throws IOException {
        if( active )
            throw new IllegalStateException("Allready Active.");
        
        this.id=id;
        this.readOnly=false;
        this.active=true;
        this.appendOffset=SEGMENT_HEADER_SIZE;
        lastSequenceId=firstSequenceId=-1;
        storeState();
        seek(appendOffset);
    }

    public int hashCode() {
        int rc = id;
        rc ^= appendOffset;
		rc ^= (int) (0xFFFFFFFF & firstSequenceId);
		rc ^= (int) (0xFFFFFFFF & (firstSequenceId>>4) );
		rc ^= (int) (0xFFFFFFFF & lastSequenceId);
		rc ^= (int) (0xFFFFFFFF & (lastSequenceId>>4) );
		if( active )
		    rc ^= 0xABD01212;
		if( readOnly )
		    rc ^= 0x1F8ADC21;
		return rc;
    }

    private void writeHeader(DataOutput f) throws IOException {
        f.writeInt(id);
        f.writeLong(firstSequenceId);
        f.writeLong(lastSequenceId);
        f.writeBoolean(active);
        f.writeBoolean(readOnly);
        f.writeLong(lastMark.sequenceId);
        f.writeInt(lastMark.offsetId);
        f.writeInt(appendOffset);
        f.writeInt(hashCode());
    }
    
    private void read(DataInput f) throws IOException {
        id = f.readInt();
        firstSequenceId = f.readLong();
        lastSequenceId = f.readLong();
        active = f.readBoolean();
        readOnly = f.readBoolean();
        lastMark.sequenceId = f.readLong();
        lastMark.offsetId = f.readInt();
        appendOffset = f.readInt();
        
        // Validate the state was consistent with the hashcode 
        if( f.readInt()!=hashCode())
            throw new IOException();            
    }

    public int getId() {
        return id;
    }
    public boolean isActive() {
        return active;
    }
    public long getFirstSequenceId() {
        return firstSequenceId;
    }
    public Mark getLastMark() {
        return lastMark;
    }
    public long getLastSequenceId() {
        return lastSequenceId;
    }
    public int getAppendOffset() {
        return appendOffset;
    }
    
    public boolean isAtAppendOffset() throws IOException {
        return appendOffset==getSeekPos();
    }
    
	/**
	 * @param write
	 * @throws IOException
	 */
	public void write(BatchedWrite write) throws IOException {
		
		ByteBuffer byteBuffer = write.getByteBuffer();
		byteBuffer.flip();
		
		write(byteBuffer);
		
		if( firstSequenceId==-1 )
            firstSequenceId=write.getFirstSequenceId();
        lastSequenceId=write.getLastSequenceId();
        
	}
	
    public void write(ByteBuffer byteBuffer) throws IOException {
        if( !active )
            throw new IllegalStateException("Segment is not active.  Writes are not allowed");
        if( readOnly )
            throw new IllegalStateException("Segment has been marked Read Only.  Writes are not allowed");
        if( !isAtAppendOffset() )
            throw new IllegalStateException("Segment not at append location.");
        
        int writeLength = byteBuffer.remaining();
        while( byteBuffer.remaining() > 0 ) {
        	fileChannel.write(byteBuffer);
        }
        
        // Keep track of where we are at.
        appendOffset+=writeLength;        
	}

    public void readRecordHeader(RecordHeader seekedRecordHeader) throws IOException {
        seekedRecordHeader.readRecordHeader(randomAccessFile);
    }
    
    public void close() throws IOException {
        this.randomAccessFile.close();
    }

    public void read(byte[] answer) throws IOException {
        randomAccessFile.readFully(answer);        
    }

    /**
     * @return
     */
    public boolean isReadOnly() {
        return readOnly;
    }

    public byte getIndex() {
        return index;
    }
    
	public String toString() {
		return "Segment:"+index+":"+id;
	}

	/**
	 * @return
	 */
	public RecordLocationImpl getFirstRecordLocation(byte fm) {
		return new RecordLocationImpl(fm, index, SEGMENT_HEADER_SIZE, firstSequenceId);
	}

}