
package de.ivu.elect.jee.entity;

import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.commons.lang3.StringUtils.length;
import static org.apache.commons.lang3.StringUtils.startsWith;

import java.util.Collection;
import java.util.Date;
import java.util.List;

import javax.persistence.EntityManager;

import com.querydsl.core.JoinExpression;
import com.querydsl.core.JoinType;
import com.querydsl.core.types.CollectionExpression;
import com.querydsl.core.types.EntityPath;
import com.querydsl.core.types.Expression;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.BooleanPath;
import com.querydsl.core.types.dsl.CollectionPath;
import com.querydsl.core.types.dsl.DateTimePath;
import com.querydsl.core.types.dsl.EntityPathBase;
import com.querydsl.core.types.dsl.EnumPath;
import com.querydsl.core.types.dsl.NumberPath;
import com.querydsl.core.types.dsl.PathInits;
import com.querydsl.core.types.dsl.SetPath;
import com.querydsl.core.types.dsl.SimpleExpression;
import com.querydsl.core.types.dsl.StringPath;
import com.querydsl.jpa.JPQLQuery;
import com.querydsl.jpa.impl.JPADeleteClause;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAUpdateClause;

import de.ivu.elect.jee.exception.ElectCommonException;
import de.ivu.elect.jee.presentation.search.WildcardConverter;

public final class QueryDsl {

  // Es werden nur alle direkte Referenzen initalisiert
  public static final PathInits DIREKT = new PathInits("*");

  // Es werden alle  direkten Referenzen und die Referenzen dieser Referenzen initialisiert
  public static final PathInits DIREKT_2_EBENEN = new PathInits("*.*");

  // Es werden bis zu 4 Ebenen von Referen initalisiert z.B. bis zu QWahlvorschlagsPerson.wahlvorschlagsPerson.wahlvorschlag.gruppeImGebiet.gebiet
  public static final PathInits DIREKT_3_EBENEN = new PathInits("*.*.*");

  // Es werden bis zu 4 Ebenen von Referen initalisiert z.B. bis zu QWahlvorschlagsPerson.wahlvorschlagsPerson.wahlvorschlag.gruppeImGebiet.gebiet.uebergeordnetesGebiet
  public static final PathInits DIREKT_4_EBENEN = new PathInits("*.*.*.*");

  private final JPQLQuery query;

  private final JPQLQuery jpaQuery;

  private EntityPath< ? > entityPath;

  private EntityManager em;

  public QueryDsl(JPQLQuery jpqlQuery) {
    super();

    this.jpaQuery = jpqlQuery;
    this.query = jpqlQuery;
  }

  public QueryDsl(JPAQuery query) {
    super();

    this.jpaQuery = query;
    this.query = query;
  }

  public QueryDsl(EntityManager em) {
    super();

    this.jpaQuery = new JPAQuery(em);
    this.query = this.jpaQuery;
    this.em = em;
  }

  /// Base Methods

  public JPAQuery getQuery() {
    checkJpaQueryPresent();
    return (JPAQuery) jpaQuery;
  }

  public JPADeleteClause getDeleteClause() {
    checkEntityPath();
    return new JPADeleteClause(em, entityPath);
  }

  public JPAUpdateClause getUpdateClause() {
    checkEntityPath();
    return new JPAUpdateClause(em, entityPath);
  }

  private void checkEntityPath() {
    if (entityPath == null) {
      throw new ElectCommonException("Die FROM-Clause muss erst gesetzt werden!");
    }
  }

  public QueryDsl from(EntityPath< ? >... args) {
    query.from(args);
    entityPath = args[0];
    return this;
  }

  public <RT> List< ? > list() {
    checkJpaQueryPresent();
    return jpaQuery.fetch();
  }

  public <RT> List< ? > list(int limit) {
    checkJpaQueryPresent();
    query.limit(limit);
    return jpaQuery.fetch();
  }

  public <RT> List<RT> list(Expression<RT> expr) {
    checkJpaQueryPresent();
    return jpaQuery.fetch();
  }

  public <P> QueryDsl distinct() {
    query.distinct();
    return this;
  }

  public <P> QueryDsl limit(int limit) {
    query.limit(limit);
    return this;
  }

  public String applySearchStringWildcards(String value) {
    return new WildcardConverter().addAndConvertToJpqlLikeExpression(value);
  }

  // ELECTMUC-1673
  public String applySearchStringNormalization(String value) {
    return EntityUtil.normalizeCaseAndDiacritics(value);
  }

  public QueryDsl orderBy(OrderSpecifier< ? >... o) {
    query.orderBy(o);

    return this;
  }

  @Override
  public String toString() {
    return query.toString();
  }

  /// Dates

  public QueryDsl eqIfNotNull(DateTimePath<Date> dateTimePath, Date date) {
    if (date != null) {
      return eq(dateTimePath, date);
    }

    return this;
  }

  public QueryDsl eqOrAfterIfNotNull(DateTimePath<Date> dateTimePath, Date date) {
    if (date != null) {
      betweenIfNotNull(dateTimePath, date, null);
    }

    return this;
  }

  public QueryDsl afterIfNotNull(DateTimePath<Date> dateTimePath, Date date) {
    if (date != null) {
      query.where(dateTimePath.after(date));
    }

    return this;
  }

  public QueryDsl eqOrBeforeIfNotNull(DateTimePath<Date> dateTimePath, Date date) {
    if (date != null) {
      betweenIfNotNull(dateTimePath, null, date);
    }

    return this;
  }

  public QueryDsl beforeIfNotNull(DateTimePath<Date> dateTimePath, Date date) {
    if (date != null) {
      query.where(dateTimePath.before(date));
    }

    return this;
  }

  public QueryDsl betweenIfNotNull(DateTimePath<Date> dateTimePath, Date from, Date to) {
    if (from != null || to != null) {
      query.where(dateTimePath.between(from, to));
    }

    return this;
  }

  /// Like

  public QueryDsl likeInNormalizedIfNotBlank(StringPath pathNormalized, String valueSeparator, String valueUnnormalized) {
    return likeInAnyNormalizedIfNotBlank(pathNormalized, null, valueSeparator, valueUnnormalized);
  }

  public QueryDsl likeInAnyNormalizedIfNotBlank(StringPath path1Normalized, StringPath path2Normalized, String valueSeparator, String valueUnnormalized) {
    return likeInAnyNormalizedIfNotBlank(path1Normalized, path2Normalized, null, valueSeparator, valueUnnormalized);
  }

  public QueryDsl likeInAnyNormalizedIfNotBlank(StringPath path1Normalized, StringPath path2Normalized, StringPath path3Normalized, String valueSeparator, String valueUnnormalized) {
    if (isNotBlank(valueUnnormalized)) {
      return likeInAnyNormalized(path1Normalized, path2Normalized, path3Normalized, valueSeparator, valueUnnormalized);
    }

    return this;
  }

  public QueryDsl likeInNormalized(StringPath pathNormalized, String valueSeparator, String valueUnnormalized) {
    return likeInAnyNormalized(pathNormalized, null, valueSeparator, valueUnnormalized);
  }

  public QueryDsl likeInAnyNormalized(StringPath path1Normalized, StringPath path2Normalized, String valueSeparator, String valueUnnormalized) {
    return likeInAnyNormalized(path1Normalized, path2Normalized, null, valueSeparator, valueUnnormalized);
  }

  // ELECTMUC-2399
  public QueryDsl likeInAnyNormalized(StringPath path1Normalized, StringPath path2Normalized, StringPath path3Normalized, String valueSeparator, String valueUnnormalized) {
    if (length(valueSeparator) > 0 && !startsWith(valueUnnormalized, valueSeparator)) {
      String valueNormalized = asNonBlankLikeValueNormalized(valueUnnormalized);

      String jpqlWildcardCharacter = WildcardConverter.WILDCARD_CHARACTER_JPQL;

      if (!valueNormalized.startsWith(jpqlWildcardCharacter)) {
        String valueNormalizedSeparated = jpqlWildcardCharacter + valueSeparator + valueNormalized;

        BooleanExpression expression = path1Normalized.like(valueNormalized).or(path1Normalized.like(valueNormalizedSeparated));

        if (path2Normalized != null) {
          expression = expression.or(path2Normalized.like(valueNormalized).or(path2Normalized.like(valueNormalizedSeparated)));
        }

        if (path3Normalized != null) {
          expression = expression.or(path3Normalized.like(valueNormalized).or(path3Normalized.like(valueNormalizedSeparated)));
        }

        query.where(expression);

        return this;
      }
    }

    return likeAnyNormalized(path1Normalized, path2Normalized, path3Normalized, valueUnnormalized);
  }

  public QueryDsl likeNormalizedIfNotBlank(StringPath pathNormalized, String valueUnnormalized) {
    return likeAnyNormalizedIfNotBlank(pathNormalized, null, valueUnnormalized);
  }

  public QueryDsl likeAnyNormalizedIfNotBlank(StringPath path1Normalized, StringPath path2Normalized, String valueUnnormalized) {
    return likeAnyNormalizedIfNotBlank(path1Normalized, path2Normalized, null, valueUnnormalized);
  }

  public QueryDsl likeAnyNormalizedIfNotBlank(StringPath path1Normalized, StringPath path2Normalized, StringPath path3Normalized, String valueUnnormalized) {
    if (isNotBlank(valueUnnormalized)) {
      return likeAnyNormalized(path1Normalized, path2Normalized, path3Normalized, valueUnnormalized);
    }

    return this;
  }

  public QueryDsl likeNormalized(StringPath pathNormalized, String valueUnnormalized) {
    return likeAnyNormalized(pathNormalized, null, valueUnnormalized);
  }

  public QueryDsl likeAnyNormalized(StringPath path1Normalized, StringPath path2Normalized, String valueUnnormalized) {
    return likeAnyNormalized(path1Normalized, path2Normalized, null, valueUnnormalized);
  }

  public QueryDsl likeAnyNormalized(StringPath path1Normalized, StringPath path2Normalized, StringPath path3Normalized, String valueUnnormalized) {
    String valueNormalized = asNonBlankLikeValueNormalized(valueUnnormalized);

    BooleanExpression expression = path1Normalized.like(valueNormalized);

    if (path2Normalized != null) {
      expression = expression.or(path2Normalized.like(valueNormalized));
    }

    if (path3Normalized != null) {
      expression = expression.or(path3Normalized.like(valueNormalized));
    }

    query.where(expression);

    return this;
  }

  public QueryDsl likeIfNotBlank(StringPath path, String value) {
    return likeAnyIfNotBlank(path, null, value);
  }

  public QueryDsl likeAnyIfNotBlank(StringPath path1, StringPath path2, String value) {
    return likeAnyIfNotBlank(path1, path2, null, value);
  }

  public QueryDsl likeAnyIfNotBlank(StringPath path1, StringPath path2, StringPath path3, String value) {
    if (isNotBlank(value)) {
      return likeAny(path1, path2, path3, value);
    }

    return this;
  }

  public QueryDsl like(StringPath path, String value) {
    return likeAny(path, null, value);
  }

  public QueryDsl likeAny(StringPath path1, StringPath path2, String value) {
    return likeAny(path1, path2, null, value);
  }

  public QueryDsl likeAny(StringPath path1, StringPath path2, StringPath path3, String value) {
    String valueLowerCase = asNonBlankLikeValueLowerCase(value);

    BooleanExpression expression = path1.toLowerCase().like(valueLowerCase);

    if (path2 != null) {
      expression = expression.or(path2.toLowerCase().like(valueLowerCase));
    }

    if (path3 != null) {
      expression = expression.or(path3.toLowerCase().like(valueLowerCase));
    }

    query.where(expression);

    return this;
  }

  public QueryDsl notLikeIfNotBlank(StringPath path, String value) {
    if (isNotBlank(value)) {
      return notLike(path, value);
    }

    return this;
  }

  public QueryDsl notLike(StringPath path, String value) {
    // NOTE: isEmpty funktioniert in Oracle nicht auf NULL-Werten, daher zusätzlich auf isNull prüfen
    query.where(path.isNull().or(path.isEmpty()).or(path.toLowerCase().like(asNonBlankLikeValueLowerCase(value)).not()));

    return this;
  }

  protected String asNonBlankLikeValueNormalized(String value) {
    return applySearchStringWildcards(applySearchStringNormalization(value));
  }

  protected String asNonBlankLikeValueLowerCase(String value) {
    return applySearchStringWildcards(value.toLowerCase());
  }

  /// IsNull

  public QueryDsl isNull(StringPath path) {
    query.where(path.isNull());
    return this;
  }

  public <E extends Enum<E>> QueryDsl isNull(EnumPath<E> path) {
    query.where(path.isNull());
    return this;
  }

  public <T extends Number & Comparable< ? >> QueryDsl isNull(NumberPath<T> path) {
    query.where(path.isNull());
    return this;
  }

  public <E> QueryDsl isNull(EntityPathBase<E> path) {
    query.where(path.isNull());
    return this;
  }

  /// IsNotNull

  public QueryDsl isNotNull(StringPath path) {
    query.where(path.isNotNull());
    return this;
  }

  public <E extends Enum<E>> QueryDsl isNotNull(EnumPath<E> path) {
    query.where(path.isNotNull());
    return this;
  }

  public <T extends Number & Comparable< ? >> QueryDsl isNotNull(NumberPath<T> path) {
    query.where(path.isNotNull());
    return this;
  }

  public <E> QueryDsl isNotNull(EntityPathBase<E> path) {
    query.where(path.isNotNull());
    return this;
  }

  /// Equals

  public QueryDsl eq(StringPath path, String string) {
    query.where(path.eq(string));
    return this;
  }

  public QueryDsl eq(DateTimePath<Date> path, Date date) {
    query.where(path.eq(date));
    return this;
  }

  public QueryDsl eq(BooleanPath path, Boolean bool) {
    query.where(path.eq(bool));
    return this;
  }

  public <E extends Enum<E>> QueryDsl eq(EnumPath<E> path, E object) {
    query.where(path.eq(object));
    return this;
  }

  public <E extends Enum<E>> QueryDsl eqIn(EnumPath<E> path, Collection<E> objects) {
    query.where(path.in(objects));
    return this;
  }

  public <T extends Number & Comparable< ? >> QueryDsl eq(NumberPath<T> path, T value) {
    query.where(path.eq(value));
    return this;
  }

  public <E> QueryDsl eq(EntityPathBase<E> path, E entity) {
    query.where(path.eq(entity));
    return this;
  }

  /// equalsIfNotNull

  public QueryDsl eqIfNotNull(BooleanPath path, Boolean booleanValue) {
    if (booleanValue != null) {
      if (booleanValue) {
        query.where(path.isTrue());
      } else {
        query.where(path.isFalse());
      }
    }
    return this;
  }

  public QueryDsl eqIfNotBlank(StringPath path, String string) {
    if (isNotBlank(string)) {
      return eq(path, string);
    }
    return this;
  }

  public <E extends Enum<E>> QueryDsl eqIfNotNull(EnumPath<E> path, E object) {
    if (object != null) {
      return eq(path, object);
    }
    return this;
  }

  public <T extends Number & Comparable< ? >> QueryDsl eqIfNotNull(NumberPath<T> path, T value) {
    if (value != null) {
      return eq(path, value);
    }
    return this;
  }

  public <E> QueryDsl eqIfNotNull(EntityPathBase<E> path, E entity) {
    if (entity != null) {
      return eq(path, entity);
    }
    return this;
  }

  public <T> void inIfNotEmpty(SimpleExpression<T> path, Collection< ? extends T> collection) {
    if (collection != null && !collection.isEmpty()) {
      query.where(path.in(collection));
    }
  }

  public <T> void notInIfNotEmpty(SimpleExpression<T> path, Collection< ? extends T> collection) {
    if (collection != null && !collection.isEmpty()) {
      query.where(path.notIn(collection));
    }
  }

  /// Join

  public <P> QueryDsl joinFetch(EntityPath<P> target) {
    checkJpaQueryPresent();
    jpaQuery.join(target).fetch();
    return this;
  }

  public <P> QueryDsl leftJoinFetch(EntityPath<P> target) {
    checkJpaQueryPresent();
    jpaQuery.leftJoin(target).fetch();
    return this;
  }

  public QueryDsl joinFetch(CollectionPath< ? , ? > target) {
    checkJpaQueryPresent();
    jpaQuery.join(target).fetch();
    return this;
  }

  public QueryDsl joinFetch(SetPath< ? , ? > target) {
    checkJpaQueryPresent();
    jpaQuery.join(target).fetch();
    return this;
  }

  public QueryDsl leftJoinFetch(CollectionPath< ? , ? > target) {
    checkJpaQueryPresent();
    jpaQuery.leftJoin(target).fetch();
    return this;
  }

  public QueryDsl leftJoinFetch(SetPath< ? , ? > target) {
    checkJpaQueryPresent();
    jpaQuery.leftJoin(target).fetch();
    return this;
  }

  private void checkJpaQueryPresent() {
    if (jpaQuery == null) {
      throw new IllegalStateException("Kein JPA Query");
    }
  }

  /**
   * @param target
   *          Das Ziel, z.B. QWahlhelfereinsatz.wahlhelfereinsatz
   * @param source
   *          Die Quelle, z.B. QWahlhelfer.wahlhelfer.wahlhelfereinsaetze
   */
  public <P> QueryDsl innerJoinOnlyOnce(CollectionExpression< ? extends Collection<P>, P> target, EntityPath<P> source) {
    if (!hasInnerJoin(source)) {
      query.innerJoin(target, source);
    }
    return this;
  }

  public <P> QueryDsl leftJoin(CollectionExpression< ? extends Collection<P>, P> target, EntityPath<P> source) {
    query.leftJoin(target, source);
    return this;
  }

  public <P> QueryDsl rightJoin(CollectionExpression< ? extends Collection<P>, P> target, EntityPath<P> source) {
    query.rightJoin(target, source);
    return this;
  }

  public <P> QueryDsl join(CollectionExpression< ? extends Collection<P>, P> target, EntityPath<P> source) {
    query.join(target, source);
    return this;
  }

  public <P> boolean hasInnerJoin(EntityPath<P> target) {
    checkJpaQueryPresent();

    for (JoinExpression join : jpaQuery.getMetadata().getJoins()) {
      if (JoinType.INNERJOIN.equals(join.getType()) && join.getTarget().getType().equals(target.getType())) {
        return true;
      }
    }

    return false;
  }
}
