/*******************************************************************************
 * Copyright (c) 2012-2017 Codenvy, S.A.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *   Codenvy, S.A. - initial API and implementation
 *******************************************************************************/
package org.eclipse.che.api.core.util;

import com.sun.jna.Library;
import com.sun.jna.Native;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;

/**
 * Process manager for *nix like system.
 *
 * @author andrew00x
 */
class UnixProcessManager extends ProcessManager {
    /*
    At the moment tested on linux only.
     */

    private static final Logger LOG = LoggerFactory.getLogger(UnixProcessManager.class);

    private static final CLibrary C_LIBRARY;

    private static final Field PID_FIELD;

    static {
        CLibrary lib = null;
        Field pidField = null;
        if (SystemInfo.isUnix()) {
            try {
                lib = ((CLibrary)Native.loadLibrary("c", CLibrary.class));
            } catch (Exception e) {
                LOG.error("Cannot load native library", e);
            }
            try {
                pidField = Thread.currentThread().getContextClassLoader().loadClass("java.lang.UNIXProcess").getDeclaredField("pid");
                pidField.setAccessible(true);
            } catch (Exception e) {
                LOG.error(e.getMessage(), e);
            }
        }
        C_LIBRARY = lib;
        PID_FIELD = pidField;
    }

    private static interface CLibrary extends Library {
        // kill -l
        int SIGKILL = 9;
        int SIGTERM = 15;

        int kill(int pid, int signal);

        String strerror(int errno);

        int system(String cmd);
    }

    private static final Pattern UNIX_PS_TABLE_PATTERN = Pattern.compile("\\s+");

    @Override
    public void kill(Process process) {
        if (C_LIBRARY != null) {
            killTree(getPid(process));
        } else {
            throw new IllegalStateException("Can't kill process. Not unix system?");
        }
    }

    private void killTree(int pid) {
        final int[] children = getChildProcesses(pid);
        LOG.debug("PID: {}, child PIDs: {}", pid, children);
        if (children.length > 0) {
            for (int cpid : children) {
                killTree(cpid); // kill process tree recursively
            }
        }
        int r = C_LIBRARY.kill(pid, CLibrary.SIGKILL); // kill origin process
        LOG.debug("kill {}", pid);
        if (r != 0) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("kill for {} returns {}, strerror '{}'", pid, r, C_LIBRARY.strerror(r));
            }
        }
    }

    private int[] getChildProcesses(final int myPid) {
        final String ps = "ps -e -o ppid,pid,comm"; /* PPID, PID, COMMAND */
        final List<Integer> children = new ArrayList<>();
        final StringBuilder error = new StringBuilder();
        final LineConsumer stdout = new LineConsumer() {
            @Override
            public void writeLine(String line) throws IOException {
                if (line != null && !line.isEmpty()) {
                    final String[] tokens = UNIX_PS_TABLE_PATTERN.split(line.trim());
                    if (tokens.length == 3 /* PPID, PID, COMMAND */) {
                        int ppid;
                        try {
                            ppid = Integer.parseInt(tokens[0]);
                        } catch (NumberFormatException e) {
                            // May be first line from process table: 'PPID PID COMMAND'. Skip it.
                            return;
                        }
                        if (ppid == myPid) {
                            int pid = Integer.parseInt(tokens[1]);
                            children.add(pid);
                        }
                    }
                }
            }

            @Override
            public void close() throws IOException {
            }
        };

        final LineConsumer stderr = new LineConsumer() {
            @Override
            public void writeLine(String line) throws IOException {
                if (error.length() > 0) {
                    error.append('\n');
                }
                error.append(line);
            }

            @Override
            public void close() throws IOException {
            }
        };

        try {
            ProcessUtil.process(Runtime.getRuntime().exec(ps), stdout, stderr);
        } catch (IOException e) {
            throw new IllegalStateException(e);
        }

        if (error.length() > 0) {
            throw new IllegalStateException("can't get child processes: " + error.toString());
        }
        final int size = children.size();
        final int[] result = new int[size];
        for (int i = 0; i < size; i++) {
            result[i] = children.get(i);
        }
        return result;
    }

    @Override
    public boolean isAlive(Process process) {
        return process.isAlive();
    }

    int getPid(Process process) {
        if (PID_FIELD != null) {
            try {
                return ((Number)PID_FIELD.get(process)).intValue();
            } catch (IllegalAccessException e) {
                throw new IllegalStateException("Can't get process' pid. Not unix system?", e);
            }
        } else {
            throw new IllegalStateException("Can't get process' pid. Not unix system?");
        }
    }

    @Override
    int system(String command) {
        return C_LIBRARY.system(command);
    }
}
