/* 
-*- coding: latin-1 -*-

This file is part of RefactorErl.

RefactorErl is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

RefactorErl is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with RefactorErl.  If not, see <http://plc.inf.elte.hu/erlang/>.

The Original Code is RefactorErl.

The Initial Developer of the Original Code is Eötvös Loránd University.
Portions created  by Eötvös Loránd University and ELTE-Soft Ltd.
are Copyright 2007-2025 Eötvös Loránd University, ELTE-Soft Ltd.
and Ericsson Hungary. All Rights Reserved.
 */

package com.refactorerl.ui.communication.process;

import java.io.File;
import java.io.IOException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.ericsson.otp.erlang.OtpAuthException;
import com.ericsson.otp.erlang.OtpConnection;
import com.ericsson.otp.erlang.OtpErlangAtom;
import com.ericsson.otp.erlang.OtpErlangExit;
import com.ericsson.otp.erlang.OtpErlangObject;
import com.ericsson.otp.erlang.OtpPeer;
import com.ericsson.otp.erlang.OtpSelf;
import com.refactorerl.ui.common.Environment;
import com.refactorerl.ui.common.OtpErlangHelper;

/**
 * A proxy class for a local RefactorErl server process. This class starts a
 * server process, checks if a local connection can be made to it and logs
 * everything the process writes to its standard output. Use the stop() method
 * to shutdown the server process.
 * 
 * @author Daniel Lukacs, 2014 ELTE IK
 *
 */
public class ReferlProcess {

	public static enum DatabaseType {
		MNESIA, NIF, KYOTO
	};

	private static Map<DatabaseType, String> databaseTypeDictionary = new HashMap<>();
	static {
		databaseTypeDictionary.put(DatabaseType.MNESIA, "mnesia"); //$NON-NLS-1$
		databaseTypeDictionary.put(DatabaseType.NIF, "nif"); //$NON-NLS-1$
		databaseTypeDictionary.put(DatabaseType.KYOTO, "kcmini"); //$NON-NLS-1$
	}

	public static String getDatabaseTypeString(DatabaseType type) {
		return databaseTypeDictionary.get(type);
	}
	
	private static int PROCESS_START_TIMEOUT = 4000;
	final private int RECONNECT_DELAY = 1000;
	final private int RECONNECT_ATTEMPTS = 10;

	private Process process;
	private IOHandler ioHandler;

	final private String baseDirPath;
	final private String executablePath;
	final private String aliveName;
	final private DatabaseType databaseType;
	private String dataDir;

	private Logger logger;

	/**
	 * 
	 * @param baseDirPath
	 *            Path to the base directory of the RefactorErl application.
	 * @param aliveName
	 *            Alive name of the RefactorErl server process to be created.
	 *            Host name will be automatically set to the local host name.
	 * @param databaseType
	 *            The type of the database to start the RefactorErl application
	 *            with.
	 * @param dataDir
	 *            Optional. The directory where the RefactorErl server will
	 *            store its data.
	 * @param logHandlers
	 */
	public ReferlProcess(String baseDirPath, String aliveName,
			DatabaseType databaseType, String dataDir,  Handler[] logHandlers, IOHandler ioHandler) {
		this.baseDirPath = baseDirPath;
		this.executablePath = baseDirPath
				+ File.separator
				+ "bin" + File.separator + "referl" + //$NON-NLS-1$ //$NON-NLS-2$ 
				(Environment.isLocalSysWindows() ? ".bat" : ""); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
		this.aliveName = aliveName == null || aliveName.isEmpty() ? "refactorerl@localhost" : aliveName; //$NON-NLS-1$ 
		this.databaseType = databaseType;
		this.dataDir = dataDir == null || dataDir.isEmpty() ? null : dataDir;
		this.ioHandler = ioHandler;	
		configureLogger(logHandlers);
	}

	/**
	 * Initializes this ReferlProcess instance. Calling this method before any
	 * other non-static method of this class is mandatory. It starts a server
	 * process, checks if a local connection can be made to it and logs
	 * everything the process writes to its standard output.
	 * 
	 * @return True, if the process was started, and all the tests regarding
	 *         connectivity passed.
	 */
	public boolean start() {
		if (process == null) {
			List<String> args = new ArrayList<>();
			args.add(executablePath);
			args.add("-db"); //$NON-NLS-1$
			args.add(getDatabaseTypeString(databaseType));
			if(dataDir != null){
				args.add("-dir"); //$NON-NLS-1$
				args.add(dataDir);
			}
			if(aliveName != null){
				args.add("-sname"); //$NON-NLS-1$
				args.add(aliveName);
			}
			ProcessBuilder pb = new ProcessBuilder(args);
			pb.directory(new File(baseDirPath));
			logger.info("Command:" + args);
			
			try {
				process = pb.start();
			} catch (IOException e) {
				getLogger().severe(Messages.ReferlProcess_10);
				getLogger().severe(e.toString());
				return false;
			} catch (NullPointerException e) {
				getLogger().severe(Messages.ReferlProcess_0);
				getLogger().severe(e.toString());
				return false;
			}

			if (process == null) {
				getLogger().severe(Messages.ReferlProcess_10);
				getLogger().severe("process == null"); //$NON-NLS-1$
				return false;
			}
			
			if(ioHandler != null){
			ioHandler.open(process.getInputStream(), 
					process.getErrorStream(),
					process.getOutputStream());
			}
		
			if (!testProcessStarted()){
				stop();
				return false;
			}
			
			if (!testConnectivity()){
				stop();
				return false;
			}

			getLogger().info(Messages.ReferlProcess_11);
			
			return true;
		}
		
		stop();
		return false;
	}
	
	public boolean testProcessStarted(){
		try {
			Thread.sleep(PROCESS_START_TIMEOUT);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		try {
			process.exitValue();
			return false;
		} catch (IllegalThreadStateException e){
			// ok, the process lives
			return true;
		}
		
	}
	
	/**
	 * Gradually tests the connectivity of the newly created RefactorErl
	 * process. This is needed to know if the process finished its
	 * initialization and is ready for communication. If a test fails, it
	 * reattempts the test for a fixed number of times. First it remotely calls
	 * erlang:node(). Next it tries if reflib_ui_router module is loaded:
	 * code:is_loaded(reflib_ui_router) If not, it remotely asks the node to
	 * load it. code:load_file(reflib_ui_router) Then it registers a message
	 * handler to RefactorErl: reflib_ui_router:add_msg_handler(pid()) Finally
	 * it unregisters that message handler:
	 * reflib_ui_router:del_msg_handler(pid())
	 * 
	 * @return True, if all the tests passed.
	 */
	private boolean testConnectivity() {
		OtpConnection conn = null;
		getLogger().info(Messages.ReferlProcess_12);
		int attempts = 1;
		while (attempts <= RECONNECT_ATTEMPTS) {

			
			try {
				OtpSelf self = new OtpSelf("test@" + Environment.getHostName()); //$NON-NLS-1$
				OtpPeer peer = new OtpPeer(getFullAddress()); //$NON-NLS-1$
				conn = self.connect(peer);
				conn.sendRPC("erlang", "node", new OtpErlangObject[0]); //$NON-NLS-1$ //$NON-NLS-2$
				OtpErlangObject reply = conn.receiveRPC();
				break;
			} catch (UnknownHostException | OtpAuthException | OtpErlangExit e) {
				getLogger().severe(Messages.ReferlProcess_17 + e);
				stop();
				if (conn != null)
					conn.close();
				return false;
			} catch (IOException e) {
				getLogger().info(
						Messages.ReferlProcess_18 + attempts
								+ Messages.ReferlProcess_19
								+ RECONNECT_ATTEMPTS + ")"); //$NON-NLS-1$
				getLogger().info(e.toString());
				attempts++;
				try {
					Thread.sleep(RECONNECT_DELAY);
				} catch (InterruptedException e1) {
					getLogger().warning(Messages.ReferlProcess_4);
				}
				continue;
			}
		}

		if (attempts > RECONNECT_ATTEMPTS) {
			getLogger().severe(Messages.ReferlProcess_5);
			getLogger().severe(Messages.ReferlProcess_21);
			stop();
			if (conn != null)
				conn.close();
			return false;
		}

		getLogger().info(Messages.ReferlProcess_22);
		getLogger().info(Messages.ReferlProcess_23);

		attempts = 1;
		int stage = 0;
		while (attempts <= RECONNECT_ATTEMPTS) {
			try {
				OtpErlangObject reply = null;
				if (stage == 0) {
					conn.sendRPC(
							"code", "is_loaded", new OtpErlangObject[] { new OtpErlangAtom("reflib_ui_router") }); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
					getLogger().info(
							Messages.ReferlProcess_27
									+ "code:is_loaded(reflib_ui_router)"); //$NON-NLS-2$ //$NON-NLS-1$ //$NON-NLS-1$
					reply = conn.receiveRPC();
					getLogger().info(Messages.ReferlProcess_29 + reply);

					if (reply == null)
						throw new IOException(Messages.ReferlProcess_30);
					else if (OtpErlangHelper.isOtpCollection(reply)
							&& OtpErlangHelper
									.nthEqualsAtom(reply, 0, "badrpc")) //$NON-NLS-1$
						throw new IOException(
								OtpErlangHelper.toUnquotedString(reply));

					else if (OtpErlangHelper.equalsAtom(reply, "false")) { //$NON-NLS-1$
						conn.sendRPC("code", "load_file", //$NON-NLS-1$ //$NON-NLS-2$
								new OtpErlangObject[] { new OtpErlangAtom(
										"reflib_ui_router") }); //$NON-NLS-1$
						getLogger().info(
								Messages.ReferlProcess_36
										+ "code:load_file(reflib_ui_router)"); //$NON-NLS-2$ //$NON-NLS-1$ //$NON-NLS-1$
						reply = conn.receiveRPC();
						getLogger().info(Messages.ReferlProcess_38 + reply);

						try {
							Thread.sleep(RECONNECT_DELAY);
						} catch (InterruptedException e1) {
							getLogger().warning(Messages.ReferlProcess_8);
						}
						continue;
					}

					attempts = 1;
					++stage;

				} else if (stage == 1) {
					conn.sendRPC(
							"reflib_ui_router", "add_msg_handler", new OtpErlangObject[] { conn.self().pid() }); //$NON-NLS-1$ //$NON-NLS-2$
					getLogger()
							.info(Messages.ReferlProcess_41
									+ "reflib_ui_router:add_msg_handler(" + conn.self().pid() + ")"); //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$
					reply = conn.receiveRPC();
					getLogger().info(Messages.ReferlProcess_44 + reply);

					if (reply == null)
						throw new IOException(Messages.ReferlProcess_45);
					else if (OtpErlangHelper.isOtpCollection(reply)
							&& OtpErlangHelper
									.nthEqualsAtom(reply, 0, "badrpc")) //$NON-NLS-1$
						throw new IOException(
								OtpErlangHelper.toUnquotedString(reply));

					attempts = 1;
					++stage;

				} else if (stage == 2) {

					conn.sendRPC(
							"reflib_ui_router", "del_msg_handler", new OtpErlangObject[] { conn.self().pid() }); //$NON-NLS-1$ //$NON-NLS-2$
					getLogger()
							.info(Messages.ReferlProcess_49
									+ "reflib_ui_router:del_msg_handler(" + conn.self().pid() + ")"); //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$
					reply = conn.receiveRPC();
					getLogger().info(Messages.ReferlProcess_52 + reply);

					if (reply == null)
						throw new IOException(Messages.ReferlProcess_53);
					else if (OtpErlangHelper.isOtpCollection(reply)
							&& OtpErlangHelper
									.nthEqualsAtom(reply, 0, "badrpc")) //$NON-NLS-1$
						throw new IOException(
								OtpErlangHelper.toUnquotedString(reply));

					attempts = 1;
					++stage;
				} else {
					break;
				}
			} catch (OtpAuthException | OtpErlangExit e) {
				getLogger().severe(Messages.ReferlProcess_55 + e);
				stop();
				conn.close();
				return false;
			} catch (IOException e) {
				getLogger().info(
						Messages.ReferlProcess_56 + attempts
								+ Messages.ReferlProcess_57
								+ RECONNECT_ATTEMPTS + ")"); //$NON-NLS-3$ //$NON-NLS-1$ //$NON-NLS-1$
				getLogger().info(e.toString());
				attempts++;
				try {
					Thread.sleep(RECONNECT_DELAY);
				} catch (InterruptedException e1) {
					getLogger().warning(Messages.ReferlProcess_13);
				}
				continue;
			}
		}
		if (attempts > RECONNECT_ATTEMPTS) {
			getLogger().severe(Messages.ReferlProcess_14);
			getLogger().severe(Messages.ReferlProcess_59);
			stop();
			conn.close();
			return false;
		}

		conn.close();
		return true;
	}
	
	/**
	 * 
	 * @return True
	 */
	public boolean stop() { // unfinished

		if (process != null) {
			if(ioHandler != null){
				try {
					ioHandler.close();
				} catch (IOException e) {
					e.printStackTrace();
				} catch (IllegalStateException e){					
				}
			}
			boolean isAlive = false;
			try{
				process.exitValue();
			} catch(IllegalThreadStateException e){
				isAlive = true;
			}
			if (isAlive) { // if(process.isAlive()){ // java8
				try {				
					process.getOutputStream().close();
				} catch (IOException e) {
					getLogger().warning(Messages.ReferlProcess_60);
					getLogger().warning(e.toString());
				}
				process.destroy();
				try {
					// process.waitFor(500, TimeUnit.MILLISECONDS); // java8
					final Thread t = Thread.currentThread();
					new Thread(new Runnable() {					
						@Override
						public void run() {
							try {
								Thread.sleep(500);
							} catch (InterruptedException e) {
								getLogger().warning(Messages.ReferlProcess_61);
								getLogger().warning(e.toString());
							}
							t.interrupt();
						}
					}).start();
					process.waitFor();					
				} catch (InterruptedException e) {
					getLogger().warning("Waiting for process to stop timed out.");
					getLogger().warning(e.toString());
				}

			}

		}
		getLogger().info(Messages.ReferlProcess_62);
		deconfigureLogger();
		return true;
	}

	public Logger getLogger() {
		return logger;
	}

	private void configureLogger(Handler[] handlers) {
		if (handlers != null && logger == null) {
			logger = Logger.getLogger(getClass().getName());
			logger.setLevel(Level.ALL);
			logger.setUseParentHandlers(false);

			for (Handler h : handlers) {
				logger.addHandler(h);

			}

		}
	}

	private void deconfigureLogger() {
		if (logger != null) {
			for (Handler h : logger.getHandlers()) {
				logger.removeHandler(h);
			}
		}
	}

	public void resetLogHandlers(Handler[] handlers) {
		deconfigureLogger();
		for (Handler h : handlers) {
			logger.addHandler(h);
		}
	}

	public String getFullAddress() {
		return  aliveName.contains("@") ? aliveName 
				: aliveName + "@"+ Environment.getHostName(); //$NON-NLS-1$
	}
}
