package com.oguerreiro.resilient.service.query;

import java.io.Serializable;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.transaction.annotation.Transactional;

import com.oguerreiro.resilient.domain.AbstractResilientEntity;
import com.oguerreiro.resilient.repository.ResilientJpaRepository;
import com.oguerreiro.resilient.service.criteria.AbstractCriteria;
import com.oguerreiro.resilient.service.dto.EmissionFactorDTO;
import com.oguerreiro.resilient.service.mapper.ResilientEntityMapper;

import tech.jhipster.service.QueryService;

/**
 * 
 * @param <T>   Entity Domain class type
 * @param <DTO> Entity Domain DTO class type, for <T>
 * @param <C>   Criteria
 */
@Transactional(readOnly = true)
public abstract class AbstractQueryService<T extends AbstractResilientEntity<T, ID>, DTO, ID extends Serializable, C extends AbstractCriteria>
    extends QueryService<T> {
  protected final Logger log = LoggerFactory.getLogger(this.getClass());

  /**
   * ATTENTION<br>
   * Use this constant to identify the id attribute in a join relation sprcification.
   * Normal usage, for a join relation between InputData and Unit, would be:
   * 
   * <pre>
   * <code>
  specification = specification.and(
    buildSpecification(
        criteria.getSourceUnitId(),
        root -> root.join(InputData_.sourceUnit, JoinType.LEFT).get(Unit_.id)
    )
  );
   * </code>
   * </pre>
   * 
   * But this will throw a NullPointerException. Because Unit_id is NOT defined in the generated classe Unit_, but in
   * the extended AbstractResilientLongIdEntity_. Hibernate doesn't manage weel this hierarchy and doesn't find it,
   * thus, throwing the NullPointerException.
   * 
   * The solution is to use the name of the property as string. And thats what this constant is for.
   * The corrected code is:
   * 
   * <pre>
   * <code>
  specification = specification.and(
    buildSpecification(
        criteria.getSourceUnitId(),
        root -> root.join(InputData_.sourceUnit, JoinType.LEFT).get(ID_ATTRIBUTE_NAME)
    )
  );
   * </code>
   * </pre>
   */
  public static final String ID_ATTRIBUTE_NAME = "id";

  private final ResilientEntityMapper<DTO, T> domainMapper;

  private final ResilientJpaRepository domainRepository;

  protected abstract Specification<T> createSpecification(C criteria);

  /**
   * Overridable method, for cases theres a need for a more complex query created in PJARepository.
   * The default is domainRepository.findAll(specification)
   * 
   * @param  specification
   * @return
   */
  protected List<T> find(Specification<T> specification) {
    return domainRepository.findAll(specification);
  }

  protected List<T> find(Specification<T> specification, Sort sort) {
    return domainRepository.findAll(specification, sort);
  }

  protected AbstractQueryService(ResilientJpaRepository domainRepository, ResilientEntityMapper<DTO, T> domainMapper) {
    this.domainRepository = domainRepository;
    this.domainMapper = domainMapper;
  }

  /**
   * Return a {@link List} of {@link EmissionFactorDTO} which matches the criteria from
   * the database.
   * 
   * @param  criteria The object which holds all the filters, which the entities
   *                  should match.
   * @return          the matching entities.
   */
  @Transactional(readOnly = true)
  public List<DTO> findByCriteria(C criteria) {
    log.debug("find by criteria : {}", criteria);

    return findByCriteria(criteria, Sort.unsorted());
  }

  @Transactional(readOnly = true)
  public List<DTO> findByCriteria(C criteria, Sort sort) {
    log.debug("find by criteria : {}", criteria, sort);

    final Specification<T> specification = createSpecification(criteria);
    List<T> list = this.find(specification, sort);
    List<DTO> dtos = this.domainMapper.toDto(list);

    return dtos;
  }

  /**
   * Return the number of matching entities in the database.
   * 
   * @param  criteria The object which holds all the filters, which the entities
   *                  should match.
   * @return          the number of matching entities.
   */
  @Transactional(readOnly = true)
  public long countByCriteria(C criteria) {
    log.debug("count by criteria : {}", criteria);
    final Specification<T> specification = createSpecification(criteria);
    return domainRepository.count(specification);
  }

}
