package org.pm4j.core.pm.impl.pageable;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyVetoException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

import org.pm4j.common.modifications.ModificationHandler;
import org.pm4j.common.modifications.Modifications;
import org.pm4j.common.pageable.PageableCollection;
import org.pm4j.common.pageable.inmem.InMemCollectionImpl;
import org.pm4j.common.query.QueryOptions;
import org.pm4j.common.query.QueryParams;
import org.pm4j.common.selection.Selection;
import org.pm4j.common.selection.SelectionHandler;
import org.pm4j.common.util.beanproperty.PropertyChangeSupportedBase;
import org.pm4j.core.exception.PmRuntimeException;
import org.pm4j.core.pm.PmBean;
import org.pm4j.core.pm.PmEvent;
import org.pm4j.core.pm.PmEvent.ValueChangeKind;
import org.pm4j.core.pm.PmEventListener;
import org.pm4j.core.pm.PmObject;
import org.pm4j.core.pm.api.PmEventApi;
import org.pm4j.core.pm.api.PmFactoryApi;
import org.pm4j.core.pm.api.PmMessageApi;
import org.pm4j.core.pm.api.PmValidationApi;
import org.pm4j.core.pm.impl.BeanPmCacheUtil;

/**
 * A {@link PageableCollection} instance that provides {@link PmBean} instances in
 * front of a {@link PageableCollection} container that handles the corresponding
 * bean instances.
 *
 * @author olaf boede
 *
 * @param <T_PM>
 *          The kind of {@link PmBean} provided by this class.
 * @param <T_BEAN>
 *          The kind of corresponding bean, handled by the backing
 *          {@link PageableCollection} instance.
 */
public class PmBeanCollection<T_PM extends PmBean<T_BEAN>, T_BEAN> extends PropertyChangeSupportedBase implements PageableCollection<T_PM> {

  /** The collection type specific selection handler. */
  private PmBeanSelectionHandler<T_PM, T_BEAN> selectionHandler;
  private final PmBeanCollectionModificationHandler   modificationHandler;

  private final PmObject                              pmCtxt;
  private final PageableCollection<T_BEAN>           beanCollection;
  /** Type of item PM's the change handler should observe changes for. */
  private final Class<T_PM>                           itemPmClass;
  /** An internal helper that prevents to call the bean cache refresh operation to often. */
  private long idxOfLastPmBeanCacheUpdate = -1;

  /**
   * Creates a collection backed by the given {@link PageableCollection} of beans.
   *
   * @param pmCtxt
   *          The PM context used to create the PM's for the bean items.
   * @param pageableBeanCollection
   *          The collection of beans to represent by this collection of bean-PM's.
   */
  @SuppressWarnings("unchecked")
  public PmBeanCollection(final PmObject pmCtxt, Class<?> itemPmClass, PageableCollection<T_BEAN> pageableBeanCollection) {
    assert pmCtxt != null;
    assert pageableBeanCollection != null;
    assert itemPmClass != null;

    this.pmCtxt = pmCtxt;
    this.beanCollection = pageableBeanCollection;
    this.itemPmClass = (Class<T_PM>) itemPmClass;
    this.selectionHandler = new PmBeanSelectionHandler<T_PM, T_BEAN>(pmCtxt, pageableBeanCollection.getSelectionHandler());
    this.modificationHandler = new PmBeanCollectionModificationHandler();

    // Observe changes of the backing collection of beans and forward the events
    // using the corresponding PM values.
    beanCollection.addPropertyChangeListener(PageableCollection.EVENT_ITEM_ADD, new PropertyChangeListener() {
      @Override
      public void propertyChange(PropertyChangeEvent evt) {
        firePropertyChange(PageableCollection.EVENT_ITEM_ADD, null, PmFactoryApi.getPmForBean(pmCtxt, evt.getNewValue()));
        PmEventApi.firePmEvent(pmCtxt, PmEvent.VALUE_CHANGE, ValueChangeKind.ADD_ITEM);
      }
    });

    beanCollection.addPropertyChangeListener(PageableCollection.EVENT_ITEM_UPDATE, new PropertyChangeListener() {
      @Override
      public void propertyChange(PropertyChangeEvent evt) {
        // propagates the changed flag value change.
        firePropertyChange(PageableCollection.EVENT_ITEM_UPDATE, evt.getOldValue(), evt.getNewValue());
        PmEventApi.firePmEvent(pmCtxt, PmEvent.VALUE_CHANGE);
      }
    });

    beanCollection.addPropertyChangeListener(PageableCollection.EVENT_REMOVE_SELECTION, new PropertyChangeListener() {
      @Override
      public void propertyChange(PropertyChangeEvent evt) {
        Selection<T_PM> deletedItemSelection = new PmBeanSelection<T_PM, T_BEAN>(pmCtxt, (Selection<T_BEAN>) evt.getOldValue());
        firePropertyChange(PageableCollection.EVENT_REMOVE_SELECTION, deletedItemSelection, null);
        PmEventApi.firePmEvent(pmCtxt, PmEvent.VALUE_CHANGE, ValueChangeKind.DELETE_ITEM);
      }
    });

    beanCollection.addPropertyChangeListener(PageableCollection.PROP_PAGE_IDX, new PropertyChangeListener() {
      @Override
      public void propertyChange(PropertyChangeEvent evt) {
        firePropertyChange(PageableCollection.PROP_PAGE_IDX, evt.getOldValue(), evt.getNewValue());
        // TODO: check which kind of PM change event should be fired here.
      }
    });

    beanCollection.addPropertyChangeListener(PageableCollection.PROP_PAGE_SIZE, new PropertyChangeListener() {
      @Override
      public void propertyChange(PropertyChangeEvent evt) {
        firePropertyChange(PageableCollection.PROP_PAGE_SIZE, evt.getOldValue(), evt.getNewValue());
        // TODO: check which kind of PM change event should be fired here.
      }
    });
  }

  /**
   * Creates a collection backed by a {@link PageableListImpl}.
   *
   * @param pmCtxt
   *          The PM context used to create the PM's for the bean items.
   * @param beans
   *          The set of beans to handle. May be <code>null</code>.
   * @param queryOptions
   *          the optional query options.
   */
  public PmBeanCollection(PmObject pmCtxt, Class<T_PM> itemPmClass, Collection<T_BEAN> beans, QueryOptions queryOptions) {
    this(pmCtxt, itemPmClass,
         new InMemCollectionImpl<T_BEAN>(
             beans,
             queryOptions)
        );
  }

  /**
   * Creates a collection backed by a {@link InMemCollectionImpl} with no query constraints.
   *
   * @param pmCtxt
   *          The PM context used to create the PM's for the bean items.
   * @param beans
   *          The set of beans to handle.
   */
  public PmBeanCollection(PmObject pmCtxt, Class<T_PM> itemPmClass, Collection<T_BEAN> beans) {
    this(pmCtxt, itemPmClass, beans, new QueryOptions());
  }

  @Override
  public QueryParams getQueryParams() {
    return beanCollection.getQueryParams();
  }

  @Override
  public QueryOptions getQueryOptions() {
    return beanCollection.getQueryOptions();
  }

  @SuppressWarnings("unchecked")
  @Override
  public List<T_PM> getItemsOnPage() {
    // On page switch: ensure that the PM's for un-modified beans are re-generated.
    // Otherwise the UI could present PM's with outdated beans.
    // XXX olaf: Shouldn't be necessary if the collection would observe page switches of the
    // base collection.
    if (idxOfLastPmBeanCacheUpdate != getPageIdx()) {
      releaseUnModifiedPmBeanCacheItems();
      idxOfLastPmBeanCacheUpdate = getPageIdx();
    }

    return (List<T_PM>) PmFactoryApi.getPmListForBeans(pmCtxt, beanCollection.getItemsOnPage(), false);
  }

  @Override
  public int getPageSize() {
    return beanCollection.getPageSize();
  }

  @Override
  public void setPageSize(int newSize) {
    beanCollection.setPageSize(newSize);
  }

  @Override
  public long getPageIdx() {
    return beanCollection.getPageIdx();
  }

  @Override
  public void setPageIdx(long pageIdx) {
    beanCollection.setPageIdx(pageIdx);
    // On page switch: ensure that the PM's for un-modified beans are re-generated.
    // Otherwise the UI could present PM's with outdated beans.
    if (idxOfLastPmBeanCacheUpdate != pageIdx) {
      releaseUnModifiedPmBeanCacheItems();
      idxOfLastPmBeanCacheUpdate = pageIdx;
    }
  }

  @Override
  public long getNumOfItems() {
    return beanCollection.getNumOfItems();
  }

  @Override
  public Iterator<T_PM> iterator() {
    final Iterator<T_BEAN> beanIter = beanCollection.iterator();
    return new Iterator<T_PM>() {
      @Override
      public boolean hasNext() {
        return beanIter.hasNext();
      }
      @Override
      public T_PM next() {
        T_BEAN b = beanIter.next();
        return b != null
            ? (T_PM) PmFactoryApi.<T_BEAN, T_PM>getPmForBean(pmCtxt, b)
            : null;
      }
      @Override
      public void remove() {
        throw new UnsupportedOperationException();
      }
    };
  }

  @Override
  public SelectionHandler<T_PM> getSelectionHandler() {
    return selectionHandler;
  }

  @Override
  public Selection<T_PM> getSelection() {
    return selectionHandler.getSelection();
  }

  @Override
  public void clearCaches() {
    beanCollection.clearCaches();
  }

  @Override
  public ModificationHandler<T_PM> getModificationHandler() {
    return modificationHandler;
  }

  @Override
  public Modifications<T_PM> getModifications() {
    return modificationHandler.getModifications();
  }

  /**
   * Provides the pageable collection of beans behind this collection of bean-PMs.
   *
   * @return the bean collection.
   */
  public PageableCollection<T_BEAN> getBeanCollection() {
    return beanCollection;
  }

  /**
   * Defines a use case specific selection handler.
   *
   * @param selectionHandler
   */
  protected void setSelectionHandler(PmBeanSelectionHandler<T_PM, T_BEAN> selectionHandler) {
    this.selectionHandler = selectionHandler;
  }

  /** Ensures that all unmodified PM factory generated item PMs get released. */
  private void releaseUnModifiedPmBeanCacheItems() {
    Collection<PmBean<?>> cachedPms = BeanPmCacheUtil.getCachedPms(pmCtxt);
    if (cachedPms.isEmpty()) {
      return;
    }

    Modifications<T_BEAN> modifiedBeans = beanCollection.getModifications();
    Collection<T_BEAN> addedBeans = modifiedBeans.getAddedItems();
    Collection<T_BEAN> updatedBeans = modifiedBeans.getUpdatedItems();

    for (PmBean<?> pm : new ArrayList<PmBean<?>>(cachedPms)) {
      if (itemPmClass.isAssignableFrom(pm.getClass())) {
        @SuppressWarnings("unchecked")
        T_BEAN b = (T_BEAN) pm.getPmBean();
        if (!addedBeans.contains(b) &&
            !updatedBeans.contains(b)) {
          BeanPmCacheUtil.removeBeanPm(pmCtxt, pm);
        }
      }
    }
  }

  /**
   * Delegates modification handling to the backing pageable bean collection.
   */
  class PmBeanCollectionModificationHandler implements ModificationHandler<T_PM> {

    /** The modification register. */
    private final PmBeanModifications modifications = new PmBeanModifications();

    /** Listens for changed state changes in the subtree and updates the changedRows accordingly. */
    private final PmChangeListener itemHierarchyChangeListener = new PmChangeListener();

    public PmBeanCollectionModificationHandler() {
      PmEventApi.addHierarchyListener(pmCtxt, PmEvent.VALUE_CHANGED_STATE_CHANGE, itemHierarchyChangeListener);
    }

    @Override
    public boolean addItem(T_PM item) {
      return getBeanCollectionModificationHandler().addItem(item.getPmBean());
    }

    @Override
    public void registerAddedItem(T_PM item) {
      getBeanCollectionModificationHandler().registerAddedItem(item.getPmBean());
    }

    @Override
    public void registerUpdatedItem(T_PM item, boolean isUpdated) {
      getBeanCollectionModificationHandler().registerUpdatedItem(item.getPmBean(), isUpdated);
    }

    // TODO olaf: ensure that the cleanup code also gets called when the bean modification handler gets used.
    // Task 122206: add internal signatures or a control parameter that allows to control event call backs.
    @Override
    public boolean removeSelectedItems() {
      Selection<T_PM> selection = getSelectionHandler().getSelection();

      try {
        PmBeanCollection.this.fireVetoableChange(PageableCollection.EVENT_REMOVE_SELECTION, selection, null);
      } catch (PropertyVetoException e) {
        return false;
      }

      // clear all messages and references to the set of items to delete.
      for (T_PM p : getItemsOnPage()) {
        if (selection.contains(p)) {
          PmMessageApi.clearPmTreeMessages(p);
          BeanPmCacheUtil.removeBeanPm(pmCtxt, p);
        }
      }
      for (T_PM p : getModifications().getUpdatedItems()) {
        if (selection.contains(p)) {
          PmMessageApi.clearPmTreeMessages(p);
          BeanPmCacheUtil.removeBeanPm(pmCtxt, p);
        }
      }

      return getBeanCollectionModificationHandler().removeSelectedItems();
    }

    @Override
    public void registerRemovedItems(Iterable<T_PM> items) {
      List<T_BEAN> beans = new ArrayList<T_BEAN>();
      // clear all messages and references to the deleted item.
      for (T_PM item : items) {
        PmMessageApi.clearPmTreeMessages(item);
        BeanPmCacheUtil.removeBeanPm(pmCtxt, item);
        beans.add(item.getPmBean());
      }
      getBeanCollectionModificationHandler().registerRemovedItems(beans);
    }

    private ModificationHandler<T_BEAN> getBeanCollectionModificationHandler() {
      ModificationHandler<T_BEAN> mh = beanCollection.getModificationHandler();
      if (mh == null) {
        throw new RuntimeException("Pageable bean collection without a modification handler can't handle modifications." + beanCollection);
      }
      return mh;
    }

    @Override
    public void clear() {
      // FIXME olaf: this clear PM code must be moved to a callback
      // that gets consistently triggered if this method was only called on
      // Bean modification handler level.
      // Otherwise we get an inconsistent PM state.

      PmValidationApi.clearInvalidValuesOfSubtree(pmCtxt);

      // This method gets usually called within load and reload operations.
      // The PM factory cache needs to be cleared to ensure that no outdated bean
      // gets re-used.
      BeanPmCacheUtil.clearBeanPmCachesOfSubtree(pmCtxt);

      ModificationHandler<T_BEAN> mh = beanCollection.getModificationHandler();
      if (mh != null && mh.getModifications().isModified()) {
        mh.clear();
      }
    }

    @Override
    public Modifications<T_PM> getModifications() {
      return modifications;
    }

    @Override
    public void setModifications(Modifications<T_PM> modifications) {
      throw new PmRuntimeException(pmCtxt, "Exchanging PmBeanCollection modifications is not yet supported.");
    }

    /** Provides PM type specific modifications based on the backing bean collection. */
    class PmBeanModifications implements Modifications<T_PM> {

      @Override
      public boolean isModified() {
        return getBeanCollectionModificationHandler().getModifications().isModified();
      }

      @SuppressWarnings("unchecked")
      @Override
      public List<T_PM> getAddedItems() {
        List<T_BEAN> beans = getBeanCollectionModificationHandler().getModifications().getAddedItems();
        return (List<T_PM>) PmFactoryApi.getPmListForBeans(pmCtxt, beans, false);
      }

      @SuppressWarnings("unchecked")
      @Override
      public Collection<T_PM> getUpdatedItems() {
        Collection<T_BEAN> beans = getBeanCollectionModificationHandler().getModifications().getUpdatedItems();
        return (Collection<T_PM>) PmFactoryApi.getPmListForBeans(pmCtxt, beans, false);
      }

      @Override
      public Selection<T_PM> getRemovedItems() {
        return new PmBeanSelection<T_PM, T_BEAN>(pmCtxt, getBeanCollectionModificationHandler().getModifications().getRemovedItems());
      }
    };

    /** Listens for changed state changes in the subtree and updates the registered changes accordingly. */
    private class PmChangeListener implements PmEventListener {
      @SuppressWarnings("unchecked")
      @Override
      public void handleEvent(PmEvent event) {
        PmObject itemPm = findChildItemToObserve(event.pm);
        // TODO oboede: Un-register of an update change is currently not possible.
        //
        // The item (the row-PM) must not be in a changed state if a change in a details area
        // was found.
        // A details area change is also registered as an update within the modification handler.
        //
        // Alternatively we might mark the row-PM as changed within the master-details handler.
        //
        // Solution approach: Modifications allow to distinguish between direct master row and details modifications.
        if (itemPm != null && itemPm.isPmValueChanged()) {
          modificationHandler.registerUpdatedItem((T_PM) itemPm, itemPm.isPmValueChanged());
        }
      }

      @Override
      public String toString() {
        return PmBeanCollection.class.getSimpleName() + "#treeValueChange";
      }

      protected PmObject findChildItemToObserve(PmObject changedItem) {
        if (changedItem == pmCtxt) {
          return null;
        }

        PmObject p = changedItem;
        do {
          if (p.getPmParent() == pmCtxt) {
            // Only PM's of the considered type are considered (e.g. row-PM type in case of tables).
            if (itemPmClass.isAssignableFrom(p.getClass())) {
              return p;
            }
            else {
              return null;
            }
          }
          p = p.getPmParent();
        }
        while (p != null);

        // the changed item is not a child of the observed PM.
        return null;
      }
    }

  }

}
