package com.lts.kv.txlog;

import com.lts.core.commons.file.FileUtils;
import com.lts.core.logger.Logger;
import com.lts.kv.*;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Timer;

/**
 * 记录格式
 * 1.MAGIC_NUMBER   1 byte
 * 2.timestamp      8 byte
 * 3.logEntrySize   4 byte
 * 4.logEntry
 * <p/>
 * 非线程安全
 *
 * @author Robert HG (254963746@qq.com) on 12/13/15.
 */
public class StoreTxLog implements Closeable {

    private static final Logger LOGGER = DB.LOGGER;
    private StoreTxLog next;

    private FileChannel fileChannel;
    private StoreConfig storeConfig;

    private final ByteBuffer entryBuffer;
    private StoreTxLogFileHeader fileHeader;

    private static final int ENTRY_HEAD_LENGTH = 1 + 4;
    private static final byte magic = (byte) 0xA2;

    public static final String LOG_FILE_SUFFIX = ".log";

    private long lastCheckPointLength = 0;

    private Timer syncTimer;
    private FutureTimerTask syncTimerTask;
    private FutureTimerTask.Callable syncCallable;

    private long fileLength;

    public StoreTxLog(StoreConfig storeConfig, File file, boolean readonly,
                      boolean isNewFile, long firstRecordId) throws IOException {
        this.storeConfig = storeConfig;
        this.entryBuffer = ByteBuffer.allocate(storeConfig.getMaxxLogEntryLength() + 1 + 4);
        this.fileHeader = new StoreTxLogFileHeader();

        if (!readonly) {
            syncTimer = new Timer("ltsdb-dblog-sync-timer", true);

            syncCallable = new FutureTimerTask.Callable() {
                @Override
                public void call() throws Exception {
                    checkPoint();
                }
            };
        }

        if (isNewFile && file.exists()) {
            throw new IOException(file + " exists already");
        } else {
            FileUtils.createFileIfNotExist(file);
        }

        fileChannel = FileUtils.newFileChannel(file, "rw");

        if (isNewFile) {
            // 新文件长度为头部长度
            fileLength = fileHeader.getLength();
            fileHeader.setFirstRecordId(firstRecordId);

            fileHeader.write(fileChannel);
        } else {
            fileHeader.read(fileChannel);
            fileLength = fileChannel.size();
            lastCheckPointLength = fileLength;
        }
    }

    /**
     * 新添加一条记录
     *
     * @return 返回记录ID
     */
    public StoreTxLogPosition append(byte[] entry) throws IOException {

        int length = entry.length;

        if (length > storeConfig.getMaxxLogEntryLength()) {
            throw new DBException("Value size can not great than " + storeConfig.getMaxxLogEntryLength());
        }

        // 检查当前文件容量是否足够
        if (fileLength + length + ENTRY_HEAD_LENGTH > storeConfig.getTxLogFileSize()) {
            throw new CapacityNotEnoughException();
        }

        StoreTxLogPosition result = new StoreTxLogPosition();
        result.setRecordId(getNextRecordId());

        boolean ok = false;
        try {

            entryBuffer.clear();
            entryBuffer.put(magic);                      // 1 byte
            entryBuffer.putInt(length);                         // 4 byte
            entryBuffer.put(entry);                             // entry.length
            entryBuffer.flip();

            fileChannel.position(fileLength);
            fileChannel.write(entryBuffer);

            int entryLength = (ENTRY_HEAD_LENGTH + length);
            fileLength += entryLength;

            ok = true;
        } finally {
            if (ok) {
                if (syncTimerTask == null || syncTimerTask.isDone()) {
                    syncTimerTask = new FutureTimerTask("ltsdb-dblog-sync-timertask", syncCallable);
                    syncTimer.schedule(syncTimerTask, storeConfig.getDbLogFlushInterval());
                }
            }
        }
        return result;
    }

    public byte[] readEntry(long position) throws IOException {
        fileChannel.position(position);
        entryBuffer.clear();
        fileChannel.read(entryBuffer);

        entryBuffer.position(0);
        byte readMagic = entryBuffer.get();
        if (readMagic != magic) {
            throw new IOException("Invalid entry type magic number 0x" + Integer.toHexString(readMagic & 0xFFFF));
        }
        int length = entryBuffer.getInt();
        byte[] entry = new byte[length];
        entryBuffer.get(entry);
        return entry;
    }

    public long nextEntryPosition(long position, long entryLength) {
        return position + entryLength + 1 + 4;
    }

    public long getFileLength() {
        return fileLength;
    }

    public long getNextRecordId() {
        return fileHeader.getFirstRecordId() + fileLength;
    }

    public long getFirstRecordId() {
        return fileHeader.getFirstRecordId();
    }

    private void checkPoint() throws IOException {

        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("checkpoint start");
        }
        // 先将内容都强制刷到磁盘,因为后面会写头部汇总信息
        if (fileLength != lastCheckPointLength) {
            fileChannel.force(true);
            lastCheckPointLength = fileLength;
        }
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("checkpoint end: fileLength=" + fileLength);
        }
    }

    public void setNext(StoreTxLog next) {
        this.next = next;
    }

    public StoreTxLog next() {
        return this.next;
    }

    public int getHeaderLength() {
        return fileHeader.getLength();
    }

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

}
