/*
 * Copyright (C) 2000 - 2024 Silverpeas
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * As a special exception to the terms and conditions of version 3.0 of
 * the GPL, you may redistribute this Program in connection with Free/Libre
 * Open Source Software ("FLOSS") applications as described in Silverpeas's
 * FLOSS exception. You should have received a copy of the text describing
 * the FLOSS exception, and it is also available here:
 * "http://www.silverpeas.org/docs/core/legal/floss_exception.html"
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */
package org.silverpeas.components.gallery.model;

import org.silverpeas.components.gallery.constant.MediaType;
import org.silverpeas.core.admin.service.OrganizationControllerProvider;
import org.silverpeas.core.util.CollectionUtil;
import org.silverpeas.kernel.util.StringUtil;
import org.silverpeas.core.admin.user.model.SilverpeasRole;
import org.silverpeas.core.admin.user.model.UserDetail;
import org.silverpeas.core.util.DateUtil;

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

import static org.silverpeas.components.gallery.model.MediaCriteria.VISIBILITY.BY_DEFAULT;
import static org.silverpeas.components.gallery.model.MediaCriteria.VISIBILITY.FORCE_GET_ALL;

/**
 * Class that permits to set media search criteria for media application.
 * @author Yohann Chastagnier
 */
public class MediaCriteria {

  public enum VISIBILITY {
    BY_DEFAULT, VISIBLE_ONLY, HIDDEN_ONLY, FORCE_GET_ALL
  }

  public enum QUERY_ORDER_BY {

    SIZE_ASC(false, "size", true), SIZE_DESC(false, "size", false),
    DIMENSION_ASC(false, "dimension", true), DIMENSION_DESC(false, "dimension", false),
    COMPONENT_INSTANCE_ASC(true, "M.instanceId", true),
    COMPONENT_INSTANCE_DESC(true, "M.instanceId", false),
    IDENTIFIER_ASC(true, "M.mediaId", true), IDENTIFIER_DESC(true, "M.mediaId", false),
    CREATE_DATE_ASC(true, "M.createDate", true), CREATE_DATE_DESC(true, "M.createDate", false),
    LAST_UPDATE_DATE_ASC(true, "M.lastUpdateDate", true),
    LAST_UPDATE_DATE_DESC(true, "M.lastUpdateDate", false),
    TITLE_ASC(true, "LOWER(M.title)", true), TITLE_DESC(true, "LOWER(M.title)", false),
    AUTHOR_ASC_EMPTY_END(false, "M.author", true), AUTHOR_DESC_EMPTY_END(false, "M.author", false),
    AUTHOR_ASC(true, "LOWER(M.author)", true), AUTHOR_DESC(true, "LOWER(M.author)", false);

    private final boolean applicableOnSQLQuery;
    private final String instructionBase;
    private final boolean asc;

    QUERY_ORDER_BY(final boolean applicableOnSQLQuery, final String instructionBase,
        final boolean asc) {
      this.applicableOnSQLQuery = applicableOnSQLQuery;
      this.instructionBase = instructionBase;
      this.asc = asc;
    }

    public boolean isApplicableOnSQLQuery() {
      return applicableOnSQLQuery;
    }

    public String getInstructionBase() {
      return instructionBase;
    }

    public boolean isAsc() {
      return asc;
    }
  }

  private UserDetail requester;
  private SilverpeasRole componentHighestRequesterRole;
  private String componentInstanceId;
  private final List<String> albumIds = new ArrayList<>();
  private final List<MediaType> mediaTypes = new ArrayList<>();
  private final List<QUERY_ORDER_BY> orderByList = new ArrayList<>();
  private final List<String> identifiers = new ArrayList<>();
  private VISIBILITY visibility = BY_DEFAULT;
  private Date referenceDate = DateUtil.getDate();
  private Integer nbDaysBeforeThatMediaIsNotVisible;
  private int resultLimit = 0;

  private MediaCriteria() {

  }

  /**
   * Initializes media search criteria axed on the given media component instance id.
   * @param componentInstanceId the identifier of the media instance.
   * @return an instance of media criteria based on the specified identifier of component instance.
   */
  public static MediaCriteria fromComponentInstanceId(String componentInstanceId) {
    MediaCriteria criteria = new MediaCriteria();
    criteria.onComponentInstanceId(componentInstanceId);
    return criteria;
  }

  /**
   * Initializes media search criteria axed on the given media identifier.
   * @param mediaId the identifier of the media instance.
   * @return an instance of media criteria based on the specified identifier of media.
   */
  public static MediaCriteria fromMediaId(String mediaId) {
    MediaCriteria criteria = new MediaCriteria();
    criteria.identifierIsOneOf(mediaId);
    return criteria;
  }

  /**
   * Initializes media search criteria axed on the given nb of days before that a media is not
   * visible.
   * @param nbDaysBeforeThatMediaIsNotVisible the nb of days before that a media is not visible.
   * @return an instance of media criteria with the nb of days before that a media is not visible
   * criterion set.
   */
  public static MediaCriteria fromNbDaysBeforeThatMediaIsNotVisible(
      Integer nbDaysBeforeThatMediaIsNotVisible) {
    MediaCriteria criteria = new MediaCriteria();
    criteria.nbDaysBeforeThatMediaIsNotVisible = nbDaysBeforeThatMediaIsNotVisible;
    return criteria;
  }

  /**
   * Sets the criterion of the identifier of the component instance the media must be attached.
   * @param componentInstanceId the identifier of the component instance.
   * @return an instance of media criteria with the instance of component instance criterion set.
   */
  public MediaCriteria onComponentInstanceId(String componentInstanceId) {
    this.componentInstanceId = componentInstanceId;
    return this;
  }

  /**
   * Sets the requester. If no requester is specified to criteria, then
   * {@link UserDetail#getCurrentRequester()} is used.
   * @param requester the requester.
   * @return an instance of media criteria with the requester criterion filled.
   */
  public MediaCriteria setRequester(UserDetail requester) {
    this.requester = requester;
    componentHighestRequesterRole = null;
    return this;
  }

  /**
   * Sets the list of media album identifiers criterion to find media which are attached to one of
   * the given ones.
   * @param albumIds the media album identifier list that the media must be attached.
   * @return the media criteria itself with the new criterion on the media types.
   */
  public MediaCriteria albumIdentifierIsOneOf(String... albumIds) {
    CollectionUtil.addAllIgnoreNull(this.albumIds, albumIds);
    return this;
  }

  /**
   * Sets the list of media type criterion to find media which have their type equals to one of the
   * given ones.
   * @param mediaTypes the media type list that the media type must verify.
   * @return the media criteria itself with the new criterion on the media types.
   */
  public MediaCriteria mediaTypeIsOneOf(MediaType... mediaTypes) {
    CollectionUtil.addAllIgnoreNull(this.mediaTypes, mediaTypes);
    return this;
  }

  /**
   * Configures the order of the media list.
   * @param orderBys the list of order by directives.
   * @return the media criteria itself with the list ordering criterion.
   */
  public MediaCriteria orderedBy(QUERY_ORDER_BY... orderBys) {
    CollectionUtil.addAllIgnoreNull(this.orderByList, orderBys);
    return this;
  }

  /**
   * Sets the identifiers criterion to find the medias with an identifier equals to one of the
   * specified ones.
   * @param identifiers a list of identifiers the medias to find should have.
   * @return the media criteria itself with the new criterion on the media identifiers.
   */
  public MediaCriteria identifierIsOneOf(String... identifiers) {
    CollectionUtil.addAllIgnoreNull(this.identifiers, identifiers);
    return this;
  }

  /**
   * Sets the visibility criterion to find the medias according to their period of visibility. If
   * visibility is {@link VISIBILITY#BY_DEFAULT}, then the requester is verified to get VISIBLE (all
   * user roles) or VISIBLE + HIDDEN (lowest user role must be the publisher one).
   * @param visibility the visibility requested.
   * @return the media criteria itself with the new criterion on the media visibility.
   */
  public MediaCriteria withVisibility(VISIBILITY visibility) {
    if (visibility == null) {
      throw new IllegalArgumentException("visibility parameter must not be null");
    }
    this.visibility = visibility;
    return this;
  }

  /**
   * Sets the reference date criterion (the date of the day by default).
   * @param referenceDate the reference date specified.
   * @return the media criteria itself with the new criterion on the media reference date.
   */
  public MediaCriteria referenceDateOf(Date referenceDate) {
    if (referenceDate == null) {
      throw new IllegalArgumentException("dateReference parameter must not be null");
    }
    this.referenceDate = referenceDate;
    return this;
  }

  /**
   * Limit the number of results.
   * @param nbMedia the maximum media in a result.
   * @return the media criteria itself with the result limit set.
   */
  public MediaCriteria limitResultTo(int nbMedia) {
    resultLimit = nbMedia;
    return this;
  }

  /**
   * Gets the maximum number of media in a result list.
   * @return the result limit.
   */
  public int getResultLimit() {
    return resultLimit;
  }

  /**
   * Gets the identifier of media instance. {@link #fromComponentInstanceId(String)}
   * @return the criterion on the media instance to which the medias should belong.
   */
  public String getComponentInstanceId() {
    return componentInstanceId;
  }

  /**
   * Gets the requester.
   * @return the criterion of requester, {@link UserDetail#getCurrentRequester()} if none.
   */
  private UserDetail getRequester() {
    if (requester == null) {
      return UserDetail.getCurrentRequester();
    }
    return requester;
  }

  private SilverpeasRole getComponentHighestRequesterRole() {
    if (componentHighestRequesterRole == null) {
      Set<SilverpeasRole> requesterRoles = SilverpeasRole.fromStrings(
          OrganizationControllerProvider.getOrganisationController()
              .getUserProfiles(getRequester().getId(), getComponentInstanceId()));
      componentHighestRequesterRole = SilverpeasRole.getHighestFrom(requesterRoles);
    }
    return componentHighestRequesterRole;
  }


  /**
   * Gets the media album identifier criteria value. {@link #albumIdentifierIsOneOf(String...)}
   * @return the criterion on the album identifiers the medias must be attached.
   */
  private List<String> getAlbumIds() {
    return albumIds;
  }

  /**
   * Gets the media type criteria value. {@link #mediaTypeIsOneOf(MediaType...)}
   * @return the criterion on the status the medias should match.
   */
  private List<MediaType> getMediaTypes() {
    return mediaTypes;
  }

  /**
   * Gets the identifiers criteria value. {@link #identifierIsOneOf(String...)}
   * @return the criterion on the identifiers the medias should match.
   */
  private List<String> getIdentifiers() {
    return identifiers;
  }

  /**
   * Gets the order by directive list.
   * @return the order by directives.
   */
  private List<QUERY_ORDER_BY> getOrderByList() {
    return orderByList;
  }

  /**
   * Gets the visibility criterion.
   * @return the visibility criterion.
   */
  private VISIBILITY getVisibility() {
    return visibility;
  }

  /**
   * Gets the reference date.
   * @return the reference date.
   */
  private Date getReferenceDate() {
    return referenceDate;
  }

  /**
   * Gets the criterion of the nb of days before that a media is not visible.
   * @return the nb of days before that a media is not visible.
   */
  public Integer getNbDaysBeforeThatMediaIsNotVisible() {
    return nbDaysBeforeThatMediaIsNotVisible;
  }

  /**
   * Processes these criteria with the specified processor. It chains in a given order the different
   * criterion to process.
   * @param processor the processor to use for processing each criterion in these criteria.
   */
  public void processWith(final MediaCriteriaProcessor processor) {
    processor.startProcessing();
    boolean isComponentCriteriaDefined = StringUtil.isDefined(getComponentInstanceId());
    if (isComponentCriteriaDefined) {
      processor.processComponentInstance(getComponentInstanceId());
    }
    UserDetail creatorForVisibility = null;
    VISIBILITY theVisibility = getVisibility();
    if (getRequester() != null) {
      if (theVisibility == BY_DEFAULT &&
          (getRequester().isAccessAdmin() ||
          (isComponentCriteriaDefined && getComponentHighestRequesterRole() != null &&
          getComponentHighestRequesterRole()
              .isGreaterThanOrEquals(SilverpeasRole.PUBLISHER)))) {
        theVisibility = FORCE_GET_ALL;
      } else if (isComponentCriteriaDefined &&
          getComponentHighestRequesterRole() == SilverpeasRole.WRITER) {
        creatorForVisibility = getRequester();
      }
    }
    processor.then().processVisibility(theVisibility, getReferenceDate(), creatorForVisibility);
    if (!getAlbumIds().isEmpty()) {
      processor.then().processAlbums(getAlbumIds());
    }
    if (!getIdentifiers().isEmpty()) {
      processor.then().processIdentifiers(getIdentifiers());
    }
    if (!getMediaTypes().isEmpty()) {
      processor.then().processMediaTypes(getMediaTypes());
    }
    if (getNbDaysBeforeThatMediaIsNotVisible() != null) {
      processor.then().processNbDaysBeforeThatMediaIsNotVisible(getReferenceDate(),
          getNbDaysBeforeThatMediaIsNotVisible());
    }
    if (!getOrderByList().isEmpty()) {
      processor.then().processOrdering(getOrderByList());
    }

    // Configuration
    processor.processResultLimit(getResultLimit());

    processor.endProcessing();
  }
}
