/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.ambari.server.api.query;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;

import jakarta.persistence.EntityManager;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Path;
import jakarta.persistence.criteria.Root;
import jakarta.persistence.metamodel.SingularAttribute;

import org.apache.ambari.server.controller.predicate.AlwaysPredicate;
import org.apache.ambari.server.controller.predicate.ArrayPredicate;
import org.apache.ambari.server.controller.predicate.CategoryPredicate;
import org.apache.ambari.server.controller.predicate.ComparisonPredicate;
import org.apache.ambari.server.controller.predicate.PredicateVisitor;
import org.apache.ambari.server.controller.predicate.UnaryPredicate;
import org.apache.ambari.server.controller.spi.Predicate;
import org.apache.ambari.server.controller.utilities.PredicateHelper;

/**
 * The {@link JpaPredicateVisitor} is used to convert an Ambari
 * {@link Predicate} into a JPA {@link jakarta.persistence.criteria.Predicate}.
 */
public abstract class JpaPredicateVisitor<T> implements PredicateVisitor {
  /**
   * JPA entity manager
   */
  private EntityManager m_entityManager;

  /**
   * Builds the {@link CriteriaQuery} from the {@link Predicate}.
   */
  private CriteriaBuilder m_builder;

  /**
   * The root that the {@code from} clause requests from.
   */
  final private Root<T> m_root;

  /**
   * The query to submit to JPA.
   */
  final private CriteriaQuery<T> m_query;

  /**
   * The last calculated predicate.
   */
  private jakarta.persistence.criteria.Predicate m_lastPredicate = null;

  /**
   * A queue of lists of {@link jakarta.persistence.criteria.Predicate}. Every
   * time an {@code OR} or {@code AND} is encountered, a new chain (list) is
   * created and the prior list is enqeued. When the logical statement is
   * closed, the chain is completed and added to the prior chain's list.
   */
  private ArrayDeque<List<jakarta.persistence.criteria.Predicate>> m_queue =
    new ArrayDeque<>();

  /**
   * Constructor.
   *
   * @param entityManager
   *          the EM used to get a {@link CriteriaBuilder}.
   * @param entityClass
   *          the entity class being queried from.
   */
  public JpaPredicateVisitor(EntityManager entityManager, Class<T> entityClass) {
    m_entityManager = entityManager;
    m_builder = m_entityManager.getCriteriaBuilder();
    m_query = m_builder.createQuery(entityClass);
    m_root = m_query.from(entityClass);
  }

  /**
   * Gets the entity class that is the root type in the JPA {@code from} clause.
   *
   * @return the entity class (not {@code null}).
   */
  public abstract Class<T> getEntityClass();

  /**
   * Gets the {@link SingularAttribute}s mapped to the specified Ambari-style
   * property.
   *
   * @param propertyId
   *          the Ambari-style property (not {@code null}).
   * @return the {@link SingularAttribute}, or {@code null} if no mapping
   *         exists.
   */
  public abstract List<? extends SingularAttribute<?, ?>> getPredicateMapping(
      String propertyId);

  /**
   * Gets the final JPA {@link jakarta.persistence.criteria.Predicate} after the
   * visitor is done traversing the Ambari {@link Predicate}.
   *
   * @return the predicate, or {@code null} if none.
   */
  public jakarta.persistence.criteria.Predicate getJpaPredicate() {
    return m_lastPredicate;
  }

  /**
   * Gets the query to use along with {@link #getJpaPredicate()}.
   *
   * @return the query (not {@code null}).
   */
  public CriteriaQuery<T> getCriteriaQuery() {
    return m_query;
  }

  /**
   * Gets the criteria builder used to construct the query and predicates.
   *
   * @return the builder (never {@code null}).
   */
  public CriteriaBuilder getCriteriaBuilder() {
    return m_builder;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  @SuppressWarnings({ "unchecked", "rawtypes" })
  public void acceptComparisonPredicate(ComparisonPredicate predicate) {
    String propertyId = predicate.getPropertyId();

    List<? extends SingularAttribute<?, ?>> singularAttributes = getPredicateMapping(propertyId);

    if (null == singularAttributes || singularAttributes.size() == 0) {
      return;
    }

    SingularAttribute<?, ?> lastSingularAttribute = null;
    Path<Comparable> path = null;

    for (SingularAttribute<?, ?> singularAttribute : singularAttributes) {
      lastSingularAttribute = singularAttribute;

      if (singularAttribute != null) {
        if (null == path) {
          path = m_root.get(singularAttribute.getName());
        } else {
          path = path.get(singularAttribute.getName());
        }
      }
    }

    if (null == path) {
      return;
    }

    String operator = predicate.getOperator();
    Comparable value = predicate.getValue();

    // convert string to enum for proper JPA comparisons
    if (lastSingularAttribute != null) {
      Class<?> clazz = lastSingularAttribute.getJavaType();
      if (clazz.isEnum()) {
        Class<? extends Enum> enumClass = (Class<? extends Enum>) clazz;
        value = Enum.valueOf(enumClass, value.toString());
      }
    }

    jakarta.persistence.criteria.Predicate jpaPredicate = null;
    if ("=".equals(operator)) {
      jpaPredicate = m_builder.equal(path, value);
    } else if ("<".equals(operator)) {
      jpaPredicate = m_builder.lessThan(path, value);
    } else if ("<=".equals(operator)) {
      jpaPredicate = m_builder.lessThanOrEqualTo(path, value);
    } else if (">".equals(operator)) {
      jpaPredicate = m_builder.greaterThan(path, value);
    } else if (">=".equals(operator)) {
      jpaPredicate = m_builder.greaterThanOrEqualTo(path, value);
    }

    if (null == jpaPredicate) {
      return;
    }

    if (null == m_queue.peekLast()) {
      m_lastPredicate = jpaPredicate;
    } else {
      m_queue.peekLast().add(jpaPredicate);
    }
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void acceptArrayPredicate(ArrayPredicate predicate) {
    // no predicates, no work
    Predicate[] predicates = predicate.getPredicates();
    if (predicates.length == 0) {
      return;
    }

    // create a new list for all of the predicates in this chain
    List<jakarta.persistence.criteria.Predicate> predicateList = new ArrayList<>();
    m_queue.add(predicateList);

    // visit every child predicate so it can be added to the list
    String operator = predicate.getOperator();
    for (int i = 0; i < predicates.length; i++) {
      PredicateHelper.visit(predicates[i], this);
    }
    jakarta.persistence.criteria.Predicate jpaPredicate = null;
    // the list is done; deque and apply logical AND or OR
    predicateList = m_queue.pollLast();
    if (predicateList != null) {
      jakarta.persistence.criteria.Predicate[] array = new jakarta.persistence.criteria.Predicate[predicateList.size()];
      array = predicateList.toArray(array);

      if ("AND".equals(operator)) {
        jpaPredicate = m_builder.and(array);
      } else {
        jpaPredicate = m_builder.or(array);
      }

      if (null == m_queue.peekLast()) {
        m_lastPredicate = jpaPredicate;
      } else {
        m_queue.peekLast().add(jpaPredicate);
      }
    }
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void acceptUnaryPredicate(UnaryPredicate predicate) {
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void acceptAlwaysPredicate(AlwaysPredicate predicate) {
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void acceptCategoryPredicate(CategoryPredicate predicate) {
  }
}
