/**
 * Copyright (C) 2014-2020 Philip Helger (www.helger.com)
 * philip[at]helger[dot]com
 *
 * 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 com.helger.collection.map;

import java.io.Serializable;
import java.util.Arrays;
import java.util.function.IntFunction;

import javax.annotation.CheckForSigned;
import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;

import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.ReturnsMutableCopy;
import com.helger.commons.equals.EqualsHelper;
import com.helger.commons.lang.GenericReflection;
import com.helger.commons.lang.IHasSize;

/**
 * Special int-Object map. Based on: https://github.com/mikvor/hashmapTest
 *
 * @author Mikhail Vorontsov
 * @author Philip Helger
 * @param <T>
 *        Element type
 */
@NotThreadSafe
public class IntObjectMap <T> implements IHasSize, Serializable
{
  private static final int FREE_KEY = 0;

  public static final Object NO_VALUE = new Object ();

  private final T m_aNoValue = GenericReflection.uncheckedCast (NO_VALUE);

  /** Keys */
  private int [] m_aKeys;
  /** Values */
  private T [] m_aValues;

  /** Do we have 'free' key in the map? */
  private boolean m_bHasFreeKey;
  /** Value of 'free' key */
  private T m_aFreeValue = m_aNoValue;

  /** Fill factor, must be between (0 and 1) */
  private final float m_fFillFactor;
  /** We will resize a map once it reaches this size */
  private int m_nThreshold;
  /** Current map size */
  private int m_nSize;
  /** Mask to calculate the original position */
  private int m_nMask;

  public IntObjectMap ()
  {
    this (16);
  }

  public IntObjectMap (final int nSize)
  {
    this (nSize, 0.75f);
  }

  public IntObjectMap (final int nSize, final float fFillFactor)
  {
    ValueEnforcer.isBetweenInclusive (fFillFactor, "FillFactor", 0f, 1f);
    ValueEnforcer.isGT0 (nSize, "Size");
    final int nCapacity = MapHelper.arraySize (nSize, fFillFactor);
    m_nMask = nCapacity - 1;
    m_fFillFactor = fFillFactor;

    m_aKeys = new int [nCapacity];
    m_aValues = _createValueArray (nCapacity);
    m_nThreshold = (int) (nCapacity * fFillFactor);
  }

  @Nonnull
  @ReturnsMutableCopy
  private static <T> T [] _createValueArray (@Nonnegative final int nSize)
  {
    final Object [] ret = new Object [nSize];
    Arrays.fill (ret, NO_VALUE);
    return GenericReflection.uncheckedCast (ret);
  }

  @Nullable
  public T get (final int key)
  {
    return get (key, null);
  }

  @Nullable
  public T get (final int key, final T aDefault)
  {
    if (key == FREE_KEY)
      return m_bHasFreeKey ? m_aFreeValue : aDefault;

    final int idx = _getReadIndex (key);
    return idx != -1 ? m_aValues[idx] : aDefault;
  }

  @Nullable
  public T computeIfAbsent (final int key, @Nonnull final IntFunction <? extends T> aProvider)
  {
    T ret = get (key);
    if (ret == null)
    {
      ret = aProvider.apply (key);
      if (ret != null)
        put (key, ret);
    }
    return ret;
  }

  @Nullable
  private T _getOld (final T aValue)
  {
    return EqualsHelper.identityEqual (aValue, m_aNoValue) ? null : aValue;
  }

  @Nullable
  public T put (final int key, final T value)
  {
    if (key == FREE_KEY)
    {
      final T ret = m_aFreeValue;
      if (!m_bHasFreeKey)
      {
        ++m_nSize;
        m_bHasFreeKey = true;
      }
      m_aFreeValue = value;
      return _getOld (ret);
    }

    int idx = _getPutIndex (key);
    if (idx < 0)
    {
      // no insertion point? Should not happen...
      _rehash (m_aKeys.length * 2);
      idx = _getPutIndex (key);
    }
    final T prev = m_aValues[idx];
    if (m_aKeys[idx] != key)
    {
      m_aKeys[idx] = key;
      m_aValues[idx] = value;
      ++m_nSize;
      if (m_nSize >= m_nThreshold)
        _rehash (m_aKeys.length * 2);
    }
    else
    {
      // it means used cell with our key
      if (m_aKeys[idx] != key)
        throw new IllegalStateException ();
      m_aValues[idx] = value;
    }
    return _getOld (prev);
  }

  @Nullable
  public T remove (final int key)
  {
    if (key == FREE_KEY)
    {
      if (!m_bHasFreeKey)
        return null;

      m_bHasFreeKey = false;
      final T ret = m_aFreeValue;
      m_aFreeValue = m_aNoValue;
      --m_nSize;
      return _getOld (ret);
    }

    final int idx = _getReadIndex (key);
    if (idx == -1)
      return null;

    final T res = m_aValues[idx];
    m_aValues[idx] = m_aNoValue;
    _shiftKeys (idx);
    --m_nSize;
    return _getOld (res);
  }

  @Nonnegative
  public int size ()
  {
    return m_nSize;
  }

  public boolean isEmpty ()
  {
    return m_nSize == 0;
  }

  private void _rehash (final int nNewCapacity)
  {
    m_nThreshold = (int) (nNewCapacity * m_fFillFactor);
    m_nMask = nNewCapacity - 1;

    final int nOldCapacity = m_aKeys.length;
    final int [] aOldKeys = m_aKeys;
    final T [] aOldValues = m_aValues;

    m_aKeys = new int [nNewCapacity];
    m_aValues = _createValueArray (nNewCapacity);
    m_nSize = m_bHasFreeKey ? 1 : 0;

    int i = nOldCapacity;
    while (i > 0)
    {
      i--;
      if (aOldKeys[i] != FREE_KEY)
        put (aOldKeys[i], aOldValues[i]);
    }
  }

  private int _getNextIndex (final int currentIndex)
  {
    return (currentIndex + 1) & m_nMask;
  }

  private int _shiftKeys (final int nPos)
  {
    // Shift entries with the same hash.
    int pos = nPos;
    final int [] keys = m_aKeys;
    while (true)
    {
      final int last = pos;
      pos = _getNextIndex (pos);
      int k;
      while (true)
      {
        k = keys[pos];
        if (k == FREE_KEY)
        {
          keys[last] = FREE_KEY;
          m_aValues[last] = m_aNoValue;
          return last;
        }
        // calculate the starting slot for the current key
        final int slot = MapHelper.phiMix (k) & m_nMask;
        if (last <= pos ? last >= slot || slot > pos : last >= slot && slot > pos)
          break;
        pos = _getNextIndex (pos);
      }
      keys[last] = k;
      m_aValues[last] = m_aValues[pos];
    }
  }

  /**
   * Find key position in the map.
   *
   * @param key
   *        Key to look for
   * @return Key position or -1 if not found
   */
  @CheckForSigned
  private int _getReadIndex (final int key)
  {
    int idx = MapHelper.phiMix (key) & m_nMask;
    if (m_aKeys[idx] == key)
    {
      // we check FREE prior to this call
      return idx;
    }
    if (m_aKeys[idx] == FREE_KEY)
    {
      // end of chain already
      return -1;
    }
    final int startIdx = idx;
    while ((idx = _getNextIndex (idx)) != startIdx)
    {
      if (m_aKeys[idx] == FREE_KEY)
        return -1;
      if (m_aKeys[idx] == key)
        return idx;
    }
    return -1;
  }

  /**
   * Find an index of a cell which should be updated by 'put' operation. It can
   * be: 1) a cell with a given key 2) first free cell in the chain
   *
   * @param key
   *        Key to look for
   * @return Index of a cell to be updated by a 'put' operation
   */
  @CheckForSigned
  private int _getPutIndex (final int key)
  {
    final int readIdx = _getReadIndex (key);
    if (readIdx >= 0)
      return readIdx;
    // key not found, find insertion point
    final int startIdx = MapHelper.phiMix (key) & m_nMask;
    if (m_aKeys[startIdx] == FREE_KEY)
      return startIdx;
    int idx = startIdx;
    while (m_aKeys[idx] != FREE_KEY)
    {
      idx = _getNextIndex (idx);
      if (idx == startIdx)
        return -1;
    }
    return idx;
  }

  public static interface IConsumer <T>
  {
    void accept (int nKey, T aValue);
  }

  public void forEach (@Nonnull final IConsumer <T> aConsumer)
  {
    if (m_bHasFreeKey)
      aConsumer.accept (FREE_KEY, m_aFreeValue);
    final int nLen = m_aKeys.length;
    for (int i = 0; i < nLen; ++i)
    {
      final int nKey = m_aKeys[i];
      if (nKey != FREE_KEY)
      {
        final T aValue = m_aValues[i];
        if (!EqualsHelper.identityEqual (aValue, m_aNoValue))
          aConsumer.accept (nKey, aValue);
      }
    }
  }
}