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

package de.rcenvironment.core.monitoring.system.internal;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hyperic.sigar.FileSystem;
import org.hyperic.sigar.FileSystemUsage;
import org.hyperic.sigar.Humidor;
import org.hyperic.sigar.ProcState;
import org.hyperic.sigar.Sigar;
import org.hyperic.sigar.SigarException;
import org.hyperic.sigar.SigarProxy;
import org.hyperic.sigar.ptql.ProcessFinder;
import org.osgi.framework.BundleContext;

import de.rcenvironment.core.monitoring.system.api.OperatingSystemException;
import de.rcenvironment.core.monitoring.system.api.SystemMonitoringDataService;
import de.rcenvironment.core.monitoring.system.api.model.ProcessInformation;

/**
 * Implementation of {@link SystemMonitoringDataService}.
 * 
 * @author David Scholz
 * @author Robert Mischke
 */
public class SystemMonitoringDataServiceImpl implements SystemMonitoringDataService {

    private static final Sigar SIGAR = new Sigar();

    private static final int CONVERSION_FACTOR_1K = 1024;

    private static final double CONVERSION_FACTOR_PERCENT = 100;

    private static final int SIG_ID_KILL = 15;

    private static final int SIG_ID_FORCE_KILL = -9;

    private static final String NO_SUCH_PROCESS_MESSAGE = "No such process";

    private static final String ACCESS_DENIED_EXCEPTION = "Access is denied.";

    private static final Log LOGGER = LogFactory.getLog(SystemMonitoringDataServiceImpl.class);

    private static final int TOTAL_CPU_NAN_MAX_RETRY_COUNT = 50; // maximum observed in local testing was ~20 retries at 1 msec wait

    private static final int TOTAL_CPU_NAN_RETRY_WAIT_MSEC = 1;

    private long cachedTotalSystemRam;

    protected void activate(BundleContext bundleContext) {
        try {
            cachedTotalSystemRam = fetchTotalSystemRAM();
        } catch (OperatingSystemException e) {
            // TODO or re-throw the exception and refuse to start instead?
            cachedTotalSystemRam = 0;
            LOGGER.error("Failed to initialize total system RAM: " + e.toString());
        }
    }

    @Override
    public double getTotalCPUUsage() throws OperatingSystemException {
        try {
            double cpuUsage;
            final SigarProxy sigarProxy = Humidor.getInstance().getSigar();
            cpuUsage = sigarProxy.getCpuPerc().getCombined();
            if (Double.isNaN(cpuUsage)) {
                int retryCount = 1;
                while (true) {
                    cpuUsage = sigarProxy.getCpuPerc().getCombined();
                    if (!Double.isNaN(cpuUsage)) {
                        // valid
                        LOGGER.debug("Fetched valid CPU load data after " + retryCount + " immediate retries");
                        break;
                    }
                    if (retryCount < TOTAL_CPU_NAN_MAX_RETRY_COUNT) {
                        try {
                            Thread.sleep(TOTAL_CPU_NAN_RETRY_WAIT_MSEC);
                        } catch (InterruptedException e) {
                            throw new OperatingSystemException(OperatingSystemException.ErrorType.FAILED_TO_GATHER_TOTAL_CPU_USAGE,
                                "Interrupted while waiting for CPU load data retry");
                        }
                        retryCount++;
                    } else {
                        throw new OperatingSystemException(OperatingSystemException.ErrorType.FAILED_TO_GATHER_TOTAL_CPU_USAGE,
                            "Failed to fetch valid CPU load data even after " + retryCount + " immediate retries");
                    }
                }
            }
            return SystemMonitoringUtils.clampToPercentageOrNAN(cpuUsage);
        } catch (SigarException e) {
            throw new OperatingSystemException(OperatingSystemException.ErrorType.FAILED_TO_GATHER_TOTAL_CPU_USAGE);
        }
    }

    @Override
    public double getProcessCPUUsage(Long pid) throws OperatingSystemException {
        double processCPUusage = 0;
        int watch = 0;
        try {
            if (pid == null) {
                throw new OperatingSystemException(OperatingSystemException.ErrorType.NO_SUCH_PROCESS);
            }

            // TODO review this for NaN handling; this only tries to prevent 0.0 values
            do {
                synchronized (SIGAR) {
                    processCPUusage = SIGAR.getProcCpu(pid).getPercent();
                }

                watch++;
                if (watch == 10) {
                    return processCPUusage;
                }
            } while (processCPUusage == 0.0);

        } catch (SigarException e) {
            if (e.getMessage().contains(NO_SUCH_PROCESS_MESSAGE)) {
                throw createNoSuchProcessException(pid);
            } else if (e.getMessage().contains(ACCESS_DENIED_EXCEPTION)) {
                throw createAccessDeniedException(pid);
            } else {
                throw new OperatingSystemException(OperatingSystemException.ErrorType.FAILED_TO_GATHER_CPU_PROCESS_USAGE,
                    " of process with pid: " + pid);
            }
        }

        synchronized (SIGAR) {
            try {
                processCPUusage /= SIGAR.getCpuList().length;
            } catch (SigarException e) {
                throw new OperatingSystemException(OperatingSystemException.ErrorType.FAILED_TO_GET_CPU_LIST);
            }
        }

        return SystemMonitoringUtils.clampToPercentageOrNAN(processCPUusage);
    }

    @Override
    public double getReportedCPUIdle() throws OperatingSystemException {
        try {
            double idle = 0.0;
            synchronized (SIGAR) {
                idle = SIGAR.getCpuPerc().getIdle();
            }
            return SystemMonitoringUtils.clampToPercentageOrNAN(idle);
        } catch (SigarException e) {
            throw new OperatingSystemException(OperatingSystemException.ErrorType.FAILED_TO_GATHER_IDLE);
        }
    }

    @Override
    public long getTotalSystemRAM() throws OperatingSystemException {
        return cachedTotalSystemRam;
    }

    @Override
    public long getProcessRAMUsage(Long pid) throws OperatingSystemException {
        try {
            if (pid == null) {
                throw new OperatingSystemException(OperatingSystemException.ErrorType.NO_SUCH_PROCESS);
            }
            return Humidor.getInstance().getSigar().getProcMem(pid).getResident() / (CONVERSION_FACTOR_1K * CONVERSION_FACTOR_1K);
        } catch (SigarException e) {
            if (e.getMessage().contains(NO_SUCH_PROCESS_MESSAGE)) {
                throw createNoSuchProcessException(pid);
            } else if (e.getMessage().contains(ACCESS_DENIED_EXCEPTION)) {
                throw createAccessDeniedException(pid);
            } else {
                throw new OperatingSystemException(OperatingSystemException.ErrorType.FAILED_TO_GATHER_RAM_PROCESS_USAGE,
                    " of process with pid: " + pid);
            }
        }
    }

    @Override
    public long getTotalUsedRAM() throws OperatingSystemException {
        return (long) (cachedTotalSystemRam * getTotalUsedRAMPercentage());
    }

    @Override
    public long getFreeRAM() throws OperatingSystemException {
        return (long) (cachedTotalSystemRam * (1.0 - getTotalUsedRAMPercentage()));
    }

    @Override
    public double getTotalUsedRAMPercentage() throws OperatingSystemException {
        try {
            return Humidor.getInstance().getSigar().getMem().getUsedPercent() / CONVERSION_FACTOR_PERCENT;
        } catch (SigarException e) {
            throw new OperatingSystemException(OperatingSystemException.ErrorType.FAILED_TO_GATHER_TOTAL_RAM_PERCENTAGE);
        }
    }

    @Override
    public double getProcessRAMPercentage(Long pid) throws OperatingSystemException {
        long totalRam = getTotalSystemRAM();
        if (totalRam == 0) {
            throw new OperatingSystemException(OperatingSystemException.ErrorType.FAILED_TO_GATHER_TOTAL_RAM_PERCENTAGE);
        }
        long ram = getProcessRAMUsage(pid);
        return ram / totalRam;
    }

    @Override
    public long getUsedLocalDiskSpace() throws OperatingSystemException {
        return getFileSystemUsage().getUsed();
    }

    @Override
    public long getFreeLocalDiskSpace() throws OperatingSystemException {
        return getFileSystemUsage().getFree();
    }

    @Override
    public Map<Long, String> getProcesses() throws OperatingSystemException {
        try {
            final Map<Long, String> processMap = new HashMap<>();
            final List<Long> processList = Arrays.<Long> asList(ArrayUtils.toObject(Humidor.getInstance().getSigar().getProcList()));

            for (Long pid : processList) {
                synchronized (SIGAR) {
                    // this is necessary!
                    long[] pids =
                        ProcessFinder.find(SIGAR,
                            PTQLWrapper.createQuery().createQueryString(PTQLWrapper.pid(), PTQLWrapper.eq()) + pid);
                    for (long process : pids) {
                        processMap.put(process, Humidor.getInstance().getSigar().getProcState(process).getName());
                    }
                }
            }
            return processMap;
        } catch (SigarException e) {
            throw new OperatingSystemException(OperatingSystemException.ErrorType.FAILED_TO_GET_PROCESS_LIST);
        }

    }

    @Override
    public Map<Long, String> getChildProcessesAndIds(Long pid) throws OperatingSystemException {

        if (pid == null) {
            return Collections.emptyMap();
        }

        final List<ProcessInformation> childList;
        childList = getFullChildProcessInformation(pid);

        final Map<Long, String> childMap = new HashMap<>();
        if (!childList.isEmpty()) {
            for (ProcessInformation process : childList) {
                childMap.put(process.getPid(), process.getName());
            }
        }
        return childMap;
    }

    @Override
    public void kill(Long pid, Boolean force) throws OperatingSystemException {
        try {
            LOGGER.info("Killing process with pid: " + pid);
            if (force) {
                synchronized (SIGAR) {
                    SIGAR.kill(pid, SIG_ID_FORCE_KILL);
                }
            } else {
                synchronized (SIGAR) {
                    SIGAR.kill(pid, SIG_ID_KILL);
                }
            }
        } catch (final SigarException e) {
            if (e.getMessage().contains("The parameter is incorrect.")) {
                LOGGER.error("Failed to shut down process caused by incorrect parameters. The process with pid: " + pid
                    + " may already be dead.");
                return;
            } else {
                if (e.getMessage().contains(ACCESS_DENIED_EXCEPTION)) {
                    throw createAccessDeniedException(pid);
                }
                throw new OperatingSystemException(OperatingSystemException.ErrorType.FAILED_TO_KILL_PROCESS, " with pid: " + pid);
            }
        }
    }

    private long fetchTotalSystemRAM() throws OperatingSystemException {
        try {
            final long ramValue = Humidor.getInstance().getSigar().getMem().getRam();
            if (ramValue == 0) {
                throw new OperatingSystemException(OperatingSystemException.ErrorType.FAILED_TO_GATHER_TOTAL_RAM);
            }
            return ramValue;
        } catch (SigarException e) {
            throw new OperatingSystemException(OperatingSystemException.ErrorType.FAILED_TO_GATHER_TOTAL_RAM);
        }
    }

    @Override
    public List<ProcessInformation> getFullChildProcessInformation(long pid) throws OperatingSystemException {
        long[] pids = null;
        List<ProcessInformation> children = null;
        synchronized (SIGAR) {
            try {
                pids = ProcessFinder.find(SIGAR,
                    PTQLWrapper.createQuery().createQueryString(PTQLWrapper.statePPID(), PTQLWrapper.eq()) + pid);
            } catch (SigarException e) {
                if (e.getMessage().equals(ACCESS_DENIED_EXCEPTION)) {
                    throw createAccessDeniedException(pid);
                }
                throw new OperatingSystemException(OperatingSystemException.ErrorType.FAILED_TO_GET_CHILD_PROCESS_LIST,
                    " of parent process with pid: " + pid);
            }
        }
        if (pids != null && pids.length > 0) {
            children = new ArrayList<>();
            for (Long child : pids) {
                ProcessInformation p;
                try {
                    p =
                        new ProcessInformation(child, fetchProcessState(child).getName(), getFullChildProcessInformation(child),
                            getProcessCPUUsage(child),
                            getProcessRAMUsage(child));
                } catch (OperatingSystemException e) {
                    if (e.getErrorType().equals(OperatingSystemException.ErrorType.NO_SUCH_PROCESS)) {
                        LOGGER.info("Couldn't find process with pid: " + child + ". Process might already be dead.");
                        continue;
                    }
                    throw new OperatingSystemException(OperatingSystemException.ErrorType.FAILED_TO_GET_CHILD_PROCESS_LIST,
                        " of parent process with pid: " + pid);
                }
                children.add(p);
            }
        }
        if (children != null) {
            return children;
        } else {
            return Collections.emptyList();
        }
    }

    private FileSystemUsage getFileSystemUsage() throws OperatingSystemException {
        FileSystem[] fileSystemList;
        try {
            fileSystemList = Humidor.getInstance().getSigar().getFileSystemList();
        } catch (SigarException e) {
            throw new OperatingSystemException(OperatingSystemException.ErrorType.FAILED_TO_GET_FILE_SYSTEM);
        }
        FileSystemUsage usage = null;

        for (FileSystem fs : fileSystemList) {
            if (fs.getType() == FileSystem.TYPE_LOCAL_DISK) {
                try {
                    usage = Humidor.getInstance().getSigar().getFileSystemUsage(fs.getDirName());
                } catch (SigarException e) {
                    throw new OperatingSystemException(OperatingSystemException.ErrorType.FAILED_TO_GET_FILE_SYSTEM_USAGE);
                }
            }
        }
        return usage;
    }

    @Override
    public ProcState fetchProcessState(long pid) throws OperatingSystemException {
        try {
            return Humidor.getInstance().getSigar().getProcState(pid);
        } catch (SigarException e) {
            throw new OperatingSystemException(
                OperatingSystemException.ErrorType.FAILED_TO_GET_PROCESS_STATE + " for process with pid: " + pid);
        }
    }

    private OperatingSystemException createAccessDeniedException(long pid) {
        return new OperatingSystemException(OperatingSystemException.ErrorType.ACCESS_DENIED, " to process with pid: " + pid
            + ". You may not have the appropriate permissions.");
    }

    private OperatingSystemException createNoSuchProcessException(long pid) {
        return new OperatingSystemException(OperatingSystemException.ErrorType.NO_SUCH_PROCESS, ". The process with pid: " + pid
            + " may not exist.");
    }

}
