/*******************************************************************************
 * Copyright (c) 2014 Ericsson
 *
 * All rights reserved. This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0 which
 * accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *   Alexandre Montplaisir - Renamed/extracted from CtfTraceManager
 *******************************************************************************/

package org.eclipse.tracecompass.internal.tmf.ctf.core.trace.iterator;

import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.tracecompass.internal.tmf.ctf.core.Activator;
import org.eclipse.tracecompass.tmf.ctf.core.context.CtfLocationInfo;
import org.eclipse.tracecompass.tmf.ctf.core.context.CtfTmfContext;
import org.eclipse.tracecompass.tmf.ctf.core.trace.CtfTmfTrace;

/**
 * A CTF trace iterator manager.
 *
 * Each instance of {@link CtfTmfTrace} should possess one of these, which will
 * manage the iterators that are opened to read that trace. This will allow
 * controlling the number of opened file handles per trace.
 *
 * @author Matthew Khouzam
 */
public class CtfIteratorManager {
    /*
     * Cache size. Under 1023 on linux32 systems. Number of file handles
     * created.
     */
    private static final int MAX_SIZE = 100;

    /** The map of the cache */
    private final Map<CtfTmfContext, CtfIterator> fMap;

    /** An array pointing to the same cache. this allows fast "random" accesses */
    private final List<CtfTmfContext> fRandomAccess;

    /** Lock for when we access the two previous data structures */
    private final Lock fAccessLock = new ReentrantLock();

    /** The parent trace */
    private final CtfTmfTrace fTrace;

    /** Random number generator */
    private final Random fRnd;

    /**
     * Constructor
     *
     * @param trace
     *            The trace whose iterators this manager will manage
     */
    public CtfIteratorManager(CtfTmfTrace trace) {
        fMap = new HashMap<>();
        fRandomAccess = new ArrayList<>();
        fRnd = new Random(System.nanoTime());
        fTrace = trace;
    }

    /**
     * This needs explaining: the iterator table is effectively a cache. Originally
     * the contexts had a 1 to 1 structure with the file handles of a trace. This
     * failed since there is a limit to how many file handles we can have opened
     * simultaneously. Then a round-robin scheme was implemented, this lead up to a
     * two competing contexts syncing up and using the same file handler, causing
     * horrible slowdowns. Now a random replacement algorithm is selected. This is
     * the same as used by arm processors, and it works quite well when many cores
     * so this looks promising for very multi-threaded systems.
     *
     * @param context
     *            the context to look up
     * @return the iterator referring to the context or null in the case of an error
     */
    public @Nullable CtfIterator getIterator(final CtfTmfContext context) {
        /*
         * if the element is in the map, we don't need to do anything else.
         */
        CtfIterator iter = fMap.get(context);
        if (iter == null) {

            fAccessLock.lock();
            try {
                /*
                 * Assign an iterator to a context.
                 */
                if (fRandomAccess.size() < MAX_SIZE) {
                    /*
                     * if we're not full yet, just add an element.
                     */
                    iter = (CtfIterator) fTrace.createIterator();
                    if (iter == null) {
                        return null;
                    }
                    addElement(context, iter);

                } else {
                    /*
                     * if we're full, randomly replace an element
                     */
                    iter = replaceRandomElement(context);
                }
                if (context.getLocation() != null) {
                    final CtfLocationInfo location = (CtfLocationInfo) context.getLocation().getLocationInfo();
                    iter.seek(location);
                }
            } finally {
                fAccessLock.unlock();
            }
        }
        return iter;
    }

    /**
     * Remove an iterator from this manager
     *
     * @param context
     *            The context of the iterator to remove
     */
    public void removeIterator(CtfTmfContext context) {
        fAccessLock.lock();
        try {
            /* The try below is only to auto-call CtfIterator.close() */
            try (CtfIterator removed = fMap.remove(context)) {
                // try with resource
            }
            fRandomAccess.remove(context);

        } finally {
            fAccessLock.unlock();
        }
    }

    /**
     * Add a pair of context and element to the hashmap and the arraylist.
     *
     * @param context
     *            the context
     * @param elem
     *            the iterator
     */
    private void addElement(final CtfTmfContext context,
            final CtfIterator elem) {
        fAccessLock.lock();
        try {
            fMap.put(context, elem);
            fRandomAccess.add(context);

        } finally {
            fAccessLock.unlock();
        }
    }

    /**
     * Replace a random element
     *
     * @param context
     *            the context to swap in
     * @return the iterator of the removed elements.
     */
    private CtfIterator replaceRandomElement(final CtfTmfContext context) {
        /*
         * This needs some explanation too: We need to select a random victim
         * and remove it. The order of the elements is not important, so instead
         * of just calling arraylist.remove(element) which has an O(n)
         * complexity, we pick an random number. The element is swapped out of
         * the array and removed and replaced in the hashmap.
         */
        fAccessLock.lock(); // just in case, should only be called when already locked
        try {
            final int size = fRandomAccess.size();
            final int pos = fRnd.nextInt(size);
            final CtfTmfContext victim = fRandomAccess.get(pos);
            fRandomAccess.set(pos, context);
            CtfIterator elem = checkNotNull(fMap.remove(victim));
            if (elem.isClosed()) {
                /*
                 * In case the iterator streams have been closed, we need to
                 * replace it by a fresh new one to access the trace. We also
                 * report that as an error as it should not happen.
                 */
                Activator.getDefault().logError("Found closed iterator in iterator manager for trace " + victim.getTrace()); //$NON-NLS-1$

                elem.dispose();
                elem = (CtfIterator) fTrace.createIterator();
            }
            fMap.put(context, elem);
            victim.dispose();
            return elem;

        } finally {
            fAccessLock.unlock();
        }
    }

    /**
     * Dispose this iterator manager, which will close all the remaining
     * iterators.
     */
    public void dispose() {
        fAccessLock.lock();
        try {
            for (CtfIterator iterator : fMap.values()) {
                iterator.dispose();
            }
            fMap.clear();
            fRandomAccess.clear();

        } finally {
            fAccessLock.unlock();
        }
    }
}