/**
 *
 * Copyright 2003-2007 Jive Software.
 *
 * Licensed 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.jivesoftware.smackx.jiveproperties.packet;

import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.util.XmlStringBuilder;
import org.jivesoftware.smack.util.stringencoder.Base64;

/**
 * Properties provide an easy mechanism for clients to share data. Each property has a
 * String name, and a value that is a Java primitive (int, long, float, double, boolean)
 * or any Serializable object (a Java object is Serializable when it implements the
 * Serializable interface).
 *
 */
public class JivePropertiesExtension implements ExtensionElement {
    /**
     * Namespace used to store stanza(/packet) properties.
     */
    public static final String NAMESPACE = "http://www.jivesoftware.com/xmlns/xmpp/properties";

    public static final String ELEMENT = "properties";

    private static final Logger LOGGER = Logger.getLogger(JivePropertiesExtension.class.getName());

    private final Map<String, Object> properties;

    public JivePropertiesExtension() {
        properties = new HashMap<String, Object>();
    }

    public JivePropertiesExtension(Map<String, Object> properties) {
        this.properties = properties;
    }

    /**
     * Returns the stanza(/packet) property with the specified name or <tt>null</tt> if the
     * property doesn't exist. Property values that were originally primitives will
     * be returned as their object equivalent. For example, an int property will be
     * returned as an Integer, a double as a Double, etc.
     *
     * @param name the name of the property.
     * @return the property, or <tt>null</tt> if the property doesn't exist.
     */
    public synchronized Object getProperty(String name) {
        if (properties == null) {
            return null;
        }
        return properties.get(name);
    }

    /**
     * Sets a property with an Object as the value. The value must be Serializable
     * or an IllegalArgumentException will be thrown.
     *
     * @param name the name of the property.
     * @param value the value of the property.
     */
    public synchronized void setProperty(String name, Object value) {
        if (!(value instanceof Serializable)) {
            throw new IllegalArgumentException("Value must be serialiazble");
        }
        properties.put(name, value);
    }

    /**
     * Deletes a property.
     *
     * @param name the name of the property to delete.
     */
    public synchronized void deleteProperty(String name) {
        if (properties == null) {
            return;
        }
        properties.remove(name);
    }

    /**
     * Returns an unmodifiable collection of all the property names that are set.
     *
     * @return all property names.
     */
    public synchronized Collection<String> getPropertyNames() {
        if (properties == null) {
            return Collections.emptySet();
        }
        return Collections.unmodifiableSet(new HashSet<String>(properties.keySet()));
    }

    /**
     * Returns an unmodifiable map of all properties.
     *
     * @return all properties.
     */
    public synchronized Map<String, Object> getProperties() {
        if (properties == null) {
            return Collections.emptyMap();
        }
        return Collections.unmodifiableMap(new HashMap<String, Object>(properties));
    }

    @Override
    public String getElementName() {
        return ELEMENT;
    }

    @Override
    public String getNamespace() {
        return NAMESPACE;
    }

    @Override
    public CharSequence toXML() {
        XmlStringBuilder xml = new XmlStringBuilder(this);
        xml.rightAngleBracket();
        // Loop through all properties and write them out.
        for (String name : getPropertyNames()) {
            Object value = getProperty(name);
            xml.openElement("property");
            xml.element("name", name);
            xml.halfOpenElement("value");

            String type;
            String valueStr;
            if (value instanceof Integer) {
                type = "integer";
                valueStr = Integer.toString((Integer) value);
            }
            else if (value instanceof Long) {
                type = "long";
                valueStr = Long.toString((Long) value);
            }
            else if (value instanceof Float) {
                type = "float";
                valueStr = Float.toString((Float) value);
            }
            else if (value instanceof Double) {
                type = "double";
                valueStr = Double.toString((Double) value);
            }
            else if (value instanceof Boolean) {
                type = "boolean";
                valueStr = Boolean.toString((Boolean) value);
            }
            else if (value instanceof String) {
                type = "string";
                valueStr = (String) value;
            }
            // Otherwise, it's a generic Serializable object. Serialized objects are in
            // a binary format, which won't work well inside of XML. Therefore, we base-64
            // encode the binary data before adding it.
            else {
                ByteArrayOutputStream byteStream = null;
                ObjectOutputStream out = null;
                try {
                    byteStream = new ByteArrayOutputStream();
                    out = new ObjectOutputStream(byteStream);
                    out.writeObject(value);
                    type = "java-object";
                    valueStr = Base64.encodeToString(byteStream.toByteArray());
                }
                catch (Exception e) {
                    LOGGER.log(Level.SEVERE, "Error encoding java object", e);
                    type = "java-object";
                    valueStr = "Serializing error: " + e.getMessage();
                }
                finally {
                    if (out != null) {
                        try {
                            out.close();
                        }
                        catch (Exception e) {
                            // Ignore.
                        }
                    }
                    if (byteStream != null) {
                        try {
                            byteStream.close();
                        }
                        catch (Exception e) {
                            // Ignore.
                        }
                    }
                }
            }
            xml.attribute("type", type);
            xml.rightAngleBracket();
            xml.escape(valueStr);
            xml.closeElement("value");
            xml.closeElement("property");
        }
        xml.closeElement(this);

        return xml;
    }

    /**
     * Return a Jive properties extensions of the given message.
     *
     * @param message the message to return the extension from.
     * @return a Jive properties extension or null.
     * @since 4.2
     */
    public static JivePropertiesExtension from(Message message) {
        return message.getExtension(ELEMENT, NAMESPACE);
    }
}
