package cz.cuni.amis.pogamut.base.agent;

import cz.cuni.amis.introspection.Folder;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;

import com.google.inject.Inject;

import cz.cuni.amis.introspection.java.ReflectionObjectFolder;
import cz.cuni.amis.pogamut.base.agent.exceptions.AgentException;
import cz.cuni.amis.pogamut.base.agent.exceptions.CantStartJMXException;
import cz.cuni.amis.pogamut.base.agent.exceptions.JMXAlreadyEnabledException;
import cz.cuni.amis.pogamut.base.agent.jmx.AgentJMX;
import cz.cuni.amis.pogamut.base.agent.jmx.AgentMBeanAdapter;
import cz.cuni.amis.pogamut.base.exceptions.PogamutException;
import cz.cuni.amis.pogamut.base.factory.guice.AgentScoped;
import cz.cuni.amis.pogamut.base.utils.FolderToIJMXEnabledAdapter;
import cz.cuni.amis.pogamut.base.utils.IJMXEnabled;
import cz.cuni.amis.pogamut.base.utils.Pogamut;
import cz.cuni.amis.pogamut.base.utils.PogamutJMX;
import cz.cuni.amis.pogamut.base.utils.PogamutPlatform;
import cz.cuni.amis.pogamut.base.utils.logging.AgentLogger;
import cz.cuni.amis.utils.ExceptionToString;
import cz.cuni.amis.utils.Job;
import cz.cuni.amis.utils.flag.Flag;
import cz.cuni.amis.utils.flag.ImmutableFlag;

/**
 * Abstract agent class, provides basic interface for the agent + introducing
 * JMX.
 * <p><p>
 * We're purposely using neither inheritance here nor decorator (or such
 * wrappers) because we need JMX to be usable by every descendant of the class
 * (so we don't want to have another JMX class branch, nor wrapper that denies
 * the inheritance by its nature).
 * <p>
 * To keep the JMX method at minimum in the agent class we group them into the
 * private inner class object JMXComponents that can be accessed via getJMX()
 * method. This object maintain also a list of IJMXEnabled components that
 * should be enabled when the whole JMX feature is being enabled.
 * <p><p>
 * Usage:
 * <p>
 * <i>To start JMX on the agent:</i> getJMX().enableJMX(mBeanServer, domain)
 * <p>
 * <i>To add another JMX component to the agent:</i>
 * getJMX().addComponent(component)
 * </p>
 * 
 * @author Jimmy
 * 
 */
@AgentScoped
public abstract class AbstractAgent implements IAgent {

	/**
	 * Default amount of time (millis) to give the agent to stop, as default
	 * returned by the method getAgentStopTimeOutMillis().
	 */
	public static final long DEFAULT_AGENT_TIMEOUT_MILLIS = 2000;

	/**
	 * Logger that is used by the Pogamut (that means by the class hierarchy
	 * starting from AbstractAgent).
	 */
	private AgentLogger logger = null;

	/**
	 * Running state of the agent.
	 */
	private Flag<AgentState> agentState = 
		new Flag<AgentState>(
				new AgentState(AgentStateType.INSTANTIATED, "Created.") 
		);

	/**
	 * Detailed description of the state the agent is in - may change even if
	 * the agent state remains the same to provide more information about what's
	 * happening.
	 */
	private Flag<String> agentStateDescription = new Flag<String>(
			"Just created");

	/**
	 * Wraps a few methods to one place so it won't plague the public method
	 * space of the agent. (Make things a bit clear...).
	 * <p>
	 * <p>
	 * Contains list of IJMXEnabled components that should be enabled when the
	 * whole JMX feature of the agent is fired up.
	 * <p>
	 * <p>
	 * Lazy initialization upon calling getJMX().
	 */
	private AgentJMX jmx = null;

	/**
	 * Introspection folder with properties and other subfolders obtained from
	 * this agent.
	 */
	// TODO private Folder folder = null;
	@Inject
	public AbstractAgent(AgentLogger logger) {
		this.logger = logger;
		this.agentInit();
	}

	private void agentInit() {
		this.logPlatform(Level.FINEST, "AbstractAgent init - done.");
	}

	public AgentLogger getLogger() {
		return this.logger;
	}

	/**
	 * @return Introspection folder representing properties of this agent.
	 */
	/*
	 * TODO public Folder getFolder() { if(folder == null) { // TODO set name of
	 * this agent as folder name folder = Introspector.getFolder("TODO name",
	 * this); } return folder; }
	 */

	/**
	 * Logging user stuff - all logs passed via this method goes to the
	 * "user log" in NetBeans plugin.
	 * 
	 * @param level
	 * @param message
	 */
	protected void log(Level level, String message) {
		this.logger.user().log(level, message);
	}

	/**
	 * Logging platform messages - all logs passed via this method goes to the
	 * "platform log" in NetBeans plugin.
	 * 
	 * @param level
	 * @param message
	 */
	protected void logPlatform(Level level, String message) {
		this.logger.platform().log(level, message);
	}

	/**
	 * Sets the state of the agent ... note that the flag is private field so
	 * you can't change it directly.
	 * 
	 * @param state
	 */
	protected void setAgentState(AgentState state) {		
		synchronized(agentState) {
			this.logPlatform(Level.FINER, "Agent state is going to be switched to: " + state.toString());
			agentState.setFlag(state);
			this.logPlatform(Level.INFO, "Agent state switched to: " + state.toString());
		}		
	}
	
	/**
	 * Sets the state of the agent ... note that the flag is private field so
	 * you can't change it directly.
	 * <p><p>
	 * Note that if you change the agent state you must also change its description.
	 * 
	 * @param state
	 * @param description
	 */
	protected void setAgentState(AgentStateType state, String description) {		
		synchronized(agentState) {
			setAgentState(new AgentState(state, description));
		}		
	}
	
	/**
	 * Sets the description for the state of the agent. May be used to provide
	 * additional informations about the agent state. 
	 * 
	 * @param newDescription
	 */
	protected void setAgentStateDescription(String description) {
		synchronized(agentState) {
			setAgentState(new AgentState(agentState.getFlag().getType(), description));
		}
	}

	/**
	 * Returns flag that tells whether the agent is running or not (boolean). 
	 * <p><p>
	 * The AgentState also contains a String description providing detail information
	 * about the state.
	 * 
	 * @return
	 */
	@Override
	public ImmutableFlag<AgentState> getAgentState() {
		return this.agentState.getImmutable();
	}

	/**
	 * Returns unique name identifying this agent instance. The name format is:
	 * {agent object address} + "@" + {host computer name}. If computer name
	 * cannot be resolved then "unknown" value is used. This name is
	 * extremely inconvenient for humans. It is strongly encouraged to override
	 * this method and provide a human readable unique name for each agent.
	 * 
	 * @return
	 */
	public String getName() {
		try {
			return getClass().getSimpleName() + "@"
					+ InetAddress.getLocalHost().getCanonicalHostName();
		} catch (UnknownHostException ex) {
			return getClass().getSimpleName() + "@unknownHost";
		}
	}

	/**
	 * Called when AgentJMX (field jmx) is instantiated to populate it with
	 * agent's JMX enabled components.
	 * <p>
	 * <p>
	 * Currently two components are added:
	 * <ol>
	 * <li>agent's logger</li>
	 * <li>agent's introspection</li>
	 * </ol>
	 * <p>
	 * <p>
	 * If you override this method <b>don't forget</b> to call
	 * <i>super.addAgentJMXComponents()</i> as the first method.
	 */
	protected void addAgentJMXComponents() {
		jmx.addComponent(logger);
		jmx.addComponent(new FolderToIJMXEnabledAdapter(getFolder()));
	}

	/**
	 * Returns support class for the JMX feature of the agent. You may use it to
	 * register new JMX components of the agent or for enabling of the whole
	 * feature.
	 * 
	 * @return
	 */
	public synchronized AgentJMX getJMX() {
		if (jmx == null) {
			jmx = new AgentJMX();
			addAgentJMXComponents();
		}
		return jmx;
	}

	/**
	 * Attempt to launch the agent. <BR>
	 * <BR>
	 * Returns Future with the result of the start, it's get() method will block
	 * until the agent reaches one of it's "ok" or "end" states (see AgentState
	 * doc).
	 * 
	 * @return future object describing success, whether the agent has been
	 *         started or not.
	 * @throws AgentException
	 */
	@Override
	public abstract void start() throws AgentException;

	/**
	 * This should pause the logic of the agent. It should also be reflected in
	 * the agent state. <BR>
	 * <BR>
	 * If your agent can't be paused, throw OperationNotSupportedException.
	 */
	@Override
	public abstract void pause() throws AgentException;

	/**
	 * This should resume the logic of the agent. It should also be reflected in
	 * the agent state. <BR>
	 * <BR>
	 * If your agent can't be paused therefore can't be resumed, throw
	 * OperationNotSupportedException.
	 * 
	 * @return
	 */
	@Override
	public abstract void resume() throws AgentException;

	/**
	 * Attempt to stop the agent, usually meaning dropping all running flags and
	 * see whether it will stop automatically.
	 * <p>
         * 
	 * <p>
	 * Should not produce any exceptions if possible...
	 */
	@Override
	public abstract void stop();

	/**
	 * Stops the agent (unconditionally), closing whatever connection it may
	 * have, this method must be non-blocking + interrupting all the
	 * communication, logic, whatever threads the agent may have in hope it
	 * helps.
	 */
	@Override
	public abstract void kill();

	/**
	 * Defines amount of time (millis) to give the agent to stop. Note that
	 * during the stopAgent() many things might happen (like serializing the
	 * results). So everybody should honor this method.
	 * <p>
	 * <p>
	 * Override to specify custom stop timeout.
	 * 
	 * @return
	 */
	public long getAgentStopTimeoutMillis() {
		return DEFAULT_AGENT_TIMEOUT_MILLIS;
	}

	/**
	 * Terminates the agent - first it tries to stop the agent with
	 * 'getAgentStopTimeoutMillis()' millis timeout, if it fails it calls
	 * killAgent().
	 */
	public void terminate() {
		terminate(getAgentStopTimeoutMillis());
	}

	/**
	 * Terminates the agent - first it tries to stop the agent with
	 * 'agentStopTimeoutMillis' millis timeout, if it fails it calls
	 * killAgent().
	 * <p>
	 * <p>
	 * <b>BETTER TO USE METHOD terminateAgent() WITHOUT PARAMETERS, TO GIVE
	 * USER-SPECIFIED AMOUNT OF TIME FOR THE AGENT TO BE STOPPED</b>
	 * 
	 * @param agentStopTimeout
	 *            how many millis we should wait before calling killAgent()
	 *            considering the stopAgent() diverges
	 */
	public void terminate(long agentStopTimeoutMillis) {
		terminate(null, null, agentStopTimeoutMillis);
	}

	/**
	 * Terminates the agent - first it tries to stop the agent with
	 * 'agentStopTimeOutMillis' millis timeout, if it fails it calls
	 * killAgent().
	 * <p><p>
	 * After the agent is stopped/killed the agent state and its description is
	 * changed. Should be used in case of the "emergency termination of the agent".
	 * <p>
	 * 
	 * @param resultState
	 *            may be only one from "end states", if null - nothing is changed
	 * @param description
	 * 			  final description for the agent, if null - nothing is changed
	 * @param agentStopTimeout
	 *            how many millis we should wait before calling killAgent()
	 *            considering the stopAgent() diverges
	 */
	protected synchronized void terminate(AgentStateType resultState,
			String description, long agentStopTimeoutMillis) {
		
		getLogger().platform().warning(this + ".terminateAgent(): terminateAgent() called");
		
		Job<Boolean> stopAgent = new Job<Boolean>() {				
			@Override
			protected void job() throws AgentException {
				stop();					
				setResult(true);
			}
		};
		
		try {
			stopAgent.startJob().await(agentStopTimeoutMillis);
		} catch (InterruptedException e) {
			getLogger().platform().severe(this + ".terminateAgent(): interrupted during stop()ing of the agent.");
		}
		if (!stopAgent.isFinishedOk()) {
			stopAgent.interrupt(); // try to terminate the job
			if (stopAgent.isException() &&
				stopAgent.getException() instanceof PogamutException) {
				((PogamutException)stopAgent.getException()).logExceptionOnce(getLogger().platform());
			}
			getLogger().platform().severe(this + ".terminateAgent(): stop()ing agent failed, trying to kill");
			// TODO: possible deadlock - if stopAgent() won't complete it's task and hangs somewhere
			//       in some class mutex that is used also in killAgent() then next call
			//       will result in deadlock...
			kill();
		}
		
		if (resultState != null) {
			if (!resultState.isEndState()) {
				getLogger().platform().warning(
								this + ".terminateAgent(): newState is not one of the end states (is "
									 + resultState + "), state not changed");
			} else {
				if (description == null) setAgentState(resultState, "Agent terminated");
				else setAgentState(resultState, description);
			}
		} else {
			if (description != null) setAgentStateDescription(description);
		}
	}

    private Folder folder = null;
        
    /**
     * Returns introspection info obtained through Java Introspection API.
     * @return introspection info
     */    
    public Folder getFolder() {
        if(folder == null) {
            folder = new ReflectionObjectFolder(getName().replace('.', '_'), this);
        }
        return folder;
    }
        


}
