/*

   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.batik.dom.util;

import org.w3c.dom.Element;

import org.apache.batik.util.CleanerThread;

/**
 * This class contains informations about a document.
 *
 * @author <a href="mailto:stephane@hillion.org">Stephane Hillion</a>
 * @version $Id$
 */
public class DocumentDescriptor {

    /**
     * The table initial capacity
     */
    protected static final int INITIAL_CAPACITY = 101;

    /**
     * The underlying array
     */
    protected Entry[] table;

    /**
     * The number of entries
     */
    protected int count;

    /**
     * Creates a new table.
     */
    public DocumentDescriptor() {
        table = new Entry[INITIAL_CAPACITY];
    }

    /**
     * Returns the number of elements in the document.
     */
    public int getNumberOfElements() {
        synchronized (this) {
                  return count;
        }
    }

    /**
     * Returns the location line in the source file of the end element.
     * @return zero if the information is unknown.
     */
    public int getLocationLine(Element elt) {
        synchronized (this) {
            int hash = elt.hashCode() & 0x7FFFFFFF;
            int index = hash % table.length;

            for (Entry e = table[index]; e != null; e = e.next) {
                if (e.hash != hash)
                    continue;
                Object o = e.get();
                if (o == elt)
                    return e.locationLine;
            }
        }
        return 0;
    }

    /**
     * Returns the location column in the source file of the end element.
     * @return zero if the information is unknown.
     */
    public int getLocationColumn(Element elt) {
        synchronized (this) {
            int hash = elt.hashCode() & 0x7FFFFFFF;
            int index = hash % table.length;

            for (Entry e = table[index]; e != null; e = e.next) {
                if (e.hash != hash)
                    continue;
                Object o = e.get();
                if (o == elt)
                    return e.locationColumn;
            }
        }
        return 0;
    }

    /**
     * Sets the location in the source file of the end element.
     */
    public void setLocation(Element elt, int line, int col) {
        synchronized (this) {
            int hash  = elt.hashCode() & 0x7FFFFFFF;
            int index = hash % table.length;

            for (Entry e = table[index]; e != null; e = e.next) {
                if (e.hash != hash)
                    continue;
                Object o = e.get();
                if (o == elt)
                    e.locationLine = line;
            }

            // The key is not in the hash table
            int len = table.length;
            if (count++ >= (len - ( len >> 2 ))) {
                // more than 75% loaded: grow
                rehash();
                index = hash % table.length;
            }

            Entry e = new Entry(hash, elt, line, col, table[index]);
            table[index] = e;
        }
    }

    /**
     * Rehash the table
     */
    protected void rehash () {
        Entry[] oldTable = table;

        table = new Entry[oldTable.length * 2 + 1];

        for (int i = oldTable.length-1; i >= 0; i--) {
            for (Entry old = oldTable[i]; old != null;) {
                Entry e = old;
                old = old.next;

                int index = e.hash % table.length;
                e.next = table[index];
                table[index] = e;
            }
        }
    }

    protected void removeEntry(Entry e) {
        synchronized (this) {
            int hash = e.hash;
            int index = hash % table.length;
            Entry curr = table[index];
            Entry prev = null;
            while (curr != e) {
                prev = curr;
                curr = curr.next;
            }
            if (curr == null) return; // already remove???

            if (prev == null)
                // First entry.
                table[index] = curr.next;
            else
                prev.next = curr.next;
            count--;
        }
    }

    /**
     * To manage collisions
     */
    protected class Entry extends CleanerThread.WeakReferenceCleared {
      /**
       * The hash code
       */
      public int hash;

      /**
       * The line number.
       */
      public int locationLine;

      /**
       * The column number.
       */
      public int locationColumn;

      /**
       * The next entry
       */
      public Entry next;

        /**
         * Creates a new entry
         */
        public Entry(int hash,
                     Element element,
                     int locationLine,
                     int locationColumn,
                     Entry next) {
            super(element);
            this.hash           = hash;
            this.locationLine   = locationLine;
            this.locationColumn = locationColumn;
            this.next           = next;
        }

        public void cleared() {
            removeEntry(this);
        }
    }
}
