/*
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 *
 */
package org.apache.qpid.qmf2.console;

// Misc Imports
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

// QMF2 Imports
import org.apache.qpid.qmf2.common.ObjectId;
import org.apache.qpid.qmf2.common.QmfData;
import org.apache.qpid.qmf2.common.QmfException;
import org.apache.qpid.qmf2.common.SchemaClass;
import org.apache.qpid.qmf2.common.SchemaClassId;

/**
 * Local representation (proxy) of a remote Agent.
 * <p>
 * This class holds some state that relates to the Agent and in addition some methods may be called on the agent.
 * destroy(), invokeMethod() and refresh() are actually called by a proxy class AgentProxy. AgentProxy is actually
 * an interface that is implemented by Console (as that's where all the JMS stuff is), we use this approach to 
 * avoid introducing a circular dependency between Agent and Console.
 * <p>
 * The Console application maintains a list of all known remote Agents.
 * Each Agent is represented by an instance of the Agent class:
 * <p>
 * The following diagram illustrates the interactions between the Console, AgentProxy and the client side Agent
 * representation.
 * <p>
 * <img alt="" src="doc-files/Subscriptions.png">
 * @author Fraser Adams
 */
public final class Agent extends QmfData
{
    private AgentProxy                      _proxy;
    private List<String>                    _packages = new ArrayList<String>();
    private Map<SchemaClassId, SchemaClass> _schemaCache = new ConcurrentHashMap<SchemaClassId, SchemaClass>();
    private long                            _epoch;
    private long                            _heartbeatInterval;
    private long                            _timestamp;
    private boolean                         _eventsEnabled = true;
    private boolean                         _isActive = true;

    /**
     * The main constructor, taking a java.util.Map as a parameter. In essence it "deserialises" its state from the Map.
     *
     * @param m the map used to construct the SchemaClass
     * @param p the AgentProxy instance that implements some of the concrete behaviour of the local Agent representation.
     */
    public Agent(final Map m, final AgentProxy p)
    {
        super(m);
        // Populate attributes translating any old style keys if necessary.
        _epoch = hasValue("_epoch") ? getLongValue("_epoch") : getLongValue("epoch");
        _heartbeatInterval = hasValue("_heartbeat_interval") ? getLongValue("_heartbeat_interval") : 
                                                               getLongValue("heartbeat_interval");
        _timestamp = hasValue("_timestamp") ? getLongValue("_timestamp") : getLongValue("timestamp");
        _proxy = p;
    }

    /**
     * Sets the state of the Agent, used as an assignment operator.
     * 
     * @param m the Map used to initialise the Agent.
     */
    @SuppressWarnings("unchecked")
    public void initialise(final Map m)
    {        
        Map<String, Object> values = (Map<String, Object>)m.get("_values");
        _values = (values == null) ? m : values;

        // Populate attributes translating any old style keys if necessary.
        _epoch = hasValue("_epoch") ? getLongValue("_epoch") : getLongValue("epoch");
        _heartbeatInterval = hasValue("_heartbeat_interval") ? getLongValue("_heartbeat_interval") : 
                                                               getLongValue("heartbeat_interval");
        _timestamp = hasValue("_timestamp") ? getLongValue("_timestamp") : getLongValue("timestamp");
    }

    /**
     * Return whether or not events are enabled for this Agent.
     * @return a boolean indication of whether or not events are enabled for this Agent.
     */
    public boolean eventsEnabled()
    {
        return _eventsEnabled;
    }

    /**
     * Deactivated this Agent. Called by the Console when the Agent times out.
     */
    public void deactivate()
    {
        _isActive = false;
    }

    /**
     * Return the Agent instance name.
     * @return the Agent instance name.
     */
    public String getInstance()
    {
        return getStringValue("_instance");
    }

    /**
     * Return the identifying name string of the Agent. 
     * @return the identifying name string of the Agent. This name is used to send AMQP messages directly to this agent.
     */
    public String getName()
    {
        return getStringValue("_name");
    }

    /**
     * Return the product name string of the Agent.
     * @return the product name string of the Agent.
     */
    public String getProduct()
    {
        return getStringValue("_product");
    }

    /**
     * Return the Agent vendor name.
     * @return the Agent vendor name.
     */
    public String getVendor()
    {
        return getStringValue("_vendor");
    }

    /**
     * Return the Epoch stamp.
     * @return the Epoch stamp, used to determine if an Agent has been restarted.
     */
    public long getEpoch()
    {
        return _epoch;
    }

    /**
     * Set the Epoch stamp.
     * @param epoch the new Epoch stamp, used to indicate that an Agent has been restarted.
     */
    public void setEpoch(long epoch)
    {
        _epoch = epoch;
    }

    /**
     * Return the time that the Agent waits between sending hearbeat messages.
     * @return the time that the Agent waits between sending hearbeat messages.
     */
    public long getHeartbeatInterval()
    {
        return _heartbeatInterval;
    }

    /**
     * Return the timestamp of the Agent's last update.
     * @return the timestamp of the Agent's last update.
     */
    public long getTimestamp()
    {
        return _timestamp;
    }

    /**
     * Return true if the agent is alive.
     * @return true if the agent is alive (heartbeats have not timed out).
     */
    public boolean isActive()
    {
        return _isActive;
    }

    /**
     * Request that the Agent updates the value of this object's contents.
     *
     * @param objectId the ObjectId being queried for..
     * @param replyHandle the correlation handle used to tie asynchronous method requests with responses.
     * @param timeout the maximum time to wait for a response, overrides default replyTimeout.
     * @return the refreshed object.
     */    
    public QmfConsoleData refresh(final ObjectId objectId, final String replyHandle, final int timeout) throws QmfException
    {
        if (isActive())
        {
            return _proxy.refresh(this, objectId, replyHandle, timeout);
        }
        else
        {
            throw new QmfException("Agent.refresh() called from deactivated Agent");
        }
    }

    /**
     * Helper method to create a Map containing a QMF method request.
     *
     * @param objectId the objectId of the remote object.
     * @param name the remote method name.
     * @param inArgs the formal parameters of the remote method name.
     * @return a Map containing a QMF method request.
     */
    private Map<String, Object> createRequest(final ObjectId objectId, final String name, final QmfData inArgs)
    {
        // Default sizes for HashMap should be fine for request
        Map<String, Object> request = new HashMap<String, Object>();
        if (objectId != null)
        {
            request.put("_object_id", objectId.mapEncode());
        }
        request.put("_method_name", name);
        if (inArgs != null)
        {
            request.put("_arguments", inArgs.mapEncode());
            if (inArgs.getSubtypes() != null)
            {
                request.put("_subtypes", inArgs.getSubtypes());
            }
        }
        return request;
    }

    /**
     * Sends a method request to the Agent. Delegates to the AgentProxy to actually send the method as it's the
     * AgentProxy that knows about connections, sessions and messages.
     *
     * @param objectId the objectId of the remote object.
     * @param name the remote method name.
     * @param inArgs the formal parameters of the remote method name.
     * @param timeout the maximum time to wait for a response, overrides default replyTimeout.
     * @return the MethodResult.
     */
    protected MethodResult invokeMethod(final ObjectId objectId, final String name,
                                        final QmfData inArgs, final int timeout) throws QmfException
    {
        if (isActive())
        {
            return _proxy.invokeMethod(this, createRequest(objectId, name, inArgs), null, timeout);
        }
        else
        {
            throw new QmfException("Agent.invokeMethod() called from deactivated Agent");
        }
    }

    /**
     * Sends an asynchronous method request to the Agent. Delegates to the AgentProxy to actually send the method as
     * it's the AgentProxy that knows about connections, sessions and messages.
     *
     * @param objectId the objectId of the remote object.
     * @param name the remote method name.
     * @param inArgs the formal parameters of the remote method name.
     * @param replyHandle the correlation handle used to tie asynchronous method requests with responses.
     */
    protected void invokeMethod(final ObjectId objectId, final String name,
                                final QmfData inArgs, final String replyHandle) throws QmfException
    {
        if (isActive())
        {
            _proxy.invokeMethod(this, createRequest(objectId, name, inArgs), replyHandle, -1);
        }
        else
        {
            throw new QmfException("Agent.invokeMethod() called from deactivated Agent");
        }
    }

    /**
     * Sends a method request to the Agent. Delegates to the AgentProxy to actually send the method as it's the
     * AgentProxy that knows about connections, sessions and messages.
     *
     * @param name the remote method name.
     * @param inArgs the formal parameters of the remote method name.
     * @return the MethodResult.
     */
    public MethodResult invokeMethod(final String name, final QmfData inArgs) throws QmfException
    {
        return invokeMethod(null, name, inArgs, -1);
    }

    /**
     * Sends a method request to the Agent. Delegates to the AgentProxy to actually send the method as it's the
     * AgentProxy that knows about connections, sessions and messages.
     *
     * @param name the remote method name.
     * @param inArgs the formal parameters of the remote method name.
     * @param timeout the maximum time to wait for a response, overrides default replyTimeout.
     * @return the MethodResult.
     */
    public MethodResult invokeMethod(final String name, final QmfData inArgs, final int timeout) throws QmfException
    {
        return invokeMethod(null, name, inArgs, timeout);
    }

    /**
     * Sends a method request to the Agent. Delegates to the AgentProxy to actually send the method as it's the
     * AgentProxy that knows about connections, sessions and messages.
     *
     * @param name the remote method name.
     * @param inArgs the formal parameters of the remote method name.
     * @param replyHandle the correlation handle used to tie asynchronous method requests with responses.
     */
    public void invokeMethod(final String name, final QmfData inArgs, final String replyHandle) throws QmfException
    {
        invokeMethod(null, name, inArgs, replyHandle);
    }

    /**
     * Remove a Subscription. Delegates to the AgentProxy to actually remove the Subscription as it's the AgentProxy
     * that really knows about subscriptions.
     *
     * @param subscription the SubscriptionManager that we wish to remove.
     */
    public void removeSubscription(final SubscriptionManager subscription)
    {
        _proxy.removeSubscription(subscription);
    }

    /**
     * Allows reception of events from this agent.
     */
    public void enableEvents()
    {
        _eventsEnabled = true;
    }

    /**
     * Prevents reception of events from this agent.
     */
    public void disableEvents()
    {
        _eventsEnabled = false;
    }

    /**
     * Releases this Agent instance. Once called, the Console application should not reference this instance again.
     */
    public void destroy()
    {
        _timestamp = 0;
        _proxy.destroy(this);
    }

    /**
     * Clears the internally cached schema. Generally done when we wich to refresh the schema information from the
     * remote Agent.
     */
    public void clearSchemaCache()
    {
        _schemaCache.clear();
        _packages.clear();
    }

    /**
     * Stores the schema and package information obtained by querying the remote Agent.
     *
     * @param classes the list of SchemaClassIds obtained by querying the remote Agent.
     */
    public void setClasses(final List<SchemaClassId> classes)
    {
        if (classes == null)
        {
            clearSchemaCache();
            return;
        }

        for (SchemaClassId classId : classes)
        {
            _schemaCache.put(classId, SchemaClass.EMPTY_SCHEMA);
            if (!_packages.contains(classId.getPackageName()))
            {
                _packages.add(classId.getPackageName());
            }
        }
    }

    /**
     * Return the list of SchemaClassIds associated with this Agent.
     * @return the list of SchemaClassIds associated with this Agent.
     */
    public List<SchemaClassId> getClasses()
    {
        if (_schemaCache.size() == 0)
        {
            return Collections.emptyList();
        }
        return new ArrayList<SchemaClassId>(_schemaCache.keySet());
    }

    /**
     * Return the list of packages associated with this Agent.
     * @return the list of packages associated with this Agent.
     */
    public List<String> getPackages()
    {
        return _packages;
    }

    /**
     * Return the SchemaClass associated with this Agent.
     * @return the list of SchemaClass associated with this Agent.
     * <p>
     * I <i>believe</i> that there should only be one entry in the list returned when looking up a specific chema by classId.
     */
    public List<SchemaClass> getSchema(final SchemaClassId classId)
    {
        SchemaClass schema = _schemaCache.get(classId);
        if (schema == SchemaClass.EMPTY_SCHEMA)
        {
            return Collections.emptyList();
        }
        
        List<SchemaClass> results = new ArrayList<SchemaClass>();
        results.add(schema);
        return results;
    }

    /**
     * Set a schema keyed by SchemaClassId.
     *
     * @param classId the SchemaClassId indexing the particular schema.
     * @param schemaList the schema being indexed.
     * <p>
     * I <i>believe</i> that there should only be one entry in the list returned when looking up a specific chema by classId.
     */
    public void setSchema(final SchemaClassId classId, final List<SchemaClass> schemaList)
    {
        if (schemaList == null || schemaList.size() == 0)
        {
            _schemaCache.put(classId, SchemaClass.EMPTY_SCHEMA);
        }
        else
        {
            // I believe that there should only be one entry in the list returned when looking up
            // a specific chema by classId
            _schemaCache.put(classId, schemaList.get(0));
        }
    }

    /**
     * Helper/debug method to list the QMF Object properties and their type.
     */
    @Override
    public void listValues()
    {
        super.listValues();
        System.out.println("Agent:");
        System.out.println("instance: " + getInstance());
        System.out.println("name: " + getName());
        System.out.println("product: " + getProduct());
        System.out.println("vendor: " + getVendor());
        System.out.println("epoch: " + getEpoch());
        System.out.println("heartbeatInterval: " + getHeartbeatInterval());
        System.out.println("timestamp: " + new Date(getTimestamp()/1000000l));
    }
}

