/**
 * The contents of this file are subject to the license and copyright
 * detailed in the LICENSE and NOTICE files at the root of the source
 * tree and available online at
 *
 * http://www.dspace.org/license/
 */
package org.dspace.services.sessions.model;

import java.io.Serializable;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionContext;

import org.dspace.services.model.Session;

/**
 * Represents a users session (login session) in the system.  Can hold 
 * some additional attributes as needed, but the underlying
 * implementation may limit the number and size of attributes to ensure
 * that session replication is not impacted negatively.
 * 
 * @author Aaron Zeckoski (azeckoski @ gmail.com)
 */
@SuppressWarnings("deprecation")
public final class SessionImpl implements Session {

    // keys for things stored in the session
    public static final String SESSION_ID = "dspaceSessionId";
    public static final String USER_ID = "userId";
    public static final String USER_EID = "userEid";
    public static final String SERVER_ID = "serverId";
    public static final String HOST_IP = "originatingHostIP";
    public static final String HOST_NAME = "originatingHostName";

    /**
     * This is the only thing that is actually replicated across the cluster.
     */
    private transient HttpSession httpSession;

    /**
     * Make a session that is associated with the current HTTP request.
     *
     * @param request
     */
    public SessionImpl(HttpServletRequest request) {
        if (request == null) {
            throw new IllegalArgumentException("Cannot create a session without an http request");
        }
        this.httpSession = request.getSession(); // establish the session
        setKeyAttribute(HOST_IP, request.getRemoteAddr());
        setKeyAttribute(HOST_NAME, request.getRemoteHost());
    }

    /**
     * Make a session which is not associated with the current HTTP request.
     */
    public SessionImpl() {
        // creates a new internal http session that is not cached anywhere
        this.httpSession = new InternalHttpSession();
        try {
            InetAddress i4 = Inet4Address.getLocalHost();
            setKeyAttribute(HOST_IP, i4.getHostAddress()); // IP address
            setKeyAttribute(HOST_NAME, i4.getHostName());
        } catch (UnknownHostException e) {
            // could not get address
            setKeyAttribute(HOST_IP, "10.0.0.1"); // set a fake one I guess
        }

    }

    /**
     * Set the sessionId.  Normally this should not probably happen much.
     * @param sessionId
     */
    public void setSessionId(String sessionId) {
        if (! isAttributeSet(SESSION_ID)) {
            if (isBlank(sessionId)) {
                // just use the http session id
                setKeyAttribute(SESSION_ID, this.httpSession.getId());
            } else {
                setKeyAttribute(SESSION_ID, sessionId);
            }
        }
    }

    /**
     * Set the userId and userEid.  This should only happen when
     * re-binding the session or clearing the associated user.
     * If userId is null then user is cleared.
     *
     * @param userId
     * @param userEid
     */
    public void setUserId(String userId, String userEid) {
        if (isBlank(userId)) {
            removeKeyAttribute(USER_ID);
            removeKeyAttribute(USER_EID);
        } else {
            setKeyAttribute(USER_ID, userId);
            setKeyAttribute(USER_EID, userEid);
        }
    }

    /**
     * Set the DSpace serverId which originated this session.
     *
     * @param serverId the serverId
     */
    public void setServerId(String serverId) {
        setKeyAttribute(SERVER_ID, serverId);
    }

    /**
     * Are all required values set?  Notice that the sense of the test 
     * is the complement of what the name of this method implies.
     *
     * @return true if this session already has all the required values
     * needed to complete it.  This means the serverId and other values
     * in the session are already set.
     */
    public boolean isIncomplete() {
        boolean complete = false;
        if (isAttributeSet(SERVER_ID) 
                && isAttributeSet(SESSION_ID)
                && isAttributeSet(HOST_IP)) {
            complete = true;
        }
        return ! complete;
    }

    /**
     * @param key the attribute key
     * @return true if the attribute is set
     */
    public boolean isAttributeSet(String key) {
        return getKeyAtribute(key) != null;
    }

    /**
     * @return true if this session is invalidated
     */
    public boolean isInvalidated() {
        boolean invalid = true;
        if (this.httpSession != null) {
            try {
                this.httpSession.getCreationTime();
                invalid = false;
            } catch (IllegalStateException e) {
                invalid = true;
            }
        } else {
            // no httpsession
            invalid = false;
        }
        return invalid;
    }

    /**
     * Handles the general setting of things in the session.
     * Use this to build other set methods.
     * Handles checking the session is still valid, and
     * the checking for null values in the value and key.
     *
     * @param key the key to use
     * @param value the value to set
     * @return true if the value was set, false if cleared or failure
     * @throws IllegalArgumentException if the key is null
     */
    protected boolean setKeyAttribute(String key, String value) {
        if (key == null) {
            throw new IllegalArgumentException("session attribute key cannot be null");
        }
        boolean wasSet = false;
        if (! isInvalidated()) {
            if (isBlank(value)) {
                this.httpSession.removeAttribute(key);
            } else {
                this.httpSession.setAttribute(key, value);
                wasSet = true;
            }
        }
        return wasSet;
    }

    /**
     * Handles the general getting of things from the session.
     * Use this to build other set methods.
     * Checks that the session is still valid.
     *
     * @param key the key to use
     * @return the value OR null if not found
     * @throws IllegalArgumentException if the key is null
     */
    protected String getKeyAtribute(String key) {
        if (key == null) {
            throw new IllegalArgumentException("session attribute key cannot be null");
        }
        String value = null;
        if (! isInvalidated()) {
            value = (String) this.httpSession.getAttribute(key);
        }
        return value;
    }

    /**
     * Handles removal of attributes and related checks.
     *
     * @param key the key to use
     * @throws IllegalArgumentException if the key is null
     */
    protected void removeKeyAttribute(String key) {
        if (key == null) {
            throw new IllegalArgumentException("session attribute key cannot be null");
        }
        if (! isInvalidated()) {
            this.httpSession.removeAttribute(key);
        }
    }

    @Override
    public boolean equals(Object obj) {
        // sessions are equal if the ids are the same, allows comparison across reloaded items
        if (null == obj) {
            return false;
        }
        if (!(obj instanceof SessionImpl)) {
            return false;
        } else {
            SessionImpl castObj = (SessionImpl) obj;
            boolean eq;
            try {
                eq = this.getId().equals(castObj.getId());
            } catch (IllegalStateException e) {
                eq = false;
            }
            return eq;
        }
    }

    @Override
    public int hashCode() {
        String hashStr = this.getClass().getName() + ":" + this.httpSession.toString();
        return hashStr.hashCode();
    }

    @Override
    public String toString() {
        String str;
        if (isInvalidated()) {
            str = "invalidated:" + this.httpSession.toString() + ":" + super.toString();
        } else {
            str = "active:"+getId()+":user="+getUserId()+"("+getUserEID()+"):sid="+getSessionId()+":server="+getServerId()+":created="+getCreationTime()+":accessed="+getLastAccessedTime()+":maxInactiveSecs="+getMaxInactiveInterval()+":hostIP="+getOriginatingHostIP()+":hostName="+getOriginatingHostName()+":"+super.toString();
        }
        return "Session:" + str;
    }


    // INTERFACE methods

    /* (non-Javadoc)
     * @see org.dspace.services.model.Session#getAttribute(java.lang.String)
     */
    public String getAttribute(String key) {
        return getKeyAtribute(key);
    }

    /* (non-Javadoc)
     * @see org.dspace.services.model.Session#setAttribute(java.lang.String, java.lang.String)
     */
    public void setAttribute(String key, String value) {
        setKeyAttribute(key, value);
    }

    /* (non-Javadoc)
     * @see javax.servlet.http.HttpSession#removeAttribute(java.lang.String)
     */
    public void removeAttribute(String name) {
        removeKeyAttribute(name);
    }

    /* (non-Javadoc)
     * @see javax.servlet.http.HttpSession#setAttribute(java.lang.String, java.lang.Object)
     */
    public void setAttribute(String name, Object value) {
        if (value != null && !(value instanceof String)) {
            throw new UnsupportedOperationException("Invalid session attribute ("+name+","+value+"), Only strings can be stored in the session");
        }
        setKeyAttribute(name, (String) value);
    }

    /**
     * @return a copy of the attributes in this session.
     * Modifying it has no effect on the session attributes.
     */
    @SuppressWarnings("unchecked")
    public Map<String, String> getAttributes() {
        Map<String, String> map = new HashMap<String, String>();
        if (! isInvalidated()) {
            Enumeration<String> names = this.httpSession.getAttributeNames();
            while (names.hasMoreElements()) {
                String key = names.nextElement();
                String value = (String) this.httpSession.getAttribute(key);
                map.put(key, value);
            }
        }
        return map;
    }

    /* (non-Javadoc)
     * @see javax.servlet.http.HttpSession#getAttributeNames()
     */
    @SuppressWarnings("unchecked")
    public Enumeration getAttributeNames() {
        return this.httpSession.getAttributeNames();
    }

    /* (non-Javadoc)
     * @see org.dspace.services.model.Session#clear()
     */
    @SuppressWarnings("unchecked")
    public void clear() {
        if (! isInvalidated()) {
            Enumeration<String> names = this.httpSession.getAttributeNames();
            while (names.hasMoreElements()) {
                String name = names.nextElement();
                this.httpSession.removeAttribute(name);
            }
        }
    }

    public String getOriginatingHostIP() {
        return getKeyAtribute(HOST_IP);
    }

    public String getOriginatingHostName() {
        return getKeyAtribute(HOST_NAME);
    }

    public String getServerId() {
        return getKeyAtribute(SERVER_ID);
    }

    public String getSessionId() {
        return getKeyAtribute(SESSION_ID);
    }

    public String getUserEID() {
        return getKeyAtribute(USER_EID);
    }

    public String getUserId() {
        return getKeyAtribute(USER_ID);
    }

    public boolean isActive() {
        return ! isInvalidated();
    }

    // HTTP SESSION passthroughs

    public long getCreationTime() {
        return this.httpSession.getCreationTime();
    }

    public String getId() {
        String id = null;
        if (isAttributeSet(SESSION_ID)) {
            id = getKeyAtribute(SESSION_ID);
        } else {
            id = this.httpSession.getId();
        }
        return id;
    }

    public long getLastAccessedTime() {
        return this.httpSession.getLastAccessedTime();
    }

    public int getMaxInactiveInterval() {
        return this.httpSession.getMaxInactiveInterval();
    }

    public void setMaxInactiveInterval(int interval) {
        this.httpSession.setMaxInactiveInterval(interval);
    }

    public ServletContext getServletContext() {
        if (this.httpSession != null) {
            return this.httpSession.getServletContext();
        }
        throw new UnsupportedOperationException("No http session available for this operation");
    }

    public void invalidate() {
        if (! isInvalidated()) {
            this.httpSession.invalidate();
        }
        // TODO nothing otherwise?
    }

    public boolean isNew() {
        if (! isInvalidated()) {
            return this.httpSession.isNew();
        }
        return false;
    }

    // DEPRECATED

    /* (non-Javadoc)
     * @see javax.servlet.http.HttpSession#getValue(java.lang.String)
     */
    public Object getValue(String name) {
        return getKeyAtribute(name);
    }

    /* (non-Javadoc)
     * @see javax.servlet.http.HttpSession#getValueNames()
     */
    public String[] getValueNames() {
        Set<String> keys = getAttributes().keySet();
        return keys.toArray(new String[keys.size()]);
    }

    /* (non-Javadoc)
     * @see javax.servlet.http.HttpSession#removeValue(java.lang.String)
     */
    public void removeValue(String name) {
        removeAttribute(name);
    }

    /* (non-Javadoc)
     * @see javax.servlet.http.HttpSession#putValue(java.lang.String, java.lang.Object)
     */
    public void putValue(String name, Object value) {
        setAttribute(name, value);
    }

    public HttpSessionContext getSessionContext() {
        if (this.httpSession != null) {
            return this.httpSession.getSessionContext();
        }
        throw new UnsupportedOperationException("No http session available for this operation");
    }

    // END DEPRECATED



    /**
     * Check if something is blank (null or "").
     *
     * @param string
     * @return true if is blank
     */
    public static boolean isBlank(String string) {
        return (string == null) || ("".equals(string));
    }

    /**
     * Compares sessions by the last time they were accessed, with more 
     * recent first.
     */
    public static final class SessionLastAccessedComparator implements Comparator<Session>, Serializable {
        public static final long serialVersionUID = 1l;
        public int compare(Session o1, Session o2) {
            try {
                Long lat1 = Long.valueOf(o1.getLastAccessedTime());
                Long lat2 = Long.valueOf(o2.getLastAccessedTime());
                return lat2.compareTo(lat1); // reverse
            } catch (Exception e) {
                return 0;
            }
        }
    }

}
