/*
 * Copyright 2006-2021 DLR, Germany
 * 
 * SPDX-License-Identifier: EPL-1.0
 * 
 * https://rcenvironment.de/
 */

package de.rcenvironment.core.gui.log.internal;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.osgi.service.log.LogEntry;
import org.osgi.service.log.LogService;

import de.rcenvironment.core.communication.api.PlatformService;
import de.rcenvironment.core.communication.common.InstanceNodeSessionId;
import de.rcenvironment.core.communication.management.WorkflowHostService;
import de.rcenvironment.core.gui.log.LogListener;
import de.rcenvironment.core.log.DistributedLogReaderService;
import de.rcenvironment.core.log.SerializableLogEntry;
import de.rcenvironment.core.utils.incubator.ServiceRegistry;
import de.rcenvironment.core.utils.incubator.ServiceRegistryAccess;

/**
 * Provides central local access to the whole logging data (log entries, remote platforms) to display.
 * 
 * @author Doreen Seider
 * @author Enrico Tappert
 * @author Robert Mischke
 */
public final class LogModel {

    private static final int LOG_POOL_SIZE = 7000;

    private static LogModel instance;

    private final List<Listener> listeners = new LinkedList<Listener>();

    private Set<InstanceNodeSessionId> currentWorkflowHostsAndSelf;

    private InstanceNodeSessionId selectedLogSource;

    private Map<InstanceNodeSessionId, Map<Integer, SortedSet<SerializableLogEntry>>> allLogEntries;

    private final WorkflowHostService workflowHostService;

    private final PlatformService platformService;

    private final DistributedLogReaderService logReaderService;

    private LogModel() {
        ServiceRegistryAccess registryAccess = ServiceRegistry.createAccessFor(this);
        workflowHostService = registryAccess.getService(WorkflowHostService.class);
        platformService = registryAccess.getService(PlatformService.class);
        logReaderService = registryAccess.getService(DistributedLogReaderService.class);

        allLogEntries = new ConcurrentHashMap<InstanceNodeSessionId, Map<Integer, SortedSet<SerializableLogEntry>>>();
        currentWorkflowHostsAndSelf = workflowHostService.getWorkflowHostNodesAndSelf();
    }

    /**
     * Singleton to enable central access to the data.
     * 
     * @return Singleton instance.
     */
    public static synchronized LogModel getInstance() {
        if (null == instance) {
            instance = new LogModel();
        }
        return instance;
    }

    /**
     * Returns a list of {@link LogEntry} for the specified {@link InstanceNodeSessionId} set by
     * {@link LogModel#setSelectedLogSource(String)}.
     * 
     * @return {@link SortedSet} of {@link LogEntry}.
     */
    public SortedSet<SerializableLogEntry> getLogEntries() {

        SortedSet<SerializableLogEntry> entries = new TreeSet<SerializableLogEntry>();

        synchronized (allLogEntries) {
            if (selectedLogSource != null && !allLogEntries.containsKey(selectedLogSource)) {
                allLogEntries.put(selectedLogSource, new HashMap<Integer, SortedSet<SerializableLogEntry>>());
                // the log model of this instance does not contain any entries of remote instance until this subscribe is called for the
                // first time
                subscribeForNewLogEntriesAndRetrieveOldOnes(selectedLogSource);
            } else {
                for (Integer level : allLogEntries.get(selectedLogSource).keySet()) {
                    if (level != LogService.LOG_DEBUG) {
                        Map<Integer, SortedSet<SerializableLogEntry>> platformEntries = allLogEntries.get(selectedLogSource);
                        SortedSet<SerializableLogEntry> levelEntries = platformEntries.get(level);
                        synchronized (levelEntries) {
                            entries.addAll(levelEntries);
                        }
                    }
                }
            }
        }
        return entries;
    }

    /**
     * Adds a {@link LogEntry} to the whole list of the specified {@link InstanceNodeSessionId}.
     * 
     * @param logEntry The {@link LogEntry} to add.
     */
    public void addLogEntry(SerializableLogEntry logEntry) {
        InstanceNodeSessionId nodeId = logEntry.getPlatformIdentifer();

        synchronized (allLogEntries) {

            // If a node ID was removed from the allLogEntries map during a call of updateListOfLogSources, but we still receive a log entry
            // for this node, it can be ignored. The ignored log entry is not relevant for now (since the corresponding node is offline) and
            // the entry will be retrieved from the source instance again if the instance becomes available next time (and is selected as
            // log source and getLogEntries is called).
            if (allLogEntries.get(nodeId) == null) {
                return;
            }

            if (!allLogEntries.get(nodeId).containsKey(logEntry.getLevel())) {
                allLogEntries.get(nodeId).put(logEntry.getLevel(),
                    Collections.synchronizedSortedSet(new TreeSet<SerializableLogEntry>()));
            }

            SortedSet<SerializableLogEntry> logEntries = allLogEntries.get(nodeId).get(logEntry.getLevel());
            while (logEntries.size() >= LOG_POOL_SIZE) {
                final SerializableLogEntry logEntryToRemove = logEntries.first();
                logEntries.remove(logEntryToRemove);
                for (final Listener listener : listeners) {
                    listener.handleLogEntryRemoved(logEntryToRemove);
                }
            }
            if (logEntries.add(logEntry)) {
                for (final Listener listener : listeners) {
                    listener.handleLogEntryAdded(logEntry);
                }
            }
        }

    }

    /**
     * Lets identify the current platform for which logging messages has to be shown.
     * 
     * @param nodeId The current platform identifier to set.
     */
    public synchronized void setSelectedLogSource(InstanceNodeSessionId nodeId) {
        selectedLogSource = nodeId;
    }

    /**
     * @return current platform.
     */
    public synchronized InstanceNodeSessionId getCurrentLogSource() {
        return selectedLogSource;
    }

    /**
     * Gathers all platform identifiers and provides them in array.
     * 
     * @return Array of platform identifiers.
     */
    public synchronized List<InstanceNodeSessionId> updateListOfLogSources() {

        Set<InstanceNodeSessionId> newWorkflowHostNodesAndSelf = workflowHostService.getWorkflowHostNodesAndSelf();

        // remove all entries for nodes that are not reachable anymore
        Set<InstanceNodeSessionId> nodeIdsRemoved = new HashSet<>(currentWorkflowHostsAndSelf);
        nodeIdsRemoved.removeAll(newWorkflowHostNodesAndSelf);
        for (InstanceNodeSessionId nodeIdRemoved : nodeIdsRemoved) {
            allLogEntries.remove(nodeIdRemoved);
        }

        currentWorkflowHostsAndSelf = newWorkflowHostNodesAndSelf;

        // gather all log sources ...
        List<InstanceNodeSessionId> logSources = new ArrayList<>();

        InstanceNodeSessionId localNodeId = null;
        for (InstanceNodeSessionId nodeId : currentWorkflowHostsAndSelf) {
            // ... but skip the current instance ...
            if (platformService.matchesLocalInstance(nodeId)) {
                localNodeId = nodeId;
            } else {
                logSources.add(nodeId);
            }
        }

        Collections.sort(logSources, new Comparator<InstanceNodeSessionId>() {

            @Override
            public int compare(InstanceNodeSessionId o1, InstanceNodeSessionId o2) {
                return o1.getAssociatedDisplayName().compareTo(o2.getAssociatedDisplayName());
            }
        });

        // ... and add it to the front of the log sources after they have been sorted
        if (localNodeId != null) {
            logSources.add(0, localNodeId);
        }

        return logSources;
    }

    /** Removes log entries. **/
    public void clear() {
        synchronized (allLogEntries) {
            if (selectedLogSource == null) {
                for (InstanceNodeSessionId pi : allLogEntries.keySet()) {
                    allLogEntries.get(pi).clear();
                }
            } else {
                allLogEntries.get(selectedLogSource).clear();
            }
        }
    }

    private synchronized void subscribeForNewLogEntriesAndRetrieveOldOnes(final InstanceNodeSessionId node) {

        Job job = new Job("Fetching log") {

            @Override
            protected IStatus run(IProgressMonitor monitor) {
                try {
                    monitor.beginTask(Messages.fetchingLogs, 7);
                    monitor.worked(1);
                    // set the listener to recognize new message in future
                    LogListener logListener = new LogListener(selectedLogSource);
                    logReaderService.addLogListener(logListener, selectedLogSource);
                    monitor.worked(1);
                    List<SerializableLogEntry> retrievedLogEntries = logReaderService.getLog(node);
                    monitor.worked(2);
                    Map<Integer, SortedSet<SerializableLogEntry>> logEntries = new ConcurrentHashMap<Integer,
                        SortedSet<SerializableLogEntry>>();
                    for (SerializableLogEntry retrievedLogEntry : retrievedLogEntries) {
                        if (!logEntries.containsKey(retrievedLogEntry.getLevel())) {
                            logEntries.put(retrievedLogEntry.getLevel(),
                                Collections.synchronizedSortedSet(new TreeSet<SerializableLogEntry>()));
                        }
                        retrievedLogEntry.setPlatformIdentifer(node);
                        logEntries.get(retrievedLogEntry.getLevel()).add(retrievedLogEntry);
                    }
                    monitor.worked(1);
                    monitor.worked(1);
                    for (Set<SerializableLogEntry> entries : logEntries.values()) {
                        for (SerializableLogEntry entry : entries) {
                            addLogEntry(entry);
                        }
                    }
                    monitor.worked(1);
                    return Status.OK_STATUS;
                } finally {
                    monitor.done();
                }
            };
        };
        job.setUser(true);
        job.schedule();
    }

    /**
     * Adds a {@link Listener}.
     * 
     * @param listener the {@link Listener} to add
     */
    public void addListener(final Listener listener) {
        listeners.add(listener);
    }

    /**
     * Removes a {@link Listener}.
     * 
     * @param listener the {@link Listener} to remove
     */
    public void removeListener(final Listener listener) {
        listeners.remove(listener);
    }

    /**
     * Listener interface to listen to {@link LogModel} changes.
     * 
     * @author Christian Weiss
     */
    public interface Listener {

        /**
         * Handle the addition of a {@link LogEntry}.
         * 
         * @param logEntry the newly added {@link LogEntry}
         */
        void handleLogEntryAdded(SerializableLogEntry logEntry);

        /**
         * Handle the removal of a {@link LogEntry}.
         * 
         * @param logEntry the removed {@link LogEntry}
         */
        void handleLogEntryRemoved(SerializableLogEntry logEntry);

    }

}
