package com.avaje.ebeaninternal.server.cache;

import com.avaje.ebeaninternal.server.deploy.BeanDescriptor;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * List of changes to be applied to L2 cache.
 */
public class CacheChangeSet {

  private final List<CacheChange> entries = new ArrayList<CacheChange>();

  private final Set<BeanDescriptor> queryCaches = new HashSet<BeanDescriptor>();

  private final Map<ManyKey, ManyChange> manyChangeMap = new HashMap<ManyKey, ManyChange>();

  /**
   * Set of "base tables" modified used to invalidate entities based on views.
   */
  private final Set<String> viewInvalidation = new HashSet<String>();

  private final boolean viewEntityInvalidation;

  /**
   * Construct specifying if we also need to process invalidation for entities based on views.
   */
  public CacheChangeSet(boolean viewEntityInvalidation) {
    this.viewEntityInvalidation = viewEntityInvalidation;
  }

  /**
   * Apply the changes to the L2 cache except entity/view invalidation.
   *
   * Return the set of table changes to process invalidation for entities based on views.
   */
  public Set<String> apply() {
    for (BeanDescriptor entry : queryCaches) {
      entry.queryCacheClear();
    }
    for (CacheChange entry : entries) {
      entry.apply();
    }
    for (CacheChange entry : manyChangeMap.values()) {
      entry.apply();
    }
    return viewInvalidation;
  }

  /**
   * Add an entry to clear a query cache.
   */
  public void addClearQuery(BeanDescriptor<?> descriptor) {
    queryCaches.add(descriptor);
  }

  /**
   * Add many property clear.
   */
  public <T> void addManyClear(BeanDescriptor<T> desc, String manyProperty) {
    many(desc, manyProperty).setClear();
  }

  /**
   * Add many property remove.
   */
  public <T> void addManyRemove(BeanDescriptor<T> desc, String manyProperty, Object parentId) {
    many(desc, manyProperty).addRemove(parentId);
  }

  /**
   * Add many property put.
   */
  public <T> void addManyPut(BeanDescriptor<T> desc, String manyProperty, Object parentId, CachedManyIds entry) {
    many(desc, manyProperty).addPut(parentId, entry);
  }

  /**
   * On bean insert register table for view based entity invalidation.
   */
  public void addBeanInsert(String baseTable) {
    if (viewEntityInvalidation) {
      viewInvalidation.add(baseTable);
    }
  }

  /**
   * Remove a bean from the cache.
   */
  public <T> void addBeanRemove(BeanDescriptor<T> desc, Object id) {
    entries.add(new CacheChangeBeanRemove(desc, id));
    if (viewEntityInvalidation) {
      viewInvalidation.add(desc.getBaseTable());
    }
  }

  /**
   * Update a bean entry.
   */
  public <T> void addBeanUpdate(BeanDescriptor<T> desc, Object id, Map<String, Object> changes, boolean updateNaturalKey, long version) {
    entries.add(new CacheChangeBeanUpdate(desc, id, changes, updateNaturalKey, version));
    if (viewEntityInvalidation) {
      viewInvalidation.add(desc.getBaseTable());
    }
  }

  /**
   * Update a natural key.
   */
  public <T> void addNaturalKeyPut(BeanDescriptor<T> desc, Object id, Object val) {
    entries.add(new CacheChangeNaturalKeyPut(desc, id, val));
  }

  /**
   * Return the ManyChange for the given descriptor and property manyProperty.
   */
  private ManyChange many(BeanDescriptor<?> desc, String manyProperty) {
    ManyKey key = new ManyKey(desc, manyProperty);
    ManyChange manyChange = manyChangeMap.get(key);
    if (manyChange == null) {
      manyChange = new ManyChange(key);
      manyChangeMap.put(key, manyChange);
    }
    return manyChange;
  }

  /**
   * Changes for a specific many property.
   */
  private static class ManyChange implements CacheChange {

    final ManyKey key;

    final List<Object> removes = new ArrayList<Object>();

    final Map<Object,CachedManyIds> puts = new LinkedHashMap<Object, CachedManyIds>();

    boolean clear;

    ManyChange(ManyKey key) {
      this.key = key;
    }

    /**
     * Clear all entries.
     */
    void setClear() {
      this.clear = true;
      removes.clear();
    }

    /**
     * Remove entry for the given parentId.
     */
    void addRemove(Object parentId) {
      if (!clear) {
        removes.add(parentId);
      }
    }

    /**
     * Put entry for the given parentId.
     */
    void addPut(Object parentId, CachedManyIds entry) {
      puts.put(parentId, entry);
    }

    @Override
    public void apply() {
      if (clear) {
        key.cacheClear();
      } else {
        for (Map.Entry<Object, CachedManyIds> entry : puts.entrySet()) {
          key.cachePut(entry.getKey(), entry.getValue());
        }
        for (Object parentId : removes) {
          key.cacheRemove(parentId);
        }
      }
    }
  }

  /**
   * Key for changes on a many property.
   */
  private static class ManyKey {

    private final BeanDescriptor<?> desc;

    private final String manyProperty;

    ManyKey(BeanDescriptor<?> desc, String manyProperty) {
      this.desc = desc;
      this.manyProperty = manyProperty;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;
      ManyKey manyKey = (ManyKey) o;
      return desc.equals(manyKey.desc) && manyProperty.equals(manyKey.manyProperty);
    }

    @Override
    public int hashCode() {
      return 31 * desc.hashCode() + manyProperty.hashCode();
    }

    void cacheClear() {
      desc.cacheManyPropClear(manyProperty);
    }

    void cachePut(Object parentId, CachedManyIds entry) {
      desc.cacheManyPropPut(manyProperty, parentId, entry);
    }

    void cacheRemove(Object parentId) {
      desc.cacheManyPropRemove(manyProperty, parentId);
    }
  }
}
