/*   Copyright 2004 The Apache Software Foundation
 *
 *   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.apache.xmlbeans;

import javax.xml.namespace.QName;

import java.util.Set;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Collections;
import java.util.Arrays;

/**
 * This interface represents a lattice of finite and infinite sets of QNames.
 * The lattice the minimal one that is closed under union, intersection, and
 * inverse, and contains individual QNames as well as entire namespaces.
 * Here is a summary of the two kinds of QNameSets:
 * <ul>
 * <li>A QNameSet can cover a finite set of namespaces, additionally including a finite
 *     set of QNames outside those namespaces, and with the exception of
 *     a finite set of QNames excluded from those namespaes:
 *   <ul>
 *   <li>excludedQNamesInIncludedURIs == the set of excluded QNames from coveredURIs namespaces
 *   <li>excludedURIs == null
 *   <li>includedURIs == the set of covered namespace URIs
 *   <li>includedQNamesInExcludedURIs == set of additional QNames outside coveredURIs namespaces
 *   </ul>
 * </li>
 * <li>A QNameSet can cover all namespaces except for a finite number of excluded ones,
 *     additionally including a finite set of QNames within the excluded namespaces,
 *     and with the exception of a finite set of QNames outside the excluded namespaces:
 *   <ul>
 *   <li>excludedQNamesInIncludedURIs == the set of excluded QNames outside uncoveredURIs namespaces
 *   <li>excludedURIs == the set of uncovered namespace URIs
 *   <li>includedURIs == null
 *   <li>includedQNamesInExcludedURIs == set of additional QNames from uncoveredURIs namespaces
 *   </ul>
 * </li>
 * </ul>
 * <p>
 * Notice that a finite set of QNames is a degenerate case of the first
 * category outlined above:
 * <ul>
 * <li>A QnameSet can contain a finite number of QNames:
 *   <ul>
 *   <li>excludedQNamesInIncludedURIs == null
 *   <li>excludedURIs == null
 *   <li>includedURIs == empty set
 *   <li>includedQNamesInExcludedURIs == set of included QNames
 *   </ul>
 * </li>
 * </ul>
 */
public final class QNameSet implements QNameSetSpecification, java.io.Serializable
{
    private static final long serialVersionUID = 1L;
    
    private final boolean _inverted;
    private final Set _includedURIs;
    private final Set _excludedQNames;
    private final Set _includedQNames;

    /**
     * The empty QNameSet.
     */
    public static final QNameSet EMPTY = new QNameSet(null, Collections.EMPTY_SET, Collections.EMPTY_SET, Collections.EMPTY_SET);

    /**
     * The QNameSet containing all QNames.
     */
    public static final QNameSet ALL = new QNameSet(Collections.EMPTY_SET, null, Collections.EMPTY_SET, Collections.EMPTY_SET);

    /**
     * The QNameSet containing all QNames in the local (no-)namespace.
     */
    public static final QNameSet LOCAL = new QNameSet(null, Collections.singleton(""), Collections.EMPTY_SET, Collections.EMPTY_SET);

    /**
     * The QNameSet containing all QNames except for those in the local (no-)namespace.
     */
    public static final QNameSet NONLOCAL = new QNameSet(Collections.singleton(""), null, Collections.EMPTY_SET, Collections.EMPTY_SET);

    /**
     * Private function to minimize object creation when copying sets.
     */
    private static Set minSetCopy(Set original)
    {
        if (original == null)
            return null;
        if (original.isEmpty())
            return Collections.EMPTY_SET;
        if (original.size() == 1)
            return Collections.singleton(original.iterator().next());
        return new HashSet(original);
    }

    /**
     * Returns a QNameSet based on the given sets of excluded URIs,
     * included URIs, excluded QNames in included namespaces, and included
     * QNames in excluded namespaces.
     * 
     * @param excludedURIs the finite set of namespace URI strings to exclude from the set, or null if this set is infinite
     * @param includedURIs the finite set of namespace URI strings to include in the set, or null if this set is infinite
     * @param excludedQNamesInIncludedURIs the finite set of exceptional QNames to exclude from the included namespaces
     * @param excludedQNamesInIncludedURIs the finite set of exceptional QNames to include that are in the excluded namespaces
     * 
     * @return the constructed QNameSet
     */
    public static QNameSet forSets(Set excludedURIs, Set includedURIs, Set excludedQNamesInIncludedURIs, Set includedQNamesInExcludedURIs)
    {
        if ((excludedURIs != null) == (includedURIs != null))
            throw new IllegalArgumentException("Exactly one of excludedURIs and includedURIs must be null");

        if (excludedURIs == null && includedURIs.isEmpty() && includedQNamesInExcludedURIs.isEmpty())
            return EMPTY;
        if (includedURIs == null && excludedURIs.isEmpty() && excludedQNamesInIncludedURIs.isEmpty())
            return ALL;
        if (excludedURIs == null && includedURIs.size() == 1 && includedURIs.contains("") &&
            includedQNamesInExcludedURIs.isEmpty() && excludedQNamesInIncludedURIs.isEmpty())
            return LOCAL;
        if (includedURIs == null && excludedURIs.size() == 1 && excludedURIs.contains("") &&
            excludedQNamesInIncludedURIs.isEmpty() && includedQNamesInExcludedURIs.isEmpty())
            return NONLOCAL;

        return new QNameSet(
                minSetCopy(excludedURIs),
                minSetCopy(includedURIs),
                minSetCopy(excludedQNamesInIncludedURIs),
                minSetCopy(includedQNamesInExcludedURIs));
    }

    /**
     * Returns a QNameSet based on the given array of included QNames
     * 
     * @param includedQNames the array of included QNames
     */
    public static QNameSet forArray(QName[] includedQNames)
    {
        if (includedQNames == null)
            throw new IllegalArgumentException("includedQNames cannot be null");

        return new QNameSet(null, new HashSet(), null, new HashSet(Arrays.asList(includedQNames)));
    }

    /**
     * Returns a QNameSet with the same contents as the given
     * QNameSetSpecification.
     * @return the copied QNameSet
     */
    public static QNameSet forSpecification(QNameSetSpecification spec)
    {
        if (spec instanceof QNameSet)
            return (QNameSet)spec;
        return QNameSet.forSets(spec.excludedURIs(), spec.includedURIs(), spec.excludedQNamesInIncludedURIs(), spec.includedQNamesInExcludedURIs());
    }

    /**
     * Returns a QNameSet corresponding to the given wildcard namespace string.
     * This is a space-separated list of URIs, plus special tokens as specified
     * in the XML Schema specification (##any, ##other, ##targetNamespace, ##local).
     * @return the constructed QNameSet
     */
    public static QNameSet forWildcardNamespaceString(String wildcard, String targetURI)
    {
        return QNameSet.forSpecification(new QNameSetBuilder(wildcard, targetURI));
    }

    /**
     * Returns a QNameSet containing only the given QName.
     * @return the constructed QNameSet
     */
    public static QNameSet singleton(QName name)
    {
        return new QNameSet(null, Collections.EMPTY_SET, Collections.EMPTY_SET, Collections.singleton(name));
    }


    /**
     * Constructs a QNameSetBuilder whose contents are given by
     * the four sets.
     *
     * This constuctor is PRIVATE because it uses the given
     * sets directly, and it trusts its callers to set only immutable values.
     * This constructor is is only called by the static builder methods on
     * QNameSet: those methods are all careful assign only unchanging sets.
     */
    private QNameSet(Set excludedURIs, Set includedURIs, Set excludedQNamesInIncludedURIs, Set includedQNamesInExcludedURIs)
    {
        if (includedURIs != null && excludedURIs == null)
        {
            _inverted = false;
            _includedURIs = includedURIs;
            _excludedQNames = excludedQNamesInIncludedURIs;
            _includedQNames = includedQNamesInExcludedURIs;
        }
        else if (excludedURIs != null && includedURIs == null)
        {
            _inverted = true;
            _includedURIs = excludedURIs;
            _excludedQNames = includedQNamesInExcludedURIs;
            _includedQNames = excludedQNamesInIncludedURIs;
        }
        else
            throw new IllegalArgumentException("Exactly one of excludedURIs and includedURIs must be null");
    }

    /**
     * Local xml names are hased using "" as the namespace.
     */
    private static String nsFromName(QName xmlName)
    {
        String ns = xmlName.getNamespaceURI();
        return ns == null ? "" : ns;
    }

    /**
     * True if this ModelTransitionSet contains the given qname.
     */
    public boolean contains(QName name)
    {
        boolean in = _includedURIs.contains(nsFromName(name)) ?
                     !_excludedQNames.contains(name) :
                      _includedQNames.contains(name);
        return _inverted ^ in;
    }

    /**
     * True if this ModelTransitionSet contains all QNames.
     */
    public boolean isAll()
    {
        return _inverted && _includedURIs.isEmpty() && _includedQNames.isEmpty();
    }

    /**
     * True if this ModelTransitionSet contains no QNames.
     */
    public boolean isEmpty()
    {
        return !_inverted && _includedURIs.isEmpty() && _includedQNames.isEmpty();
    }

    /**
     * Returns a new QNameSet that is the intersection of this one and another.
     * @param set the set to insersect with
     * @return the intersection
     */
    public QNameSet intersect(QNameSetSpecification set)
    {
        QNameSetBuilder result = new QNameSetBuilder(this);
        result.restrict(set);
        return result.toQNameSet();
    }

    /**
     * Returns a new QNameSet that is the union of this one and another.
     * @param set the set to union with
     * @return the union
     */
    public QNameSet union(QNameSetSpecification set)
    {
        QNameSetBuilder result = new QNameSetBuilder(this);
        result.addAll(set);
        return result.toQNameSet();
    }

    /**
     * Returns a new QNameSet that is the inverse of this one.
     */
    public QNameSet inverse()
    {
        if (this == EMPTY)
            return ALL;
        if (this == ALL)
            return EMPTY;
        if (this == LOCAL)
            return NONLOCAL;
        if (this == NONLOCAL)
            return LOCAL;
        return new QNameSet(includedURIs(), excludedURIs(), includedQNamesInExcludedURIs(), excludedQNamesInIncludedURIs());
    }

    /**
     * True if the given set is a subset of this one.
     * @param set the set to test
     * @return true if this contains all QNames contained by the given set
     */
    public boolean containsAll(QNameSetSpecification set)
    {
        // a.contains(b) == a.inverse.isDisjoint(b)
        if (!_inverted && set.excludedURIs() != null)
            return false;
        
        return inverse().isDisjoint(set);
    }

    /**
     * True if the given set is disjoint from this one.
     * @param set the set to test
     * @return true if the set is disjoint from this set
     */
    public boolean isDisjoint(QNameSetSpecification set)
    {
        if (_inverted && set.excludedURIs() != null)
            return false;

        if (_inverted)
            return isDisjointImpl(set, this);
        else
            return isDisjointImpl(this, set);
    }
    
    private boolean isDisjointImpl(QNameSetSpecification set1, QNameSetSpecification set2)
    {
        Set includeURIs = set1.includedURIs();
        Set otherIncludeURIs = set2.includedURIs();
        if (otherIncludeURIs != null)
        {
            for (Iterator i = includeURIs.iterator(); i.hasNext(); )
            {
                if (otherIncludeURIs.contains(i.next()))
                    return false;
            }
        }
        else
        {
            Set otherExcludeURIs = set2.excludedURIs();
            for (Iterator i = includeURIs.iterator(); i.hasNext(); )
            {
                if (!otherExcludeURIs.contains(i.next()))
                    return false;
            }
        }

        for (Iterator i = set1.includedQNamesInExcludedURIs().iterator(); i.hasNext(); )
        {
            if (set2.contains((QName)i.next()))
                return false;
        }

        if (includeURIs.size() > 0)
            for (Iterator i = set2.includedQNamesInExcludedURIs().iterator(); i.hasNext(); )
        {
            if (set1.contains((QName)i.next()))
                return false;
        }

        return true;
    }

    
    /**
     * Namespaces that are fully excluded from the set except for a finite
     * number of individual QName exceptions.  Returns null if this set is infinite.
     * @return the set of excluded namespace URI strings
     */ 
    public Set excludedURIs()
    {
        if (_inverted) return Collections.unmodifiableSet(_includedURIs);
        return null;
    }

    /**
     * Namespaces that are fully included in set except for a finite
     * number of individual QName exceptions. Returns null if this set is infinite.
     * @return the set of included namespace URI strings
     */ 
    public Set includedURIs()
    {
        if (!_inverted) return _includedURIs;
        return null;
    }
    
    /**
     * The set of QNames excluded from the set even though they are within
     * a namespace that is otherwise fully included in the set.
     * @return the set of excluded QNames from within includedURI namespaces
     */ 
    public Set excludedQNamesInIncludedURIs()
    {
        return Collections.unmodifiableSet(_inverted ? _includedQNames : _excludedQNames);
    }

    /**
     * The set of QNames included in the set even though they are within
     * a namespace that is otherwise fully included in the set.
     * @return the set of included QNames from within excludedURI namespaces
     */ 
    public Set includedQNamesInExcludedURIs()
    {
        return Collections.unmodifiableSet(_inverted ? _excludedQNames : _includedQNames);
    }
    
    private String prettyQName(QName name)
    {
        if (name.getNamespaceURI() == null)
            return name.getLocalPart();
        return name.getLocalPart() + "@" + name.getNamespaceURI();
    }

    /**
     * Returns a string representation useful for debugging, subject to change.
     */ 
    public String toString()
    {
        StringBuffer sb = new StringBuffer();
        sb.append("QNameSet");
        sb.append(_inverted ? "-(" : "+(");
        for (Iterator i = _includedURIs.iterator(); i.hasNext(); )
        {
            sb.append("+*@");
            sb.append(i.next());
            sb.append(", ");
        }
        for (Iterator i = _excludedQNames.iterator(); i.hasNext(); )
        {
            sb.append("-");
            sb.append(prettyQName((QName)i.next()));
            sb.append(", ");
        }
        for (Iterator i = _includedQNames.iterator(); i.hasNext(); )
        {
            sb.append("+");
            sb.append(prettyQName((QName)i.next()));
            sb.append(", ");
        }
        int index = sb.lastIndexOf(", ");
        if (index > 0)
            sb.setLength(index);
        sb.append(')');
        return sb.toString();
    }
}
