// 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.tapestry5.ioc.util;

import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
import org.apache.tapestry5.ioc.internal.util.InternalUtils;

import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;

/**
 * Used to "uniquify" names within a given context. A base name is passed in, and the return value is the base name, or
 * the base name extended with a suffix to make it unique.
 *
 * This class is not threadsafe.
 */

public final class IdAllocator
{
    private static final String SEPARATOR = "_";

    /**
     * Map from allocated id to a generator for names associated with the allocated id.
     */
    private final Map<String, NameGenerator> generatorMap;

    private final String namespace;

    /**
     * Generates unique names with a particular prefix.
     */
    private static class NameGenerator implements Cloneable
    {
        private final String baseId;

        private int index;

        NameGenerator(String baseId)
        {
            this.baseId = baseId + SEPARATOR;
        }

        public String nextId()
        {
            return baseId + index++;
        }

        /**
         * Clones this instance, returning an equivalent but separate copy.
         */
        @SuppressWarnings(
        { "CloneDoesntDeclareCloneNotSupportedException" })
        @Override
        public NameGenerator clone()
        {
            try
            {
                return (NameGenerator) super.clone();
            }
            catch (CloneNotSupportedException ex)
            {
                // Unreachable!
                throw new RuntimeException(ex);
            }
        }
    }

    /**
     * Creates a new allocator with no namespace.
     */
    public IdAllocator()
    {
        this("");
    }

    /**
     * Creates a new allocator with the provided namespace.
     */
    public IdAllocator(String namespace)
    {
        this(namespace, new HashMap<String, NameGenerator>());
    }

    private IdAllocator(String namespace, Map<String, NameGenerator> generatorMap)
    {
        this.namespace = namespace;
        this.generatorMap = generatorMap;
    }

    /**
     * Returns a list of all allocated ids, sorted alphabetically.
     */
    public List<String> getAllocatedIds()
    {
        return InternalUtils.sortedKeys(generatorMap);
    }

    /**
     * Creates a clone of this IdAllocator instance, copying the allocator's namespace and key map.
     */
    @SuppressWarnings(
    { "CloneDoesntCallSuperClone" })
    @Override
    public IdAllocator clone()
    {
        // Copying the generatorMap is tricky; multiple keys will point to the same NameGenerator
        // instance. We need to clone the NameGenerators, then build a new map around the clones.

        IdentityHashMap<NameGenerator, NameGenerator> transformMap = new IdentityHashMap<NameGenerator, NameGenerator>();

        for (NameGenerator original : generatorMap.values())
        {
            NameGenerator copy = original.clone();

            transformMap.put(original, copy);
        }

        Map<String, NameGenerator> mapCopy = CollectionFactory.newMap();

        for (Map.Entry<String, NameGenerator> entry : generatorMap.entrySet())
        {
            NameGenerator copy = transformMap.get(entry.getValue());

            mapCopy.put(entry.getKey(), copy);
        }

        return new IdAllocator(namespace, mapCopy);
    }

    /**
     * Allocates the id. Repeated calls for the same name will return "name", "name_0", "name_1", etc.
     */
    public String allocateId(String name)
    {
        String key = name + namespace;

        NameGenerator g = generatorMap.get(key);
        String result;

        if (g == null)
        {
            g = new NameGenerator(key);
            result = key;
        }
        else
            result = g.nextId();

        // Handle the degenerate case, where a base name of the form "foo_0" has been
        // requested. Skip over any duplicates thus formed.

        while (generatorMap.containsKey(result))
            result = g.nextId();

        generatorMap.put(result, g);

        return result;
    }

    /**
     * Checks to see if a given name has been allocated.
     */
    public boolean isAllocated(String name)
    {
        return generatorMap.containsKey(name);
    }

    /**
     * Clears the allocator, resetting it to freshly allocated state.
     */
    public void clear()
    {
        generatorMap.clear();
    }
}
