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

package com.sleepycat.je.recovery;

import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;

import com.sleepycat.je.CacheMode;
import com.sleepycat.je.DatabaseException;
import com.sleepycat.je.dbi.DatabaseId;
import com.sleepycat.je.dbi.DatabaseImpl;
import com.sleepycat.je.dbi.DbTree;
import com.sleepycat.je.dbi.EnvironmentImpl;
import com.sleepycat.je.dbi.INList;
import com.sleepycat.je.dbi.MemoryBudget;
import com.sleepycat.je.recovery.Checkpointer.CheckpointReference;
import com.sleepycat.je.tree.BIN;
import com.sleepycat.je.tree.IN;
import com.sleepycat.je.tree.MapLN;

/**
 * Map of Integer->Set
 * level->Set of checkpoint references
 */
class DirtyINMap {

    private EnvironmentImpl envImpl;
    private SortedMap<Integer,Map<Long,CheckpointReference>> levelMap;
    private int numEntries;
    private Set<DatabaseId> mapLNsToFlush;

    DirtyINMap(EnvironmentImpl envImpl) {
        this.envImpl = envImpl;
        levelMap = new TreeMap<Integer,Map<Long,CheckpointReference>>();
        numEntries = 0;
        mapLNsToFlush = new HashSet<DatabaseId>();
    }

    /**
     * Scan the INList for all dirty INs, excluding temp DB INs.  Save them in
     * a tree-level ordered map for level ordered flushing.
     *
     * Take this opportunity to reset the memory budget tree value.
     *
     * @return highestFlushLevels, map of DatabaseImpl to Integer.
     */
    Map<DatabaseImpl,Integer> 
        selectDirtyINsForCheckpoint(boolean flushAll, boolean flushExtraLevel)
        throws DatabaseException {

        Map<DatabaseImpl,Integer> highestLevelSeenMap =
            new IdentityHashMap<DatabaseImpl,Integer>();
        DbTree dbTree = envImpl.getDbTree();

        INList inMemINs = envImpl.getInMemoryINs();

        /*
         * Opportunistically recalculate the INList memory budget while
         * traversing the entire INList.
         */
        inMemINs.memRecalcBegin();
        boolean completed = false;

        try {
            for (IN in : inMemINs) {
                in.latch(CacheMode.UNCHANGED);
                DatabaseImpl db = in.getDatabase();

                try {
                    inMemINs.memRecalcIterate(in);

                    /* Do not checkpoint temporary databases. */
                    if (db.isTemporary()) {
                        continue;
                    }

                    Integer level =
                        addDirtyIN(in, false /*updateMemoryBudget*/);
                    if (level != null) {

                        /*
                         * IN was added to the dirty map.  Update the highest
                         * level seen for the database.  Use one level higher
                         * when flushExtraLevel is set.  When flushAll is set,
                         * use the maximum level for the database.  Durable
                         * deferred-write databases must be synced, so also use
                         * the maximum level.
                         */
                        if (flushAll || db.isDurableDeferredWrite()) {
                            if (!highestLevelSeenMap.containsKey(db)) {

                                /*
                                 * Null is used as an indicator that
                                 * getHighestLevel should be called below, when
                                 * no latches are held.
                                 */
                                highestLevelSeenMap.put(db, null);
                            }
                        } else {
                            int levelVal = level.intValue();
                            if (flushExtraLevel) {
                                if (in.isRoot()) {
                                    /* No reason to go above DB root. */
                                    if (!in.isDbRoot()) {
                                        /* The level above DIN root is BIN. */
                                        levelVal = IN.BIN_LEVEL;
                                    }
                                } else {
                                    /* Next level up in the same tree. */
                                    levelVal += 1;
                                }
                            }
                            Integer highestLevelSeen =
                                highestLevelSeenMap.get(db);
                            if (highestLevelSeen == null ||
                                levelVal > highestLevelSeen.intValue()) {
                                if (flushExtraLevel) {
                                    level = Integer.valueOf(levelVal);
                                }
                                highestLevelSeenMap.put(db, level);
                            }
                        }
                    }

                    /* Save dirty/temp DBs for later. */
                    saveMapLNsToFlush(in);
                } finally {
                    in.releaseLatch();
                }
            }
            completed = true;
        } finally {
            inMemINs.memRecalcEnd(completed);
        }

        /* Call getHighestLevel only when no latches are held. */
        for (Map.Entry<DatabaseImpl,Integer> entry :
             highestLevelSeenMap.entrySet()) {
            if (entry.getValue() == null) {
                DatabaseImpl db = entry.getKey();
                entry.setValue(Integer.valueOf(dbTree.getHighestLevel(db)));
            }
        }

        return highestLevelSeenMap;
    }

    /**
     * Scan the INList for all dirty INs for a given database.  Arrange them in
     * level sorted map for level ordered flushing.
     *
     * @return highestFlushLevels, map of DatabaseImpl to Integer.
     */
    Map<DatabaseImpl, Integer> selectDirtyINsForDbSync(DatabaseImpl dbImpl)
        throws DatabaseException {

        DatabaseId dbId = dbImpl.getId();

        for (IN in : envImpl.getInMemoryINs()) {
            if (in.getDatabaseId().equals(dbId)) {
                in.latch(CacheMode.UNCHANGED);
                try {
                    addDirtyIN(in, false /*updateMemoryBudget*/);
                } finally {
                    in.releaseLatch();
                }
            }
        }

        /*
         * Create a single entry map that forces all levels of this DB to
         * be flushed.
         */
        Map<DatabaseImpl, Integer> highestFlushLevels = 
            new IdentityHashMap<DatabaseImpl, Integer>();
        highestFlushLevels.put
            (dbImpl,
             Integer.valueOf(envImpl.getDbTree().getHighestLevel(dbImpl)));
        return highestFlushLevels;
    }

    int getNumLevels() {
        return levelMap.size();
    }

    void addCostToMemoryBudget() {
        MemoryBudget mb = envImpl.getMemoryBudget();
        int cost = numEntries * MemoryBudget.CHECKPOINT_REFERENCE_SIZE;
        mb.updateAdminMemoryUsage(cost);
    }

    void removeCostFromMemoryBudget() {
        MemoryBudget mb = envImpl.getMemoryBudget();
        int cost = numEntries * MemoryBudget.CHECKPOINT_REFERENCE_SIZE;
        mb.updateAdminMemoryUsage(0 - cost);
    }

    /**
     * Adds the IN if dirty, otherwise returns null.  See addIN.
     */
    private Integer addDirtyIN(IN in, boolean updateMemoryBudget) {

        if (in.getDirty()) {
            return addIN(in, updateMemoryBudget);
        } else {
            return null;
        }
    }


    /**
     * Add a node unconditionally to the dirty map. The dirty map is keyed by
     * level (Integers) and holds sets of IN references.
     *
     * @param updateMemoryBudget if true then update the memory budget as the
     * map is changed; if false then addCostToMemoryBudget must be called
     * later.
     *
     * @return level of IN added to the dirty map.  The level is returned
     * rather than a boolean simply to avoid allocating another Integer in the
     * caller.
     */
    Integer addIN(IN in, boolean updateMemoryBudget) {

        Integer level = Integer.valueOf(in.getLevel());
        Map<Long,CheckpointReference> nodeMap;
        if (levelMap.containsKey(level)) {
            nodeMap = levelMap.get(level);
        } else {
            nodeMap = new HashMap<Long,CheckpointReference>();
            levelMap.put(level, nodeMap);
        }

        nodeMap.put(in.getNodeId(),
                    new CheckpointReference(in.getDatabase().getId(),
                                            in.getNodeId(),
                                            in.containsDuplicates(),
                                            in.isDbRoot(),
                                            in.getMainTreeKey(),
                                            in.getDupTreeKey()));
        numEntries++;

        if (updateMemoryBudget) {
            MemoryBudget mb = envImpl.getMemoryBudget();
            mb.updateAdminMemoryUsage
                (MemoryBudget.CHECKPOINT_REFERENCE_SIZE);
        }

        return level;
    }

    /**
     * Get the lowest level currently stored in the map.
     */
    Integer getLowestLevelSet() {
        return levelMap.firstKey();
    }

    /**
     * Get an iterator over the references corresponding to the given level.
     */
    Iterator<CheckpointReference> getIterator(Integer level) {
        return levelMap.get(level).values().iterator();
    }

    /**
     * Removes the set corresponding to the given level.
     */
    void removeLevel(Integer level) {
        levelMap.remove(level);
    }

    boolean containsNode(Integer level, Long nodeId) {
        Map<Long,CheckpointReference> nodeMap = levelMap.get(level);
        if (nodeMap != null) {
            return nodeMap.containsKey(nodeId);
        }
        return false;
    }

    CheckpointReference removeNode(Integer level, Long nodeId) {
        Map<Long,CheckpointReference> nodeMap = levelMap.get(level);
        if (nodeMap != null) {
            return nodeMap.remove(nodeId);
        }
        return null;
    }

    CheckpointReference removeNextNode(Integer level) {
        Map<Long,CheckpointReference> nodeMap = levelMap.get(level);
        if (nodeMap != null) {
            Iterator<Map.Entry<Long,CheckpointReference>> iter =
                nodeMap.entrySet().iterator();
            if (iter.hasNext()) {
                CheckpointReference ref = iter.next().getValue();
                iter.remove();
                return ref;
            }
        }
        return null;
    }

    /**
     * If the given IN is a BIN for the ID mapping database, saves all
     * dirty/temp MapLNs contained in it.
     */
    private void saveMapLNsToFlush(IN in) {
        if (in instanceof BIN &&
            in.getDatabase().getId().equals(DbTree.ID_DB_ID)) {
            for (int i = 0; i < in.getNEntries(); i += 1) {
                MapLN ln = (MapLN) in.getTarget(i);
                if (ln != null && ln.getDatabase().isCheckpointNeeded()) {
                    mapLNsToFlush.add(ln.getDatabase().getId());
                }
            }
        }
    }

    /**
     * Flushes all saved dirty/temp MapLNs and clears the saved set.
     *
     * <p>If dirty, a MapLN must be flushed at each checkpoint to record
     * updated utilization info in the checkpoint interval.  If it is a
     * temporary DB, the MapLN must be flushed because all temp DBs must be
     * encountered by recovery so they can be removed if they were not closed
     * (and removed) by the user.</p>
     *
     * @param checkpointStart start LSN of the checkpoint in progress.  To
     * reduce unnecessary logging, the MapLN is only flushed if it has not been
     * written since that LSN.
     */
    void flushMapLNs(long checkpointStart)
        throws DatabaseException {

        if (!mapLNsToFlush.isEmpty()) {
            DbTree dbTree = envImpl.getDbTree();
            Iterator<DatabaseId> i = mapLNsToFlush.iterator();
            while (i.hasNext()) {
                DatabaseId dbId = i.next();
                DatabaseImpl db = null;
                try {
                    db = dbTree.getDb(dbId);
                    if (db != null &&
                        !db.isDeleted() &&
                        db.isCheckpointNeeded()) {
                        dbTree.modifyDbRoot
                            (db, checkpointStart /*ifBeforeLsn*/,
                             true /*mustExist*/);
                    }
                } finally {
                    dbTree.releaseDb(db);
                }
            }
            mapLNsToFlush.clear();
        }
    }

    /**
     * Flushes the DB mapping tree root at the end of the checkpoint, if either
     * mapping DB is dirty and the root was not flushed previously during the
     * checkpoint.
     *
     * @param checkpointStart start LSN of the checkpoint in progress.  To
     * reduce unnecessary logging, the Root is only flushed if it has not been
     * written since that LSN.
     */
    void flushRoot(long checkpointStart)
        throws DatabaseException {

        DbTree dbTree = envImpl.getDbTree();
        if (dbTree.getDb(DbTree.ID_DB_ID).isCheckpointNeeded() ||
            dbTree.getDb(DbTree.NAME_DB_ID).isCheckpointNeeded()) {
            envImpl.logMapTreeRoot(checkpointStart);
        }
    }
}
