/*
 * Copyright (c) 2002 - 2006 IBM Corporation.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 */
package com.ibm.wala.util.intset;

import com.ibm.wala.util.debug.Assertions;
import com.ibm.wala.util.debug.UnimplementedError;
import org.jspecify.annotations.NullUnmarked;

/**
 * A sparse ordered, mutable duplicate-free, fully-encapsulated set of longs. Instances are not
 * canonical, except for EMPTY.
 *
 * <p>This implementation will be inefficient if these sets get large.
 *
 * <p>TODO: even for small sets, we probably want to work on this to reduce the allocation activity.
 */
public final class MutableSparseLongSet extends SparseLongSet implements MutableLongSet {

  /** If forced to grow the backing array .. then by how much */
  private static final float EXPANSION_FACTOR = 1.5f;

  /** Default initial size for a backing array with one element */
  private static final int INITIAL_NONEMPTY_SIZE = 2;

  public static MutableSparseLongSet make(LongSet set) throws UnimplementedError {
    if (!(set instanceof SparseLongSet)) {
      Assertions.UNREACHABLE("implement me");
    }
    return new MutableSparseLongSet(set);
  }

  public static MutableSparseLongSet createMutableSparseLongSet(int initialCapacity) {
    if (initialCapacity < 0) {
      throw new IllegalArgumentException("illegal initialCapacity: " + initialCapacity);
    }
    return new MutableSparseLongSet(initialCapacity);
  }

  private MutableSparseLongSet(LongSet set) {
    super();
    copySet(set);
  }

  public MutableSparseLongSet(long[] backingStore) {
    super(backingStore);
  }

  /** Create an empty set with a non-zero capacity */
  private MutableSparseLongSet(int initialCapacity) {
    super(new long[initialCapacity]);
    size = 0;
  }

  public MutableSparseLongSet() {
    super();
  }

  /** */
  @NullUnmarked
  @Override
  public void remove(long value) {
    if (elements != null) {
      int remove;
      for (remove = 0; remove < size; remove++) {
        if (elements[remove] >= value) {
          break;
        }
      }
      if (remove == size) {
        return;
      }
      if (elements[remove] == value) {
        if (size == 1) {
          elements = null;
          size = 0;
        } else {
          if (remove < size) {
            System.arraycopy(elements, remove + 1, elements, remove, size - remove - 1);
          }
          size--;
        }
      }
    }
  }

  /** @return true iff this value changes */
  @Override
  public boolean add(long value) {
    if (value < 0) {
      throw new IllegalArgumentException("illegal value: " + value);
    }
    if (elements == null) {
      elements = new long[INITIAL_NONEMPTY_SIZE];
      size = 1;
      elements[0] = value;
    } else {
      int insert;
      if (size == 0 || value > max()) {
        insert = size;
      } else if (value == max()) {
        return false;
      } else {
        for (insert = 0; insert < size; insert++) {
          if (elements[insert] >= value) {
            break;
          }
        }
      }
      if (insert < size && elements[insert] == value) {
        return false;
      }
      if (size < elements.length - 1) {
        // there's space in the backing elements array. Use it.
        if (size != insert) {
          System.arraycopy(elements, insert, elements, insert + 1, size - insert);
        }
        size++;
        elements[insert] = value;
      } else {
        // no space left. expand the backing array.
        float newExtent = elements.length * EXPANSION_FACTOR + 1;
        long[] tmp = new long[(int) newExtent];
        System.arraycopy(elements, 0, tmp, 0, insert);
        if (size != insert) {
          System.arraycopy(elements, insert, tmp, insert + 1, size - insert);
        }
        tmp[insert] = value;
        size++;
        elements = tmp;
      }
    }
    return true;
  }

  /**
   * @throws UnimplementedError if not ( that instanceof com.ibm.wala.util.intset.SparseLongSet )
   */
  @NullUnmarked
  @Override
  public void copySet(LongSet that) throws UnimplementedError {
    if (that instanceof SparseLongSet) {
      SparseLongSet set = (SparseLongSet) that;
      if (set.elements != null) {
        elements = set.elements.clone();
        size = set.size;
      } else {
        elements = null;
        size = 0;
      }
    } else {
      Assertions.UNREACHABLE();
    }
  }

  @Override
  public void intersectWith(LongSet set) {
    if (set == null) {
      throw new IllegalArgumentException("null set");
    }
    if (set instanceof SparseLongSet) {
      intersectWith((SparseLongSet) set);
    } else {
      int j = 0;
      for (int i = 0; i < size; i++) if (set.contains(elements[i])) elements[j++] = elements[i];

      size = j;
    }
  }

  @NullUnmarked
  public void intersectWith(SparseLongSet set) {
    if (set == null) {
      throw new IllegalArgumentException("null set");
    }
    SparseLongSet that = set;
    if (this.isEmpty()) {
      return;
    } else if (that.isEmpty()) {
      elements = null;
      size = 0;
      return;
    } else if (this.equals(that)) {
      return;
    }

    // some simple optimizations
    if (size == 1) {
      if (that.contains(elements[0])) {
        return;
      } else {
        elements = null;
        size = 0;
        return;
      }
    }
    if (that.size == 1) {
      if (contains(that.elements[0])) {
        if (size > INITIAL_NONEMPTY_SIZE) {
          elements = new long[INITIAL_NONEMPTY_SIZE];
        }
        size = 1;
        elements[0] = that.elements[0];
        return;
      } else {
        elements = null;
        size = 0;
        return;
      }
    }

    long[] ar = this.elements;
    int ai = 0;
    int al = size;
    long[] br = that.elements;
    int bi = 0;
    int bl = that.size;
    long[] cr = null; // allocate on demand
    int ci = 0;

    while (ai < al && bi < bl) {
      long cmp = (ar[ai] - br[bi]);

      // (accept element only on a match)
      if (cmp > 0) { // a greater
        bi++;
      } else if (cmp < 0) { // b greater
        ai++;
      } else {
        if (cr == null) {
          cr = new long[al]; // allocate enough (i.e. too much)
        }
        cr[ci++] = ar[ai];
        ai++;
        bi++;
      }
    }

    // now compact cr to 'just enough'
    size = ci;
    elements = cr;
    return;
  }

  /**
   * Add all elements from another int set.
   *
   * @return true iff this set changes
   * @throws UnimplementedError if not ( set instanceof com.ibm.wala.util.intset.SparseLongSet )
   */
  @Override
  public boolean addAll(LongSet set) throws UnimplementedError {
    if (set instanceof SparseLongSet) {
      return addAll((SparseLongSet) set);
    } else {
      Assertions.UNREACHABLE();
      return false;
    }
  }

  /**
   * Add all elements from another int set.
   *
   * @return true iff this set changes
   */
  public boolean addAll(SparseLongSet that) {
    if (that == null) {
      throw new IllegalArgumentException("null that");
    }
    if (this.isEmpty()) {
      copySet(that);
      return !that.isEmpty();
    } else if (that.isEmpty()) {
      return false;
    } else if (this.equals(that)) {
      return false;
    }

    // common-case optimization
    if (that.size == 1) {
      boolean result = add(that.elements[0]);
      return result;
    }

    long[] br = that.elements;
    int bl = that.size();

    return addAll(br, bl);
  }

  private boolean addAll(long[] that, int thatSize) {
    long[] ar = this.elements;
    int ai = 0;
    final int al = size();
    int bi = 0;

    // invariant: assume cr has same value as ar until cr is allocated.
    // we allocate cr lazily when we discover cr != ar.
    long[] cr = null;
    int ci = 0;

    while (ai < al && bi < thatSize) {
      long cmp = (ar[ai] - that[bi]);

      // (always accept element)
      if (cmp > 0) { // a greater
        if (cr == null) {
          cr = new long[al + thatSize];
          System.arraycopy(ar, 0, cr, 0, ci);
        }
        cr[ci++] = that[bi++];
      } else if (cmp < 0) { // b greater
        if (cr != null) {
          cr[ci] = ar[ai];
        }
        ci++;
        ai++;
      } else {
        if (cr != null) {
          cr[ci] = ar[ai]; // (same: use a)
        }
        ci++;
        ai++;
        bi++;
      }
    }

    // append tail if any (at most one of a or b has tail)
    if (ai < al) {
      int tail = al - ai;
      if (cr != null) {
        System.arraycopy(ar, ai, cr, ci, tail);
      }
      ci += tail;
    } else if (bi < thatSize) {
      int tail = thatSize - bi;
      if (cr == null) {
        cr = new long[al + thatSize];
        System.arraycopy(ar, 0, cr, 0, ci);
      }
      System.arraycopy(that, bi, cr, ci, tail);
      ci += tail;
    }

    assert ci > 0;

    elements = (cr == null) ? ar : cr;
    size = ci;
    return (al != size);
  }
}
