/*
 * Copyright 2010 Google Inc.
 *
 * 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.google.gwt.view.client;

import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.NoSuchElementException;

/**
 * A concrete subclass of {@link AbstractDataProvider} that is backed by an
 * in-memory list.
 * 
 * <p>
 * Modifications (inserts, removes, sets, etc.) to the list returned by
 * {@link #getList()} will be reflected in the model. However, mutations to the
 * items contained within the list will NOT be reflected in the model. You must
 * call {@link List#set(int, Object)} to update the item within the list and
 * push the change to the display, or call {@link #refresh()} to push all rows
 * to the displays. {@link List#set(int, Object)} performs better because it
 * allows the data provider to push only those rows which have changed, and
 * usually allows the display to re-render only a subset of the rows.
 * </p>
 * 
 * <p>
 * <h3>Example</h3> {@example
 * com.google.gwt.examples.view.ListDataProviderExample}
 * </p>
 * 
 * @param <T> the data type of the list
 */
public class ListDataProvider<T> extends AbstractDataProvider<T> {

  /**
   * A wrapper around a list that updates the model on any change.
   */
  private class ListWrapper implements List<T> {

    /**
     * A wrapped ListIterator.
     */
    private final class WrappedListIterator implements ListIterator<T> {

      /**
       * The error message when {@link #add(Object)} or {@link #remove()} is
       * called more than once per call to {@link #next()} or
       * {@link #previous()}.
       */
      private static final String IMPERMEABLE_EXCEPTION =
          "Cannot call add/remove more than once per call to next/previous.";

      /**
       * The index of the object that will be returned by {@link #next()}.
       */
      private int i = 0;

      /**
       * The index of the last object accessed through {@link #next()} or
       * {@link #previous()}.
       */
      private int last = -1;

      private WrappedListIterator() {
      }

      private WrappedListIterator(int start) {
        int size = ListWrapper.this.size();
        if (start < 0 || start > size) {
          throw new IndexOutOfBoundsException(
              "Index: " + start + ", Size: " + size);
        }
        i = start;
      }

      @Override
      public void add(T o) {
        if (last < 0) {
          throw new IllegalStateException(IMPERMEABLE_EXCEPTION);
        }
        ListWrapper.this.add(i++, o);
        last = -1;
      }

      @Override
      public boolean hasNext() {
        return i < ListWrapper.this.size();
      }

      @Override
      public boolean hasPrevious() {
        return i > 0;
      }

      @Override
      public T next() {
        if (!hasNext()) {
          throw new NoSuchElementException();
        }
        return ListWrapper.this.get(last = i++);
      }

      @Override
      public int nextIndex() {
        return i;
      }

      @Override
      public T previous() {
        if (!hasPrevious()) {
          throw new NoSuchElementException();
        }
        return ListWrapper.this.get(last = --i);
      }

      @Override
      public int previousIndex() {
        return i - 1;
      }

      @Override
      public void remove() {
        if (last < 0) {
          throw new IllegalStateException(IMPERMEABLE_EXCEPTION);
        }
        ListWrapper.this.remove(last);
        i = last;
        last = -1;
      }

      @Override
      public void set(T o) {
        if (last == -1) {
          throw new IllegalStateException();
        }
        ListWrapper.this.set(last, o);
      }
    }

    /**
     * The current size of the list.
     */
    private int curSize = 0;

    /**
     * The delegate wrapper.
     */
    private final ListWrapper delegate;

    /**
     * Set to true if the pending flush has been canceled.
     */
    private boolean flushCancelled;

    /**
     * We wait until the end of the current event loop before flushing changes
     * so that we don't spam the displays. This also allows users to clear and
     * replace all of the data without forcing the display back to page 0.
     */
    private ScheduledCommand flushCommand = new ScheduledCommand() {
      @Override
      public void execute() {
        flushPending = false;
        if (flushCancelled) {
          flushCancelled = false;
          return;
        }
        flushNow();
      }
    };

    /**
     * Set to true if a flush is pending.
     */
    private boolean flushPending;

    /**
     * The list that backs the wrapper.
     */
    private List<T> list;

    /**
     * If this is a sublist, the offset it the index relative to the main list.
     */
    private final int offset;

    /**
     * If modified is true, the smallest modified index.
     */
    private int maxModified = Integer.MIN_VALUE;

    /**
     * If modified is true, one past the largest modified index.
     */
    private int minModified = Integer.MAX_VALUE;

    /**
     * True if the list data has been modified.
     */
    private boolean modified;

    public ListWrapper(List<T> list) {
      this(list, null, 0);

      // Initialize the data size based on the size of the input list.
      updateRowCount(list.size(), true);
    }

    /**
     * Construct a new {@link ListWrapper} that delegates flush calls to the
     * specified delegate.
     *
     * @param list the list to wrap
     * @param delegate the delegate
     * @param offset the offset of this list
     */
    private ListWrapper(List<T> list, ListWrapper delegate, int offset) {
      this.list = list;
      this.delegate = delegate;
      this.offset = offset;
    }

    @Override
    public void add(int index, T element) {
      try {
        list.add(index, element);
        minModified = Math.min(minModified, index);
        maxModified = size();
        modified = true;
        flush();
      } catch (IndexOutOfBoundsException e) {
        throw new IndexOutOfBoundsException(e.getMessage());
      }
    }

    @Override
    public boolean add(T e) {
      boolean toRet = list.add(e);
      minModified = Math.min(minModified, size() - 1);
      maxModified = size();
      modified = true;
      flush();
      return toRet;
    }

    @Override
    public boolean addAll(Collection<? extends T> c) {
      minModified = Math.min(minModified, size());
      boolean toRet = list.addAll(c);
      maxModified = size();
      modified = true;
      flush();
      return toRet;
    }

    @Override
    public boolean addAll(int index, Collection<? extends T> c) {
      try {
        boolean toRet = list.addAll(index, c);
        minModified = Math.min(minModified, index);
        maxModified = size();
        modified = true;
        flush();
        return toRet;
      } catch (IndexOutOfBoundsException e) {
        throw new IndexOutOfBoundsException(e.getMessage());
      }
    }

    @Override
    public void clear() {
      list.clear();
      minModified = maxModified = 0;
      modified = true;
      flush();
    }

    @Override
    public boolean contains(Object o) {
      return list.contains(o);
    }

    @Override
    public boolean containsAll(Collection<?> c) {
      return list.containsAll(c);
    }

    @Override
    public boolean equals(Object o) {
      return list.equals(o);
    }

    @Override
    public T get(int index) {
      return list.get(index);
    }

    @Override
    public int hashCode() {
      return list.hashCode();
    }

    @Override
    public int indexOf(Object o) {
      return list.indexOf(o);
    }

    @Override
    public boolean isEmpty() {
      return list.isEmpty();
    }

    @Override
    public Iterator<T> iterator() {
      return listIterator();
    }

    @Override
    public int lastIndexOf(Object o) {
      return list.lastIndexOf(o);
    }

    @Override
    public ListIterator<T> listIterator() {
      return new WrappedListIterator();
    }

    @Override
    public ListIterator<T> listIterator(int index) {
      return new WrappedListIterator(index);
    }

    @Override
    public T remove(int index) {
      try {
        T toRet = list.remove(index);
        minModified = Math.min(minModified, index);
        maxModified = size();
        modified = true;
        flush();
        return toRet;
      } catch (IndexOutOfBoundsException e) {
        throw new IndexOutOfBoundsException(e.getMessage());
      }
    }

    @Override
    public boolean remove(Object o) {
      int index = indexOf(o);
      if (index == -1) {
        return false;
      }
      remove(index);
      return true;
    }

    @Override
    public boolean removeAll(Collection<?> c) {
      boolean toRet = list.removeAll(c);
      minModified = 0;
      maxModified = size();
      modified = true;
      flush();
      return toRet;
    }

    @Override
    public boolean retainAll(Collection<?> c) {
      boolean toRet = list.retainAll(c);
      minModified = 0;
      maxModified = size();
      modified = true;
      flush();
      return toRet;
    }

    @Override
    public T set(int index, T element) {
      T toRet = list.set(index, element);
      minModified = Math.min(minModified, index);
      maxModified = Math.max(maxModified, index + 1);
      modified = true;
      flush();
      return toRet;
    }

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

    @Override
    public List<T> subList(int fromIndex, int toIndex) {
      return new ListWrapper(list.subList(fromIndex, toIndex), this, fromIndex);
    }

    @Override
    public Object[] toArray() {
      return list.toArray();
    }

    @Override
    public <C> C[] toArray(C[] a) {
      return list.toArray(a);
    }

    /**
     * Flush the data to the model.
     */
    private void flush() {
      // Defer to the delegate.
      if (delegate != null) {
        delegate.minModified = Math.min(
            minModified + offset, delegate.minModified);
        delegate.maxModified = Math.max(
            maxModified + offset, delegate.maxModified);
        delegate.modified = modified || delegate.modified;
        delegate.flush();
        return;
      }

      flushCancelled = false;
      if (!flushPending) {
        flushPending = true;
        Scheduler.get().scheduleFinally(flushCommand);
      }
    }

    /**
     * Flush pending list changes to the displays. By default,
     */
    private void flushNow() {
      // Cancel any pending flush command.
      if (flushPending) {
        flushCancelled = true;
      }

      // Early exit if this list has been replaced in the data provider.
      if (listWrapper != this) {
        return;
      }

      int newSize = list.size();
      if (curSize != newSize) {
        curSize = newSize;
        updateRowCount(curSize, true);
      }

      if (modified) {
        updateRowData(minModified, list.subList(minModified, maxModified));
        modified = false;
      }
      minModified = Integer.MAX_VALUE;
      maxModified = Integer.MIN_VALUE;
    }
  }

  /**
   * The wrapper around the actual list.
   */
  private ListWrapper listWrapper;

  /**
   * Creates an empty model.
   */
  public ListDataProvider() {
    this(new ArrayList<T>(), null);
  }

  /**
   * Creates a list model that wraps the given list.
   * 
   * <p>
   * The wrapped list should no longer be modified as the data provider cannot
   * detect changes to the wrapped list. Instead, call {@link #getList()} to
   * retrieve a wrapper that can be modified and will correctly forward changes
   * to displays.
   * 
   * @param listToWrap the List to be wrapped
   */
  public ListDataProvider(List<T> listToWrap) {
    this(listToWrap, null);
  }

  /**
   * Creates an empty list model that wraps the given collection.
   *
   * @param keyProvider an instance of ProvidesKey<T>, or null if the record
   *        object should act as its own key
   */
  public ListDataProvider(ProvidesKey<T> keyProvider) {
    this(new ArrayList<T>(), keyProvider);
  }

  /**
   * Creates a list model that wraps the given list.
   * 
   * <p>
   * The wrapped list should no longer be modified as the data provider cannot
   * detect changes to the wrapped list. Instead, call {@link #getList()} to
   * retrieve a wrapper that can be modified and will correctly forward changes
   * to displays.
   * 
   * @param listToWrap the List to be wrapped
   * @param keyProvider an instance of ProvidesKey<T>, or null if the record
   *        object should act as its own key
   */
  public ListDataProvider(List<T> listToWrap, ProvidesKey<T> keyProvider) {
    super(keyProvider);
    listWrapper = new ListWrapper(listToWrap);
  }

  /**
   * Flush pending list changes to the displays. By default, displays are
   * informed of modifications to the underlying list at the end of the current
   * event loop, which makes it possible to perform multiple operations
   * synchronously without repeatedly refreshing the displays. This method can
   * be called to flush the changes immediately instead of waiting until the end
   * of the current event loop.
   */
  public void flush() {
    listWrapper.flushNow();
  }

  /**
   * Get the list that backs this model. Changes to the list will be reflected
   * in the model.
   * 
   * <p>
   * NOTE: Mutations to the items contained within the list will NOT be
   * reflected in the model. You must call {@link List#set(int, Object)} to
   * update the item within the list and push the change to the display, or call
   * {@link #refresh()} to push all rows to the displays.
   * {@link List#set(int, Object)} performs better because it allows the data
   * provider to push only those rows which have changed, and usually allows the
   * display to re-render only a subset of the rows.
   * 
   * @return the list
   * 
   * @see #setList(List)
   */
  public List<T> getList() {
    return listWrapper;
  }

  /**
   * Refresh all of the displays listening to this adapter.
   * 
   * <p>
   * Use {@link #refresh()} to push mutations to the underlying data items
   * contained within the list. The data provider cannot detect changes to data
   * objects within the list, so you must call this method if you modify items.
   * 
   * <p>
   * This is a shortcut for calling {@link List#set(int, Object)} on every item
   * that you modify, but note that calling {@link List#set(int, Object)}
   * performs better because the data provider knows which rows were modified
   * and can push only the modified rows the displays.
   */
  public void refresh() {
    updateRowData(0, listWrapper);
  }

  /**
   * Replace this model's list with the specified list.
   * 
   * <p>
   * The wrapped list should no longer be modified as the data provider cannot
   * detect changes to the wrapped list. Instead, call {@link #getList()} to
   * retrieve a wrapper that can be modified and will correctly forward changes
   * to displays.
   * 
   * @param listToWrap the model's new list
   * 
   * @see #getList()
   */
  public void setList(List<T> listToWrap) {
    listWrapper = new ListWrapper(listToWrap);
    listWrapper.minModified = 0;
    listWrapper.maxModified = listWrapper.size();
    listWrapper.modified = true;
    flush();
  }

  @Override
  protected void onRangeChanged(HasData<T> display) {
    int size = listWrapper.size();
    if (size > 0) {
      // Do not push data if the data set is empty.
      updateRowData(display, 0, listWrapper);
    }
  }
}
