package org.apache.lucene.facet.taxonomy.writercache.cl2o;

import java.io.IOException;
import java.util.Iterator;
import java.util.NoSuchElementException;

import org.apache.lucene.facet.taxonomy.CategoryPath;

/**
 * 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.
 */

/**
 * HashMap to store colliding labels. See {@link CompactLabelToOrdinal} for
 * details.
 * 
 * @lucene.experimental
 */
public class CollisionMap {

  private int capacity;
  private float loadFactor;
  private int size;
  private int threshold;

  static class Entry {
    int offset;
    int cid;
    Entry next;
    int hash;

    Entry(int offset, int cid, int h, Entry e) {
      this.offset = offset;
      this.cid = cid;
      this.next = e;
      this.hash = h;
    }
  }

  private CharBlockArray labelRepository;

  private Entry[] entries;

  public CollisionMap(CharBlockArray labelRepository) {
    this(16 * 1024, 0.75f, labelRepository);
  }

  public CollisionMap(int initialCapacity, CharBlockArray labelRepository) {
    this(initialCapacity, 0.75f, labelRepository);
  }

  private CollisionMap(int initialCapacity, float loadFactor, CharBlockArray labelRepository) {
    this.labelRepository = labelRepository;
    this.loadFactor = loadFactor;
    this.capacity = CompactLabelToOrdinal.determineCapacity(2, initialCapacity);

    this.entries = new Entry[this.capacity];
    this.threshold = (int) (this.capacity * this.loadFactor);
  }

  public int size() {
    return this.size;
  }

  public int capacity() {
    return this.capacity;
  }

  private void grow() {
    int newCapacity = this.capacity * 2;
    Entry[] newEntries = new Entry[newCapacity];
    Entry[] src = this.entries;

    for (int j = 0; j < src.length; j++) {
      Entry e = src[j];
      if (e != null) {
        src[j] = null;
        do {
          Entry next = e.next;
          int hash = e.hash;
          int i = indexFor(hash, newCapacity);  
          e.next = newEntries[i];
          newEntries[i] = e;
          e = next;
        } while (e != null);
      }
    }

    this.capacity = newCapacity;
    this.entries = newEntries;
    this.threshold = (int) (this.capacity * this.loadFactor);
  }

  public int get(CategoryPath label, int hash) {
    int bucketIndex = indexFor(hash, this.capacity);
    Entry e = this.entries[bucketIndex];

    while (e != null && !(hash == e.hash && label.equalsToSerialized(this.labelRepository, e.offset))) { 
      e = e.next;
    }
    if (e == null) {
      return LabelToOrdinal.InvalidOrdinal;
    }

    return e.cid;
  }

  public int get(CategoryPath label, int prefixLen, int hash) {
    int bucketIndex = indexFor(hash, this.capacity);
    Entry e = this.entries[bucketIndex];

    while (e != null && !(hash == e.hash && label.equalsToSerialized(prefixLen, this.labelRepository, e.offset))) { 
      e = e.next;
    }
    if (e == null) {
      return LabelToOrdinal.InvalidOrdinal;
    }

    return e.cid;
  }

  public int addLabel(CategoryPath label, int hash, int cid) {
    int bucketIndex = indexFor(hash, this.capacity);
    for (Entry e = this.entries[bucketIndex]; e != null; e = e.next) {
      if (e.hash == hash && label.equalsToSerialized(this.labelRepository, e.offset)) {
        return e.cid;
      }
    }

    // new string; add to label repository
    int offset = this.labelRepository.length();
    try {
      label.serializeAppendTo(labelRepository);
    } catch (IOException e) {
      // can't happen, because labelRepository.append() doesn't throw an exception
    }

    addEntry(offset, cid, hash, bucketIndex);
    return cid;
  }

  public int addLabel(CategoryPath label, int prefixLen, int hash, int cid) {
    int bucketIndex = indexFor(hash, this.capacity);
    for (Entry e = this.entries[bucketIndex]; e != null; e = e.next) {
      if (e.hash == hash && label.equalsToSerialized(prefixLen, this.labelRepository, e.offset)) {
        return e.cid;
      }
    }

    // new string; add to label repository
    int offset = this.labelRepository.length();
    try {
      label.serializeAppendTo(prefixLen, labelRepository);
    } catch (IOException e) {
      // can't happen, because labelRepository.append() doesn't throw an exception
    }

    addEntry(offset, cid, hash, bucketIndex);
    return cid;
  }

  /**
   * This method does not check if the same value is already
   * in the map because we pass in an char-array offset, so
   * so we now that we're in resize-mode here. 
   */
  public void addLabelOffset(int hash, int offset, int cid) {
    int bucketIndex = indexFor(hash, this.capacity);
    addEntry(offset, cid, hash, bucketIndex);
  }

  private void addEntry(int offset, int cid, int hash, int bucketIndex) {
    Entry e = this.entries[bucketIndex];
    this.entries[bucketIndex] = new Entry(offset, cid, hash, e);
    if (this.size++ >= this.threshold) {
      grow();
    }
  }

  Iterator<CollisionMap.Entry> entryIterator() {
    return new EntryIterator(entries, size);
  }

  /**
   * Returns index for hash code h. 
   */
  static int indexFor(int h, int length) {
    return h & (length - 1);
  }

  /**
   * Returns an estimate of the memory usage of this CollisionMap.
   * @return The approximate number of bytes used by this structure.
   */
  int getMemoryUsage() {
    int memoryUsage = 0;
    if (this.entries != null) {
      for (Entry e : this.entries) {
        if (e != null) {
          memoryUsage += (4 * 4);
          for (Entry ee = e.next; ee != null; ee = ee.next) {
            memoryUsage += (4 * 4);
          }
        }
      }
    }
    return memoryUsage;
  }

  private class EntryIterator implements Iterator<Entry> {
    Entry next;    // next entry to return
    int index;        // current slot 
    Entry[] ents;
    
    EntryIterator(Entry[] entries, int size) {
      this.ents = entries;
      Entry[] t = entries;
      int i = t.length;
      Entry n = null;
      if (size != 0) { // advance to first entry
        while (i > 0 && (n = t[--i]) == null) {
          // advance
        }
      }
      this.next = n;
      this.index = i;
    }

    public boolean hasNext() {
      return this.next != null;
    }

    public Entry next() { 
      Entry e = this.next;
      if (e == null) throw new NoSuchElementException();

      Entry n = e.next;
      Entry[] t = ents;
      int i = this.index;
      while (n == null && i > 0) {
        n = t[--i];
      }
      this.index = i;
      this.next = n;
      return  e;
    }

    public void remove() {
      throw new UnsupportedOperationException();
    }

  }

}
