/*
 * 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.jackrabbit.core.nodetype;

import org.apache.jackrabbit.core.nodetype.EffectiveNodeTypeCache.Key;
import org.apache.jackrabbit.spi.Name;

import java.util.TreeSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.ArrayList;

import EDU.oswego.cs.dl.util.concurrent.ConcurrentReaderHashMap;

/**
 * Implements an effective node type cache that uses a bit set for storing the
 * information about participating node types in a set.
 */
public class BitSetENTCacheImpl implements EffectiveNodeTypeCache {

    /**
     * constant for bits-per-word
     */
    private static final int BPW = 64;

    /**
     * OR mask for bit set
     */
    private static final long[] OR_MASK = new long[BPW];
    static {
        for (int i = 0; i < BPW; i++) {
            OR_MASK[i] = 1L << i;
        }
    }

    /**
     * An ordered set of the keys. This is used for {@link #findBest(Key)}.
     */
    private final TreeSet<Key> sortedKeys;

    /**
     * cache of pre-built aggregations of node types
     */
    private final HashMap<Key, EffectiveNodeType> aggregates;

    /**
     * A lookup table for bit numbers for a given name.
     *
     * Note: further performance improvements could be made if this index would
     * be stored in the node type registry since only registered node type names
     * are allowed in the keys.
     */
    private final ConcurrentReaderHashMap nameIndex = new ConcurrentReaderHashMap();

    /**
     * The reverse lookup table for bit numbers to names
     */
    private Name[] names = new Name[1024];

    /**
     * Creates a new bitset effective node type cache
     */
    BitSetENTCacheImpl() {
        sortedKeys = new TreeSet<Key>();
        aggregates = new HashMap<Key, EffectiveNodeType>();
    }

    /**
     * {@inheritDoc}
     */
    public Key getKey(Name[] ntNames) {
        return new BitSetKey(ntNames, nameIndex.size() + ntNames.length);
    }

    /**
     * {@inheritDoc}
     */
    public void put(EffectiveNodeType ent) {
        put(getKey(ent.getMergedNodeTypes()), ent);
    }

    /**
     * {@inheritDoc}
     */
    public void put(Key key, EffectiveNodeType ent) {
        aggregates.put(key, ent);
        sortedKeys.add(key);
    }

    /**
     * {@inheritDoc}
     */
    public Key findBest(Key key) {
        // quick check for already cached key
        if (contains(key)) {
            return key;
        }
        Iterator<Key> iter = sortedKeys.iterator();
        while (iter.hasNext()) {
            Key k = iter.next();
            if (key.contains(k)) {
                return k;
            }
        }
        return null;
    }

    /**
     * {@inheritDoc}
     */
    public void invalidate(Name name) {
        /**
         * remove all affected effective node types from aggregates cache
         * (copy keys first to prevent ConcurrentModificationException)
         */
        ArrayList<Key> keys = new ArrayList<Key>(aggregates.keySet());
        for (Iterator<Key> keysIter = keys.iterator(); keysIter.hasNext();) {
            Key k = keysIter.next();
            EffectiveNodeType ent = get(k);
            if (ent.includesNodeType(name)) {
                remove(k);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    public boolean contains(Key key) {
        return aggregates.containsKey(key);
    }

    /**
     * {@inheritDoc}
     */
    public EffectiveNodeType get(Key key) {
        return aggregates.get(key);
    }

    /**
     * Returns the bit number for the given name. If the name does not exist
     * a new new bit number for that name is created.
     *
     * @param name the name to lookup
     * @return the bit number for the given name
     */
    private int getBitNumber(Name name) {
        Integer i = (Integer) nameIndex.get(name);
        if (i == null) {
            synchronized (nameIndex) {
                i = (Integer) nameIndex.get(name);
                if (i == null) {
                    int idx = nameIndex.size();
                    i = new Integer(idx);
                    nameIndex.put(name, i);
                    if (idx >= names.length) {
                        Name[] newNames = new Name[names.length * 2];
                        System.arraycopy(names, 0, newNames, 0, names.length);
                        names = newNames;
                    }
                    names[idx] = name;
                }
            }
        }
        return i.intValue();
    }

    /**
     * Returns the node type name for a given bit number.
     * @param n the bit number to lookup
     * @return the node type name
     */
    private Name getName(int n) {
        return names[n];
    }

    /**
     * Removes the effective node type for the given key from the cache.
     *
     * @param key the key of the effective node type to remove
     * @return the removed effective node type or <code>null</code> if it was
     *         never cached.
     */
    private EffectiveNodeType remove(Key key) {
        EffectiveNodeType removed = aggregates.remove(key);
        if (removed != null) {
            // other than the original implementation, the weights in the
            // treeset are now the same as in the given keys. so we can use
            // the normal remove method
            sortedKeys.remove(key);
        }
        return removed;
    }

    /**
     * {@inheritDoc}
     */
    public Object clone() {
        BitSetENTCacheImpl clone = new BitSetENTCacheImpl();
        clone.sortedKeys.addAll(sortedKeys);
        clone.aggregates.putAll(aggregates);
        clone.names = new Name[names.length];
        System.arraycopy(names, 0, clone.names, 0, names.length);
        clone.nameIndex.putAll(nameIndex);
        return clone;
    }

    /**
     * {@inheritDoc}
     */
    public String toString() {
        StringBuilder builder = new StringBuilder();
        builder.append("BitSetENTCacheImpl (" + super.toString() + ")\n");
        builder.append("EffectiveNodeTypes in cache:\n");
        for (Key key : sortedKeys) {
            builder.append(key);
            builder.append("\n");
        }
        return builder.toString();
    }

    /**
     * Implements a {@link Key} by storing the node type aggregate information
     * in a bit set. We do not use the {@link java.util.BitSet} because it
     * does not suit all our requirements. Every node type is represented by a bit
     * in the set. This key is immutable.
     */
    private class BitSetKey implements Key {

        /**
         * The names of the node types that form this key.
         */
        private final Name[] names;

        /**
         * The array of longs that hold the bit information.
         */
        private final long[] bits;

        /**
         * the hash code, only calculated once
         */
        private final int hashCode;

        /**
         * Creates a new bit set key.
         * @param names the node type names
         * @param maxBit the approximative number of the greatest bit
         */
        public BitSetKey(Name[] names, int maxBit) {
            this.names = names;
            bits = new long[maxBit / BPW + 1];

            for (int i = 0; i < names.length; i++) {
                int n = getBitNumber(names[i]);
                bits[n / BPW] |= OR_MASK[n % BPW];
            }
            hashCode = calcHashCode();
        }

        /**
         * Creates a new bit set key.
         * @param bits the array of bits
         * @param numBits the number of bits that are '1' in the given bits
         */
        private BitSetKey(long[] bits, int numBits) {
            this.bits = bits;
            names = new Name[numBits];
            int i = nextSetBit(0);
            int j = 0;
            while (i >= 0) {
                names[j++] = BitSetENTCacheImpl.this.getName(i);
                i = nextSetBit(i + 1);
            }
            hashCode = calcHashCode();
        }

        /**
         * {@inheritDoc}
         */
        public Name[] getNames() {
            return names;
        }

        /**
         * {@inheritDoc}
         */
        public boolean contains(Key otherKey) {
            /*
             * 0 - 0 => 0
             * 0 - 1 => 1
             * 1 - 0 => 0
             * 1 - 1 => 0
             * !a and b
             */
            BitSetKey other = (BitSetKey) otherKey;
            int len = Math.max(bits.length, other.bits.length);
            for (int i = 0; i < len; i++) {
                long w1 = i < bits.length ? bits[i] : 0;
                long w2 = i < other.bits.length ? other.bits[i] : 0;
                long r = ~w1 & w2;
                if (r != 0) {
                    return false;
                }
            }
            return true;
        }

        /**
         * {@inheritDoc}
         */
        public Key subtract(Key otherKey) {
            /*
             * 0 - 0 => 0
             * 0 - 1 => 0
             * 1 - 0 => 1
             * 1 - 1 => 0
             * a and !b
             */
            BitSetKey other = (BitSetKey) otherKey;
            int len = Math.max(bits.length, other.bits.length);
            long[] newBits = new long[len];
            int numBits = 0;
            for (int i = 0; i < len; i++) {
                long w1 = i < bits.length ? bits[i] : 0;
                long w2 = i < other.bits.length ? other.bits[i] : 0;
                newBits[i] = w1 & ~w2;
                numBits += bitCount(newBits[i]);
            }
            return new BitSetKey(newBits, numBits);
        }

        /**
         * Returns the bit number of the next bit that is set, starting at
         * <code>fromIndex</code> inclusive.
         *
         * @param fromIndex the bit position to start the search
         * @return the bit position of the bit or -1 if none found.
         */
        private int nextSetBit(int fromIndex) {
            int addr = fromIndex / BPW;
            int off = fromIndex % BPW;
            while (addr < bits.length) {
                if (bits[addr] != 0) {
                    while (off < BPW) {
                        if ((bits[addr] & OR_MASK[off]) != 0) {
                            return addr * BPW + off;
                        }
                        off++;
                    }
                    off = 0;
                }
                addr++;
            }
            return -1;
        }

         /**
          * Returns the number of bits set in val.
          * For a derivation of this algorithm, see
          * "Algorithms and data structures with applications to
          *  graphics and geometry", by Jurg Nievergelt and Klaus Hinrichs,
          *  Prentice Hall, 1993.
          *
          * @param val the value to calculate the bit count for
          * @return the number of '1' bits in the value
          */
         private int bitCount(long val) {
             val -= (val & 0xaaaaaaaaaaaaaaaaL) >>> 1;
             val =  (val & 0x3333333333333333L) + ((val >>> 2) & 0x3333333333333333L);
             val =  (val + (val >>> 4)) & 0x0f0f0f0f0f0f0f0fL;
             val += val >>> 8;
             val += val >>> 16;
             return ((int) (val) + (int) (val >>> 32)) & 0xff;
         }


        /**
         * {@inheritDoc}
         *
         * This compares 1. the cardinality (number of set bits) and 2. the
         * numeric value of the bit sets in descending order.
         */
        public int compareTo(Key other) {
            BitSetKey o = (BitSetKey) other;
            int res = o.names.length - names.length;
            if (res == 0) {
                int adr = Math.max(bits.length, o.bits.length) - 1;
                while (adr >= 0) {
                    long w1 = adr < bits.length ? bits[adr] : 0;
                    long w2 = adr < o.bits.length ? o.bits[adr] : 0;
                    if (w1 != w2) {
                        // some signed arithmetic here
                        long h1 = w1 >>> 32;
                        long h2 = w2 >>> 32;
                        if (h1 == h2) {
                            h1 = w1 & 0x0ffffffffL;
                            h2 = w2 & 0x0ffffffffL;
                        }
                        return Long.signum(h2 - h1);
                    }
                    adr--;
                }
            }
            return res;
        }

        /**
         * {@inheritDoc}
         */
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj instanceof BitSetKey) {
                BitSetKey o = (BitSetKey) obj;
                if (names.length != o.names.length) {
                    return false;
                }
                int adr = Math.max(bits.length, o.bits.length) - 1;
                while (adr >= 0) {
                    long w1 = adr < bits.length ? bits[adr] : 0;
                    long w2 = adr < o.bits.length ? o.bits[adr] : 0;
                    if (w1 != w2) {
                        return false;
                    }
                    adr--;
                }
                return true;
            }
            return false;
        }

        /**
         * {@inheritDoc}
         */
        public int hashCode() {
            return hashCode;
        }

        /**
         * Calculates the hash code.
         * @return the calculated hash code
         */
        private int calcHashCode() {
            long h = 1234;
            int addr = bits.length - 1;
            while (addr >= 0 && bits[addr] == 0) {
                addr--;
            }
            while (addr >= 0) {
                h ^= bits[addr] * (addr + 1);
                addr--;
            }
            return (int) ((h >> 32) ^ h);
        }

        /**
         * {@inheritDoc}
         */
        public String toString() {
            StringBuilder buf = new StringBuilder("w=");
            buf.append(names.length);
            int i = nextSetBit(0);
            while (i >= 0) {
                buf.append(", ").append(i).append("=");
                buf.append(BitSetENTCacheImpl.this.getName(i));
                i = nextSetBit(i + 1);
            }
            return buf.toString();
        }

    }

}
