package io.ebeaninternal.api;

import io.ebean.Pairs;
import io.ebeaninternal.server.deploy.BeanNaturalKey;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * Collects the data for processing the natural key cache processing.
 */
public final class NaturalKeyQueryData<T> {

  private final BeanNaturalKey naturalKey;
  /**
   * Only one of IN or IN PAIRS is allowed.
   */
  private boolean hasIn;
  // IN Pairs clause - only one allowed
  private String inProperty0, inProperty1;
  private List<Pairs.Entry> inPairs;
  // IN clause - only one allowed
  private List<Object> inValues;
  private String inProperty;
  // normal EQ expressions
  private List<NaturalKeyEq> eqList;
  private NaturalKeySet set;
  private int hitCount;

  public NaturalKeyQueryData(BeanNaturalKey naturalKey) {
    this.naturalKey = naturalKey;
  }

  private boolean matchProperty(String propName) {
    return naturalKey.matchProperty(propName);
  }

  /**
   * Match for In Pairs expression. We only allow one IN clause.
   */
  public List<Pairs.Entry> matchInPairs(String property0, String property1, List<Pairs.Entry> inPairs) {
    if (hasIn) {
      // only 1 IN allowed (to project naturalIds)
      return null;
    }
    if (matchProperty(property0) && matchProperty(property1)) {
      this.hasIn = true;
      this.inProperty0 = property0;
      this.inProperty1 = property1;
      this.inPairs = new ArrayList<>(inPairs); // will be modified
      return this.inPairs;
    }
    return null;
  }

  /**
   * Match for IN expression. We only allow one IN clause.
   */
  public boolean matchIn(String propName, List<Object> inValues) {
    if (hasIn) {
      // only 1 IN allowed (to project naturalIds)
      return false;
    }
    if (matchProperty(propName)) {
      this.hasIn = true;
      this.inProperty = propName;
      this.inValues = inValues;
      return true;
    }
    return false;
  }

  /**
   * Match for an EQ expression.
   */
  public boolean matchEq(String propName, Object bindValue) {
    if (matchProperty(propName)) {
      if (eqList == null) {
        eqList = new ArrayList<>();
      }
      eqList.add(new NaturalKeyEq(propName, bindValue));
      return true;
    }
    return false;
  }

  /**
   * Build and return the set of natural keys we will use.
   */
  public NaturalKeySet buildKeys() {

    if (!expressionCount() || !matchProperties()) {
      return null;
    }

    this.set = new NaturalKeySet();
    if (inValues != null) {
      addInValues();
    } else if (inPairs != null) {
      addInPairs();
    } else {
      addEqualsKey();
    }
    return set;
  }

  private void addInPairs() {
    // a findList() with an IN Map clause so we project
    // for every IN value a natural key combination
    for (Pairs.Entry entry : inPairs) {
      set.add(new NaturalKeyEntryBasic(naturalKey, eqList, inProperty0, inProperty1, entry));
    }
  }

  private void addInValues() {
    if (eqList == null) {
      // a single property IN expression
      for (Object inValue : inValues) {
        set.add(new NaturalKeyEntrySimple(inValue));
      }
    } else {
      // IN expression + EQ expression(s)
      for (Object inValue : inValues) {
        set.add(new NaturalKeyEntryBasic(naturalKey, eqList, inProperty, inValue));
      }
    }
  }

  private void addEqualsKey() {
    if (eqList.size() == 1) {
      // a single property EQ expression
      set.add(new NaturalKeyEntrySimple(eqList.get(0).value));
    } else {
      set.add(new NaturalKeyEntryBasic(naturalKey, eqList));
    }
  }

  /**
   * Return true if the properties match the natural key properties.
   */
  private boolean matchProperties() {
    if (naturalKey.isSingleProperty()) {
      if (naturalKey.matchSingleProperty((inProperty != null) ? inProperty : eqList.get(0).property)) {
        return true;
      }
    }

    // multiple properties case
    Set<String> exprProps = new HashSet<>();
    if (inProperty != null) {
      exprProps.add(inProperty);
    }
    if (inProperty0 != null) {
      exprProps.add(inProperty0);
    }
    if (inProperty1 != null) {
      exprProps.add(inProperty1);
    }
    if (eqList != null) {
      for (NaturalKeyEq eq : eqList) {
        exprProps.add(eq.property);
      }
    }
    return naturalKey.matchMultiProperties(exprProps);
  }

  /**
   * Check that all the natural key properties are defined.
   */
  private boolean expressionCount() {
    int defined = (inValues == null) ? 0 : 1;
    defined += (inPairs == null) ? 0 : 2;
    defined += (eqList == null) ? 0 : eqList.size();
    return defined == naturalKey.length();
  }

  /**
   * Return the number of entries in the IN clause left remaining (to hit the DB with).
   */
  public boolean allHits() {
    return hitCount > 0
      && hitCount == set.size()
      && (inValues == null || inValues.isEmpty());
  }

  /**
   * Adjust the IN clause removing the hit entry.
   */
  public List<T> removeHits(BeanCacheResult<T> cacheResult) {
    List<BeanCacheResult.Entry<T>> hits = cacheResult.hits();
    this.hitCount = hits.size();

    List<T> beans = new ArrayList<>(hitCount);
    for (BeanCacheResult.Entry<T> hit : hits) {
      removeKey(set.inValue(hit.key()));
      beans.add(hit.bean());
    }
    return beans;
  }

  private void removeKey(Object inValue) {
    if (inValues != null) {
      inValues.remove(inValue);
    } else if (inPairs != null) {
      //noinspection SuspiciousMethodCalls
      inPairs.remove(inValue);
    }
  }
}
