package com.bindroid.trackable;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Stack;

/**
 * A {@link List} implementation that implements Trackable on all of its methods, notifying
 * {@link Tracker}s whenever a change to the list occurs.
 * 
 * @param <T>
 *          The type of object in the List.
 */
public class TrackableCollection<T> extends Trackable implements List<T> {
  private List<T> backingStore;
  private List<Long> ids;
  private long curId;
  private Stack<Long> returnedIds;
  private Trackable trackable = this;

  /**
   * Constructs a new, empty, {@link ArrayList}-backed ObservableCollection.
   */
  public TrackableCollection() {
    this(new ArrayList<T>());
  }

  /**
   * Constructs a new ObservableCollection backed by the given {@link List} implementation.
   * 
   * @param backingStore
   *          The list implementation for the ObservableCollection.
   */
  public TrackableCollection(List<T> backingStore) {
    this.backingStore = backingStore;
    this.ids = new ArrayList<Long>();
    this.returnedIds = new Stack<Long>();
    for (int x = 0; x < backingStore.size(); x++) {
      this.ids.add(this.getNewId());
    }
  }

  /**
   * A utility function for cloning an ObservableCollection. Object identifiers will remain the same
   * in the cloned collection.
   * 
   * @param toClone
   *          The ObservableCollection to clone.
   */
  public TrackableCollection(TrackableCollection<T> toClone) {
    this.backingStore = new ArrayList<T>(toClone.backingStore);
    this.ids = new ArrayList<Long>(toClone.ids);
    this.returnedIds = new Stack<Long>();
  }

  @Override
  public void add(int location, T object) {
    this.backingStore.add(location, object);
    this.ids.add(location, this.getNewId());
    this.trackable.updateTrackers();
  }

  @Override
  public boolean add(T object) {
    boolean result = this.backingStore.add(object);
    this.ids.add(this.getNewId());
    if (result) {
      this.trackable.updateTrackers();
    }
    return result;
  }

  @Override
  public boolean addAll(Collection<? extends T> arg0) {
    boolean result = this.backingStore.addAll(arg0);
    for (@SuppressWarnings("unused")
    Object item : arg0) {
      this.ids.add(this.getNewId());
    }
    if (result) {
      this.trackable.updateTrackers();
    }
    return result;
  }

  @Override
  public boolean addAll(int arg0, Collection<? extends T> arg1) {
    boolean result = this.backingStore.addAll(arg0, arg1);
    for (@SuppressWarnings("unused")
    Object item : arg1) {
      this.ids.add(this.getNewId());
    }
    if (result) {
      this.trackable.updateTrackers();
    }
    return result;
  }

  @Override
  public void clear() {
    this.backingStore.clear();
    this.ids.clear();
    this.returnedIds.clear();
    this.curId = 0;
    this.trackable.updateTrackers();
  }

  @Override
  public boolean contains(Object object) {
    this.trackable.track();
    return this.backingStore.contains(object);
  }

  @Override
  public boolean containsAll(Collection<?> arg0) {
    this.trackable.track();
    return this.backingStore.containsAll(arg0);
  }

  @Override
  public T get(int location) {
    this.trackable.track();
    return this.backingStore.get(location);
  }

  /**
   * Gets a list-unique identifier associated with the object at the given index. This is useful for
   * UI to ensure that UI can be reused when the collection changes.
   * 
   * @param index
   *          The index for which an identifier should be retrieved.
   * @return The list-unique identifier for the object at the given index.
   */
  public long getId(int index) {
    return this.ids.get(index);
  }

  private long getNewId() {
    if (this.returnedIds.isEmpty()) {
      return this.curId++;
    }
    return this.returnedIds.pop();
  }

  @Override
  public int indexOf(Object object) {
    this.trackable.track();
    return this.backingStore.indexOf(object);
  }

  @Override
  public boolean isEmpty() {
    this.trackable.track();
    return this.backingStore.isEmpty();
  }

  @Override
  public Iterator<T> iterator() {
    this.trackable.track();
    return this.backingStore.iterator();
  }

  @Override
  public int lastIndexOf(Object object) {
    this.trackable.track();
    return this.backingStore.lastIndexOf(object);
  }

  @Override
  public ListIterator<T> listIterator() {
    this.trackable.track();
    return this.backingStore.listIterator();
  }

  @Override
  public ListIterator<T> listIterator(int location) {
    this.trackable.track();
    return this.backingStore.listIterator(location);
  }

  @Override
  public T remove(int location) {
    T result = this.backingStore.remove(location);
    this.returnId(this.ids.remove(location));
    this.trackable.updateTrackers();
    return result;
  }

  @Override
  public boolean remove(Object object) {
    int index = this.backingStore.indexOf(object);
    boolean result = index >= 0;
    if (result) {
      this.remove(index);
    }
    return result;
  }

  @Override
  public boolean removeAll(Collection<?> arg0) {
    HashSet<?> items = new HashSet<Object>(arg0);
    ArrayList<Long> toRemove = new ArrayList<Long>();
    for (int x = 0; x < this.backingStore.size(); x++) {
      if (items.contains(this.backingStore.get(x))) {
        toRemove.add(this.ids.get(x));
        this.returnId(this.ids.get(x));
      }
    }
    boolean result = this.backingStore.removeAll(arg0);
    this.ids.removeAll(toRemove);
    if (result) {
      this.trackable.updateTrackers();
    }
    return result;
  }

  @Override
  public boolean retainAll(Collection<?> arg0) {
    HashSet<?> items = new HashSet<Object>(arg0);
    ArrayList<Long> toRemove = new ArrayList<Long>();
    for (int x = 0; x < this.backingStore.size(); x++) {
      if (!items.contains(this.backingStore.get(x))) {
        toRemove.add(this.ids.get(x));
        this.returnId(this.ids.get(x));
      }
    }
    boolean result = this.backingStore.retainAll(arg0);
    this.ids.removeAll(toRemove);
    if (result) {
      this.trackable.updateTrackers();
    }
    return result;
  }

  private void returnId(long id) {
    this.returnedIds.push(id);
  }

  @Override
  public T set(int location, T object) {
    T result = this.backingStore.set(location, object);
    this.trackable.updateTrackers();
    return result;
  }

  @Override
  public int size() {
    this.trackable.track();
    return this.backingStore.size();
  }

  @Override
  public List<T> subList(int start, int end) {
    this.trackable.track();
    return new TrackableCollection<T>(this.backingStore.subList(start, end));
  }

  @Override
  public Object[] toArray() {
    this.trackable.track();
    return this.backingStore.toArray();
  }

  @Override
  public <T1> T1[] toArray(T1[] array) {
    this.trackable.track();
    return this.backingStore.toArray(array);
  }

}