// Copyright 2023 The Bazel Authors. All rights reserved.
//
// 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 net.starlark.java.eval;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import java.util.Arrays;
import java.util.Collection;
import javax.annotation.Nullable;

/** A mutable implementation of StarlarkList. */
final class MutableStarlarkList<E> extends StarlarkList<E> {

  // The implementation strategy is similar to ArrayList,
  // but without the extra indirection of using ArrayList.

  // elems[0:size] holds the logical elements, and elems[size:] are not used.
  // elems.getClass() == Object[].class. This is necessary to avoid ArrayStoreException.
  private int size;
  private int iteratorCount; // number of active iterators (unused once frozen)
  Object[] elems = StarlarkList.EMPTY_ARRAY; // elems[i] == null  iff  i >= size

  /** Final except for {@link #unsafeShallowFreeze}; must not be modified any other way. */
  private Mutability mutability;

  MutableStarlarkList(@Nullable Mutability mutability, Object[] elems, int size) {
    Preconditions.checkArgument(elems.getClass() == Object[].class);
    this.elems = elems;
    this.size = size;
    this.mutability = mutability == null ? Mutability.IMMUTABLE : mutability;
  }

  @Override
  public boolean isImmutable() {
    return mutability().isFrozen();
  }

  @Override
  public boolean updateIteratorCount(int delta) {
    if (mutability().isFrozen()) {
      return false;
    }
    if (delta > 0) {
      iteratorCount++;
    } else if (delta < 0) {
      iteratorCount--;
    }
    return iteratorCount > 0;
  }

  @Override
  public Mutability mutability() {
    return mutability;
  }

  @Override
  public void unsafeShallowFreeze() {
    Mutability.Freezable.checkUnsafeShallowFreezePrecondition(this);
    this.mutability = Mutability.IMMUTABLE;
  }

  @Override
  public ImmutableList<E> getImmutableList() {
    // Optimization: a frozen array needn't be copied.
    // If the entire array is full, we can wrap it directly.
    if (elems.length == size && mutability().isFrozen()) {
      return Tuple.wrapImmutable(elems);
    }

    return ImmutableList.copyOf(this);
  }

  @Override
  @SuppressWarnings("unchecked")
  public E get(int i) {
    Preconditions.checkElementIndex(i, size);
    return (E) elems[i]; // unchecked
  }

  @Override
  public int size() {
    return size;
  }

  @Override
  public boolean contains(Object o) {
    // StarlarkList contains only valid Starlark objects (which are non-null)
    if (o == null) {
      return false;
    }
    for (int i = 0; i < size; i++) {
      Object elem = elems[i];
      if (o.equals(elem)) {
        return true;
      }
    }
    return false;
  }

  // Postcondition: elems.length >= mincap.
  private void grow(int mincap) {
    int oldcap = elems.length;
    if (oldcap < mincap) {
      int newcap = oldcap + (oldcap >> 1); // grow by at least 50%
      if (newcap < mincap) {
        newcap = mincap;
      }
      elems = Arrays.copyOf(elems, newcap);
    }
  }

  // Grow capacity enough to insert given number of elements
  private void growAdditional(int additional) throws EvalException {
    int mincap = size + additional;
    if (mincap < 0 || mincap > MAX_ALLOC) {
      throw Starlark.errorf("excessive capacity requested (%d + %d elements)", size, additional);
    }
    grow(mincap);
  }

  @Override
  public void addElement(E element) throws EvalException {
    Starlark.checkMutable(this);
    growAdditional(1);
    elems[size++] = element;
  }

  @Override
  public void addElementAt(int index, E element) throws EvalException {
    Starlark.checkMutable(this);
    growAdditional(1);
    System.arraycopy(elems, index, elems, index + 1, size - index);
    elems[index] = element;
    size++;
  }

  @Override
  public void addElements(Iterable<? extends E> elements) throws EvalException {
    Starlark.checkMutable(this);
    if (elements instanceof MutableStarlarkList) {
      MutableStarlarkList<?> that = (MutableStarlarkList) elements;
      // (safe even if this == that)
      growAdditional(that.size);
      System.arraycopy(that.elems, 0, this.elems, this.size, that.size);
      this.size += that.size;
    } else if (elements instanceof Collection) {
      // collection of known size
      Collection<?> that = (Collection) elements;
      growAdditional(that.size());
      for (Object x : that) {
        elems[size++] = x;
      }
    } else {
      // iterable
      for (Object x : elements) {
        growAdditional(1);
        elems[size++] = x;
      }
    }
  }

  @Override
  public void removeElementAt(int index) throws EvalException {
    Starlark.checkMutable(this);
    int n = size - index - 1;
    if (n > 0) {
      System.arraycopy(elems, index + 1, elems, index, n);
    }
    elems[--size] = null; // aid GC
  }

  @Override
  public void setElementAt(int index, E value) throws EvalException {
    Starlark.checkMutable(this);
    Preconditions.checkArgument(index < size);
    elems[index] = value;
  }

  @Override
  public void clearElements() throws EvalException {
    Starlark.checkMutable(this);
    for (int i = 0; i < size; i++) {
      elems[i] = null; // aid GC
    }
    size = 0;
  }

  /** Returns a new array of class Object[] containing the list elements. */
  @Override
  public Object[] toArray() {
    return size != 0 ? Arrays.copyOf(elems, size, Object[].class) : EMPTY_ARRAY;
  }

  @SuppressWarnings("unchecked")
  @Override
  public <T> T[] toArray(T[] a) {
    if (a.length < size) {
      return (T[]) Arrays.copyOf(elems, size, a.getClass());
    } else {
      System.arraycopy(elems, 0, a, 0, size);
      Arrays.fill(a, size, a.length, null);
      return a;
    }
  }

  @Override
  Object[] elems() {
    return elems;
  }

  @Override
  public StarlarkList<E> unsafeOptimizeMemoryLayout() {
    Preconditions.checkState(mutability.isFrozen());
    // Canonicalize our Mutability, on the off-chance the old one may be freed.
    mutability = Mutability.IMMUTABLE;
    if (elems.length > size) {
      // shrink the Object array
      elems = Arrays.copyOf(elems, size);
    }
    // Give the caller an immutable specialization of StarlarkList.
    return wrap(mutability, elems);
  }
}
