/*-
 * See the file LICENSE for redistribution information.
 *
 * Copyright (c) 2002,2010 Oracle.  All rights reserved.
 *
 * $Id: UtilizationFileReader.java,v 1.19.2.2 2010/01/04 15:30:30 cwl Exp $
 */

package com.sleepycat.je.log;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import com.sleepycat.je.DatabaseException;
import com.sleepycat.je.cleaner.FileSummary;
import com.sleepycat.je.config.EnvironmentParams;
import com.sleepycat.je.dbi.EnvironmentImpl;
import com.sleepycat.je.log.entry.INLogEntry;
import com.sleepycat.je.log.entry.LNLogEntry;
import com.sleepycat.je.log.entry.LogEntry;
import com.sleepycat.je.log.entry.SingleItemEntry;
import com.sleepycat.je.tree.INDeleteInfo;
import com.sleepycat.je.tree.INDupDeleteInfo;
import com.sleepycat.je.tree.LN;
import com.sleepycat.je.tree.MapLN;
import com.sleepycat.je.txn.TxnCommit;
import com.sleepycat.je.txn.TxnEnd;
import com.sleepycat.je.utilint.DbLsn;

/**
 * Summarizes the utilized and unutilized portion of each log file by examining
 * each log entry.  Does not use the Cleaner UtilizationProfile information in
 * order to provide a second measure against which to evaluation the
 * UtilizationProfile accuracy.
 */
public class UtilizationFileReader extends FileReader {

    private static final boolean DEBUG = true;

    private Map<Long,FileSummary> summaries;     // Long file -> FileSummary
    private Map<Long,NodeInfo> activeNodes;   // Long node ID -> NodeInfo
    private Map<Long,List<Object>> txns;     // Long txn ID -> List of pairs, 
                                     // where each pair
                                     // is [ExtendedFileSummary, LNLogEntry]

    /* holds one [ExtendedFileSummary, LNLogEntry] */
    private List<Object> twoEntryList;

    private UtilizationFileReader(EnvironmentImpl env, int readBufferSize)
        throws IOException, DatabaseException {

        super(env,
              readBufferSize,
              true,            // read forward
              DbLsn.NULL_LSN,  // start LSN
              null,            // single file number
              DbLsn.NULL_LSN,  // end of file LSN
              DbLsn.NULL_LSN); // finish LSN

        summaries = new HashMap<Long,FileSummary>();
        activeNodes = new HashMap<Long,NodeInfo>();
        txns = new HashMap<Long,List<Object>>();

        twoEntryList = new ArrayList<Object>();
        twoEntryList.add(null);
        twoEntryList.add(null);
    }

    @Override
    protected boolean isTargetEntry() {
        /* UtilizationTracker does not count the file header. */
        return currentEntryHeader.getType() !=
               LogEntryType.LOG_FILE_HEADER.getTypeNum();
    }

    protected boolean processEntry(ByteBuffer entryBuffer)
        throws DatabaseException {

        LogEntryType lastEntryType =
            LogEntryType.findType(currentEntryHeader.getType());
        LogEntry entry = lastEntryType.getNewLogEntry();
        entry.readEntry(currentEntryHeader, entryBuffer, true); // readFullItem

        ExtendedFileSummary summary = 
        	(ExtendedFileSummary) summaries.get(readBufferFileNum);
        if (summary == null) {
            summary = new ExtendedFileSummary();
            summaries.put(readBufferFileNum, summary);
        }

        int size = getLastEntrySize();

        summary.totalCount += 1;
        summary.totalSize += size;

        if (entry instanceof LNLogEntry) {
            LNLogEntry lnEntry = (LNLogEntry) entry;
            if (DEBUG) {
                int otherSize = lnEntry.getLN().getLastLoggedSize();
                if (size != otherSize) {
                    System.out.println
                        ("LogReader.getLastEntrySize=" + size +
                         " LN.getLastLoggedSize=" + otherSize +
                         " " + lnEntry.getLogType());
                }
            }
            if (lastEntryType.isTransactional()) {
                Long txnId = Long.valueOf(lnEntry.getTransactionId());
                List<Object> txnEntries = txns.get(txnId);
                if (txnEntries == null) {
                    txnEntries = new ArrayList<Object>();
                    txns.put(txnId, txnEntries);
                }
                txnEntries.add(summary);
                txnEntries.add(lnEntry);
            } else {
                twoEntryList.set(0, summary);
                twoEntryList.set(1, lnEntry);
                applyTxn(twoEntryList, true);
            }
        } else if (entry instanceof INLogEntry) {
            INLogEntry inEntry = (INLogEntry) entry;
            Long nodeId = Long.valueOf(inEntry.getNodeId());
            summary.totalINCount += 1;
            summary.totalINSize += size;
            countObsoleteNode(nodeId);
            putActiveNode(nodeId, size, summary,
                          inEntry.getDbId().getId(),
                          false);
        } else if (entry instanceof SingleItemEntry) {
            Object item = ((SingleItemEntry) entry).getMainItem();
            long deletedNodeId = -1;
            if (item instanceof INDeleteInfo) {
                deletedNodeId = ((INDeleteInfo) item).getDeletedNodeId();
            } else if (item instanceof INDupDeleteInfo) {
                deletedNodeId = ((INDupDeleteInfo) item).getDeletedNodeId();
            }
            if (deletedNodeId != -1) {
                Long nodeId = Long.valueOf(deletedNodeId);
                countObsoleteNode(nodeId);
                activeNodes.remove(nodeId);
            }
            if (item instanceof TxnEnd) {
                Long txnId = Long.valueOf(((TxnEnd) item).getTransactionId());
                List<Object> txnEntries = txns.remove(txnId);
                if (txnEntries != null) {
                    applyTxn(txnEntries, item instanceof TxnCommit);
                }
            }
        }

        return true;
    }

    private void applyTxn(List<Object> entries, boolean commit) {
        for (int i = 0; i < entries.size(); i += 2) {
            ExtendedFileSummary summary = (ExtendedFileSummary) entries.get(i);
            LNLogEntry lnEntry = (LNLogEntry) entries.get(i + 1);
            LN ln = lnEntry.getLN();
            int size = ln.getLastLoggedSize();

            summary.totalLNCount += 1;
            summary.totalLNSize += size;

            if (!commit || ln.isDeleted()) {
                summary.obsoleteLNCount += 1;
                summary.recalcObsoleteLNSize += size;
            }

            if (commit) {
                Long nodeId = Long.valueOf(lnEntry.getNodeId());
                countObsoleteNode(nodeId);
                if (ln.isDeleted()) {
                    activeNodes.remove(nodeId);
                } else {
                    putActiveNode(nodeId, size, summary,
                                  lnEntry.getDbId().getId(),
                                  true);
                }
            }

            /* Process Database truncate or remove. */
            if (commit && ln.isDeleted() && ln instanceof MapLN) {
                int dbId = ((MapLN) ln).getDatabase().getId().getId();
                Iterator<Map.Entry<Long,NodeInfo>> iter = 
                    activeNodes.entrySet().iterator();
                while (iter.hasNext()) {
                    Map.Entry<Long,NodeInfo> iEntry = iter.next();
                    NodeInfo info = iEntry.getValue();
                    if (info.dbId == dbId) {
                        Long nodeId = iEntry.getKey();
                        countObsoleteNode(nodeId);
                        iter.remove();
                    }
                }
            }
        }
    }

    private void finishProcessing() {

        /* Apply uncomitted transactions. */
        Iterator<List<Object>> txnIter = txns.values().iterator();
        while (txnIter.hasNext()) {
            List<Object> txnEntries = txnIter.next();
            applyTxn(txnEntries, false);
        }
    }

    private void putActiveNode(Long nodeId,
                               int size,
                               ExtendedFileSummary summary,
                               int dbId,
                               boolean isLN) {
        NodeInfo info = activeNodes.get(nodeId);
        if (info == null) {
            info = new NodeInfo();
            activeNodes.put(nodeId, info);
        }
        info.size = size;
        info.summary = summary;
        info.dbId = dbId;
        info.isLN = isLN;
    }

    private void countObsoleteNode(Long nodeId) {
        NodeInfo info = activeNodes.get(nodeId);
        if (info != null) {
            ExtendedFileSummary summary = info.summary;
            if (info.isLN) {
                summary.obsoleteLNCount += 1;
                summary.recalcObsoleteLNSize += info.size;
            } else {
                summary.obsoleteINCount += 1;
                summary.recalcObsoleteINSize += info.size;
            }
        }
    }

    /**
     * Creates a UtilizationReader, reads the log, and returns the resulting
     * Map of Long file number to FileSummary.
     */
    public static Map<Long,FileSummary> calcFileSummaryMap(EnvironmentImpl env)
        throws IOException, DatabaseException {

        int readBufferSize = env.getConfigManager().getInt
            (EnvironmentParams.LOG_ITERATOR_READ_SIZE);

        UtilizationFileReader reader =
            new UtilizationFileReader(env, readBufferSize);
        while (reader.readNextEntry()) {
            /* All the work is done in processEntry. */
        }

        reader.finishProcessing();

        return reader.summaries;
    }

    private static class ExtendedFileSummary extends FileSummary {
        private int recalcObsoleteINSize;
        private int recalcObsoleteLNSize;

        /**
         * Overrides the LN size calculation to return the recalculated number
         * of obsolete LN bytes.
         */
        @Override
        public int getObsoleteLNSize() {
            return recalcObsoleteLNSize;
        }

        /**
         * Overrides the IN size calculation to return the recalculated number
         * of obsolete IN bytes.
         */
        @Override
        public int getObsoleteINSize() {
            return recalcObsoleteINSize;
        }

        /**
         * Overrides to add the extended data fields.
         */
        @Override
        public String toString() {
            StringBuffer buf = new StringBuffer();
            buf.append(super.toString());
            buf.append("<extended-info recalcObosleteINSize=\"");
            buf.append(recalcObsoleteINSize);
            buf.append("\" recalcObosletedLNSize=\"");
            buf.append(recalcObsoleteLNSize);
            buf.append("\"/>");
            return buf.toString();
        }
    }

    private static class NodeInfo {
        ExtendedFileSummary summary;
        int size;
        int dbId;
        boolean isLN;
    }
}
