/*
 * ====================================================================
 * Copyright (c) 2004-2012 TMate Software Ltd.  All rights reserved.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution.  The terms
 * are also available at http://svnkit.com/license.html
 * If newer versions of this license are posted there, you may use a
 * newer version instead, at your option.
 * ====================================================================
 */
package org.tmatesoft.svn.core;

import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import org.tmatesoft.svn.core.internal.util.SVNHashMap;

/**
 * The <b>SVNProperties</b> class represents an object wrapper for
 * <code>String</code> to {@link SVNPropertyValue} mappings where
 * <code>String</code> keys represent property names and values - property
 * values wrapped in {@link SVNPropertyValue} objects.
 *
 * <p>
 * This class is backed by a <code>Map</code> object and brings specific methods
 * useful for working with version controlled properties.
 *
 * <p>
 * Objects of this type are modifiable.
 *
 * @author TMate Software Ltd.
 * @version 1.3
 * @since 1.2
 */
public class SVNProperties implements Cloneable, Serializable {

    private static final long serialVersionUID = 1L;

    private Map<String, SVNPropertyValue> myProperties;

    /**
     * Creates a new <code>SVNProperties</code> object wrapping a given map with
     * properties.
     *
     * <p/>
     * <code>map</code> is not stored by this object, instead its contents are
     * copied into a new <code>Map</code> object (which will be backed by a new
     * <code>SVNProperties</code> object) according to the following rules:
     *
     * <ul>
     * <li/>if the value is of type <code>String</code>, then it's wrapped into
     * {@link SVNPropertyValue} using the
     * {@link SVNPropertyValue#create(String)} method;
     * <li/>if the value is of type <code>byte[]</code>, then it's wrapped into
     * {@link SVNPropertyValue} using the
     * {@link SVNPropertyValue#create(String, byte[])} method;
     * <li/>if the value is of type {@link SVNPropertyValue}, then it's not
     * copied but is put into a new map as is;
     * </ul>
     *
     * @param map
     *            initial map holding properties
     * @return <code>SVNProperties</code> object; if <code>map</code> is <span
     *         class="javakeyword">null</span>, returns an empty
     *         <code>SVNProperties</code> object created as
     *         <code>new SVNProperties()</code>
     * @see #SVNProperties()
     */
    public static SVNProperties wrap(Map map) {
        if (map == null) {
            return new SVNProperties();
        }
        Map propertiesMap = new SVNHashMap();
        for (Iterator names = map.keySet().iterator(); names.hasNext();) {
            Object n = names.next();
            if (!(n instanceof String)) {
                continue;
            }
            Object value = map.get(n);
            SVNPropertyValue v = null;
            if (value instanceof String) {
                v = SVNPropertyValue.create((String) value);
            } else if (value instanceof byte[]) {
                v = SVNPropertyValue.create(n.toString(), (byte[]) value);
            } else if (value instanceof SVNPropertyValue) {
                v = (SVNPropertyValue) value;
            }
            if (v != null) {
                propertiesMap.put(n, v);
            }
        }
        return new SVNProperties(propertiesMap);
    }

    /**
     * Returns an unmodifiable view of the specified <code>properties</code>.
     * Any attempt to modify the returned <code>SVNProperties</code> object
     * result in an <code>UnsupportedOperationException</code>.
     *
     * @param properties
     *            <code>SVNProperties</code> object for which an unmodifiable
     *            view is to be returned.
     * @return an unmodifiable view of the specified properties.
     */
    public static SVNProperties unmodifiableProperties(SVNProperties properties) {
        Map propertiesMap = properties.myProperties;
        propertiesMap = Collections.unmodifiableMap(propertiesMap);
        return new SVNProperties(propertiesMap);
    }

    /**
     * Creates an empty <code>SVNProperties</code> object.
     */
    public SVNProperties() {
        myProperties = new SVNHashMap();
    }

    /**
     * Creates a new <code>SVNProperties</code> object copying the given one.
     *
     * @param properties
     *            an initializer
     */
    public SVNProperties(SVNProperties properties) {
        myProperties = new SVNHashMap(properties.myProperties);
    }

    private SVNProperties(Map properties) {
        myProperties = properties;
    }

    /**
     * Returns SVNProperties as Map of String, SVNPropertyValue pairs.
     *
     * @return copy of SVNProperties as Map object
     */
    public Map<String, SVNPropertyValue> asMap() {
        if (myProperties == null) {
            return Collections.unmodifiableMap(Collections.EMPTY_MAP);
        }
        return Collections.unmodifiableMap(myProperties);
    }

    /**
     * Stores a new mapping <code>propertyName</code> to
     * <code>propertyValue</code> in this object.
     *
     * @param propertyName
     *            property name
     * @param propertyValue
     *            property value object
     */
    public void put(String propertyName, SVNPropertyValue propertyValue) {
        myProperties.put(propertyName, propertyValue);
    }

    /**
     * Stores a new property name-to-value mapping in this object.
     *
     * <p>
     * <code>propertyValue</code> is converted to an {@link SVNPropertyValue}
     * object through a call to {@link SVNPropertyValue#create(String)}.
     *
     * @param propertyName
     *            property name
     * @param propertyValue
     *            property value string
     */
    public void put(String propertyName, String propertyValue) {
        myProperties.put(propertyName, SVNPropertyValue.create(propertyValue));
    }

    /**
     * Stores a new property name-to-value mapping in this object.
     *
     * <p>
     * <code>propertyValue</code> is converted to an {@link SVNPropertyValue}
     * object through a call to {@link SVNPropertyValue#create(String)}.
     *
     * @param propertyName
     *            property name
     * @param propertyValue
     *            property value string
     */
    public void put(String propertyName, char[] propertyValue, String encoding) {
        myProperties.put(propertyName, SVNPropertyValue.create(propertyValue, encoding));
    }

    /**
     * Stores a new property name-to-value mapping in this object.
     *
     * <p>
     * <code>propertyValue</code> is converted to an {@link SVNPropertyValue}
     * object through a call to {@link SVNPropertyValue#create(String, byte[])}.
     *
     * @param propertyName
     *            property name
     * @param propertyValue
     *            property value bytes
     */
    public void put(String propertyName, byte[] propertyValue) {
        myProperties.put(propertyName, SVNPropertyValue.create(propertyName, propertyValue));
    }

    /**
     * Returns a <code>String</code> property value.
     *
     * @param propertyName
     *            property name
     * @return property value string; <span class="javakeyword">null</span> if
     *         there's no such property or if it's not a <code>String</code>
     *         property value
     */
    public String getStringValue(String propertyName) {
        SVNPropertyValue value = (SVNPropertyValue) myProperties.get(propertyName);
        return value == null ? null : value.getString();
    }

    /**
     * Returns a binary property value.
     *
     * @param propertyName
     *            property name
     * @return byte array containing property value bytes; <span
     *         class="javakeyword">null</span> if there's no such property or if
     *         it's not a binary property value
     */
    public byte[] getBinaryValue(String propertyName) {
        SVNPropertyValue value = (SVNPropertyValue) myProperties.get(propertyName);
        return value == null ? null : value.getBytes();
    }

    /**
     * Returns a property value as an {@link SVNPropertyValue}.
     *
     * @param propertyName
     *            property name
     * @return property value object; <span class="javakeyword">null</span> if
     *         there's no such property
     */
    public SVNPropertyValue getSVNPropertyValue(String propertyName) {
        return (SVNPropertyValue) myProperties.get(propertyName);
    }

    /**
     * Removes the specified property from this properties object.
     *
     * @param propertyName
     *            name of the property to remove from this object
     * @return the value of the removed object
     */
    public SVNPropertyValue remove(String propertyName) {
        return (SVNPropertyValue) myProperties.remove(propertyName);
    }

    /**
     * Puts all properties from the specified properties object to this object.
     *
     * @param properties
     *            properties object
     */
    public void putAll(SVNProperties properties) {
        myProperties.putAll(properties.myProperties);
    }

    /**
     * Tells if this properties object holds no properties (empty).
     *
     * @return <span class="javakeyword">true</span> if this object holds no
     *         properties; otherwise <span class="javakeyword">false</span>
     */
    public boolean isEmpty() {
        return myProperties.isEmpty();
    }

    /**
     * Removes all properties from this object.
     *
     */
    public void clear() {
        myProperties.clear();
    }

    /**
     * Removes all mappings which values are <span
     * class="javakeyword">null</span>s from this object.
     */
    public void removeNullValues() {
        for (Iterator iterator = myProperties.keySet().iterator(); iterator.hasNext();) {
            String name = (String) iterator.next();
            if (myProperties.get(name) == null) {
                iterator.remove();
            }
        }
    }

    /**
     * Returns the number of properties held by this object.
     *
     * @return number of properties
     */
    public int size() {
        return myProperties.size();
    }

    /**
     * Tells whether this properties object contains the specified property
     * name.
     *
     * @param propertyName
     *            property name
     * @return <span class="javakeyword">true</span> if this object contains a
     *         mapping with the specified key (<code>propertyName</code>)
     */
    public boolean containsName(String propertyName) {
        return myProperties.containsKey(propertyName);
    }

    /**
     * Returns a set of property names contained by this object.
     *
     * @return property names set
     */
    public Set<String> nameSet() {
        return myProperties.keySet();
    }

    /**
     * Tells whether this properties object contains the specified property
     * value.
     *
     * @param value
     *            property value
     * @return <span class="javakeyword">true</span> if this object contains
     *         <code>value</code>
     *
     */
    public boolean containsValue(SVNPropertyValue value) {
        return myProperties.containsValue(value);
    }

    /**
     * Returns a collection of property values contained in this properties
     * object.
     *
     * @return property values collection
     */
    public Collection values() {
        return myProperties.values();
    }

    /**
     * Returns a subset of properties contained in this properties object which
     * suffice for {@link SVNProperty#isRegularProperty(String)} clause.
     *
     * @return regular properties; if there are no properties which would
     *         suffice the aforementioned clause, an empty
     *         <code>SVNProperties</code> object is returned
     */
    public SVNProperties getRegularProperties() {
        SVNProperties result = new SVNProperties();
        for (Iterator propNamesIter = nameSet().iterator(); propNamesIter.hasNext();) {
            String propName = (String) propNamesIter.next();
            if (SVNProperty.isRegularProperty(propName)) {
                result.put(propName, getSVNPropertyValue(propName));
            }
        }
        return result;
    }

    /**
     * Compares this object against another one returning a difference between
     * them.
     *
     * <p/>
     * Properties which are present in this object but are not in
     * <code>properties</code>, are put to the result as property name to <span
     * class="javakeyword">null</span> mappings. Properties which are present
     * only in <code>properties</code> but not in this object, are added to the
     * result. Also result will include those properties which are present in
     * both objects but have different values; in this case result will include
     * such properties with values from <code>properties</code>.
     *
     * @param properties
     *            another properties object
     * @return properties object holding the properties difference
     */
    public SVNProperties compareTo(SVNProperties properties) {
        SVNProperties result = new SVNProperties();
        if (isEmpty()) {
            result.putAll(properties);
            return result;
        }

        Collection props1 = nameSet();
        Collection props2 = properties.nameSet();

        // missed in props2.
        Collection tmp = new TreeSet(props1);
        tmp.removeAll(props2);
        for (Iterator props = tmp.iterator(); props.hasNext();) {
            String missing = (String) props.next();
            result.put(missing, (byte[]) null);
        }

        // added in props2.
        tmp = new TreeSet(props2);
        tmp.removeAll(props1);

        for (Iterator props = tmp.iterator(); props.hasNext();) {
            String added = (String) props.next();
            result.put(added, properties.getSVNPropertyValue(added));
        }

        // changed in props2
        tmp = new TreeSet(props2);
        tmp.retainAll(props1);

        for (Iterator props = tmp.iterator(); props.hasNext();) {
            String changed = (String) props.next();
            SVNPropertyValue value1 = getSVNPropertyValue(changed);
            SVNPropertyValue value2 = properties.getSVNPropertyValue(changed);
            if (!SVNPropertyValue.areEqual(value1, value2)) {
                result.put(changed, value2);
            }
        }
        return result;
    }

    /**
     * Returns a hash code of this object.
     *
     * <p/>
     * A hash code is evaluated as follows: <code>31 + </code>
     * {@link java.util.Map#hashCode() hash code} of the underlying
     * <code>Map</code> holding the property key to property value mappings.
     *
     * @return hash code of this object
     */
    public int hashCode() {
        return 31 + ((myProperties == null) ? 0 : myProperties.hashCode());
    }

    /**
     * Tells whether this object and <code>obj</code> are equal.
     *
     * @param obj
     *            object to compare with
     * @return <span class="javakeyword">true</span> if <code>obj</code> is
     *         either this very object, or is an instance of
     *         <code>SVNProperties</code> with the same contents of properties
     *
     */
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        SVNProperties other = (SVNProperties) obj;
        if (myProperties == null) {
            if (other.myProperties != null) {
                return false;
            }
        } else if (!myProperties.equals(other.myProperties)) {
            return false;
        }
        return true;
    }

    /**
     * Creates and returns a copy of this object.
     *
     * @return a clone of this instance
     *
     */
    public Object clone() throws CloneNotSupportedException {
        try {
            super.clone();
        } catch (CloneNotSupportedException cnse) {
            return null;
        }

        SVNProperties result = new SVNProperties();
        result.putAll(this);
        return result;
    }

    public String toString() {
        if (myProperties != null) {
            return myProperties.toString();
        }
        return "";
    }

    public void dispose() {
        if (myProperties != null) {
            for (SVNPropertyValue value : myProperties.values()) {
                value.clear();
            }
        }
    }

}
