/*
 *
 *  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.flex.abc.semantics;

import org.apache.flex.abc.ABCConstants;

/**
 * A representation of a <a href="http://learn.adobe.com/wiki/display/AVM2/4.3+Constant+pool">namespace</a>.
 */
public class Namespace
{
    /**
     * A prime number unlikely to be a divisor of the hash table size, used to
     * generate composite hash keys.
     * 
     * @warn if you copy this, pick a new prime to generate distinct hash keys
     * in different classes.
     */
    private static final long PRIME_MULTIPLIER = 9929;

    /**
     * Whether or not private namespaces should be merged.
     * 
     * This changes the semantics of private namespaces which is
     * officially Not A Good Thing (tm), but is the only way inlining
     * can be made to work until we can do it at the ABC link time, but
     * that requires better static analysis which we don't have yet.
     */
    private boolean mergePrivateNamespaces = false;

    public Namespace(int kind)
    {
        this(kind, "");
    }

    public Namespace(int kind, String name)
    {
        this.kind = kind;
        this.apiVersion = extractApiVersion(name);

        this.name = this.apiVersion != ABCConstants.NO_API_VERSION ?
                stripApiVersion(name) :
                name;
    }

    /**
     * The Namespace's kind.
     * 
     * @see ABCConstants
     */
    private final int kind;

    /**
     * The Namespace's name, stripped of any API version markers.
     */
    private final String name;

    /**
     * The Namespace's API version, or ABCConstants.NO_API_VERSION.
     */
    private final int apiVersion;

    /**
     * Use name-oriented hashing unless the kind is private, for private
     * namespaces use identity semantics.
     * <p>
     * {@inheritDoc}
     */
    @Override
    public int hashCode()
    {
        if (kind != ABCConstants.CONSTANT_PrivateNs || mergePrivateNamespaces)
        {
            long result = this.name.hashCode();
            result = result * PRIME_MULTIPLIER + this.kind;
            if (this.apiVersion != ABCConstants.NO_API_VERSION)
                result = result * PRIME_MULTIPLIER + this.apiVersion;

            return (int)result;
        }
        else
        {
            //  Always use identity comparision for private namespaces.
            return super.hashCode();
        }
    }

    /**
     * Check name-oriented equality unless the kind is private, for private
     * namespaces use identity semantics.
     * <p>
     * {@inheritDoc}
     */
    @Override
    public boolean equals(Object obj)
    {
        if (this == obj)
            return true;

        if (obj instanceof Namespace && this.kind != ABCConstants.CONSTANT_PrivateNs || mergePrivateNamespaces)
        {
            Namespace that = (Namespace) obj;
            if(that != null) {
                return that.kind == this.kind &&
                        that.apiVersion == this.apiVersion &&
                        that.name.equals(this.name);
            }
            return false;
        }

        //  Other object is not a Namespace.
        return false;
    }

    @Override
    public String toString()
    {
        return String.format("%s:\"%s\"", getKindString(), this.name);
    }

    /**
     * @return the Namespace's kind.
     */
    public int getKind()
    {
        return kind;
    }

    /**
     * Helper method used by toString().
     * 
     * @return A readable string for the namespace kind.
     */
    private String getKindString()
    {
        switch (kind)
        {
            case ABCConstants.CONSTANT_Namespace:
                return "Ns";
                
            case ABCConstants.CONSTANT_PackageNs:
                return "PackageNs";
                
            case ABCConstants.CONSTANT_PackageInternalNs:
                return "PackageInternalNs";
                
            case ABCConstants.CONSTANT_ProtectedNs:
                return "ProtectedNs";
                
            case ABCConstants.CONSTANT_ExplicitNamespace:
                return "ExplicitNs";
                
            case ABCConstants.CONSTANT_StaticProtectedNs:
                return "StaticProtectedNs";
                
            case ABCConstants.CONSTANT_PrivateNs:
                return "PrivateNs";
        }

        return "Unknown(0x" + Integer.toHexString(kind) + ")";
    }

    /**
     * @return the namespace's name.
     */
    public String getName()
    {
        return this.name;
    }

    /**
     * @return the namespace's name and API version, encoded into the ABC-format
     * URI.
     */
    public String getVersionedName()
    {
        if (getApiVersion() == ABCConstants.NO_API_VERSION)
            return this.name;
        else
            return this.name + new String(new int[] {getApiVersion() + ABCConstants.MIN_API_MARK}, 0, 1);
    }

    /**
     * @return the namespace's API version.
     */
    public int getApiVersion()
    {
        return this.apiVersion;
    }

    /**
     * Get the API version from a Namespace's name.
     * 
     * @return the Namespace's version number, or NO_API_VERSION if not
     * specified.
     */
    public static int extractApiVersion(String uri)
    {
        if (uri == null || uri.length() == 0)
            return ABCConstants.NO_API_VERSION;

        int last = uri.codePointAt(uri.length() - 1);

        if (last >= ABCConstants.MIN_API_MARK && last <= ABCConstants.MAX_API_MARK)
            return last - ABCConstants.MIN_API_MARK;

        return ABCConstants.NO_API_VERSION;
    }

    /**
     * Get a Namespace's name, minus any API versioning information.
     */
    static String stripApiVersion(String uri)
    {
        return extractApiVersion(uri) > 0 ?
               uri.substring(0, uri.length() - 1) :
               uri;
    }

    /**
     * Set whether or not private namespaces should be merged
     * 
     * @param mergePrivateNamespaces <code>true</code if they should be merged.
     */
    public void setMergePrivateNamespaces(boolean mergePrivateNamespaces)
    {
        assert ((mergePrivateNamespaces == true && kind == ABCConstants.CONSTANT_PrivateNs) ? !name.isEmpty() : true) :
            "private namespaces must have a name when merging enabled";

        this.mergePrivateNamespaces = mergePrivateNamespaces;
    }
}
