package com.capgemini.framework.teamcenter.queries.saved;

import com.capgemini.framework.teamcenter.ServiceDataHelper;
import com.capgemini.framework.teamcenter.authent.TcSession;
import com.capgemini.framework.teamcenter.exceptions.KurtException;
import com.capgemini.framework.teamcenter.exceptions.QueryDoesNotExist;
import com.teamcenter.schemas.soa._2006_03.exceptions.ServiceException;
import com.teamcenter.services.strong.query.SavedQueryService;
import com.teamcenter.services.strong.query._2006_03.SavedQuery.GetSavedQueriesResponse;
import com.teamcenter.services.strong.query._2006_03.SavedQuery.SavedQueryObject;
import com.teamcenter.services.strong.query._2007_09.SavedQuery.QueryResults;
import com.teamcenter.services.strong.query._2007_09.SavedQuery.SavedQueriesResponse;
import com.teamcenter.services.strong.query._2008_06.SavedQuery.QueryInput;
import com.teamcenter.soa.client.model.ModelObject;
import com.teamcenter.soa.client.model.strong.ImanQuery;
import com.teamcenter.soa.client.model.strong.Item;
import com.teamcenter.soa.client.model.strong.ItemRevision;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;
import java.util.Map.Entry;

/**
 * Setup Teamcenter Search objects, using names, IdDs atc. as parameters.<br>
 * Additional search parameters are available. Uses Teamcenter SavedQueries.
 *
 * @param <OBJ>   The type of TC objets request (generally {@link Item} or {@link ItemRevision}). Must be a sub-type of {@link ModelObject}.
 * @param <QUERY> The type of the actual daughter class. Provided to chain requests.
 */
public abstract class AbstractSavedQuery<OBJ extends ModelObject, QUERY extends AbstractSavedQuery<OBJ, QUERY>> {
    private static final Logger LOGGER = LoggerFactory.getLogger(AbstractSavedQuery.class);

    private static final int DEFAULT_QUERY_RESULT_PAGING = 1000;
    private static final int DEFAULT_MAX_QUERY_RESULTS = -1; // NO limit!
    private static final String UNABLE_TO_FIND_THE_ITEM = "Unable to find the Item: ";
    protected final Map<String, String> parameters;
    protected final TcSession session;
    final String savedQueryName;
    private final SavedQueryService savedQueryService;
    private final int paging;
    /**
     * Maximum number of results for this query.
     */
    private int maxQueries;

    /**
     * Basic constructor. Caps the number of results.
     *
     * @param session        Required to setup the search services.
     * @param savedQueryName the name of the request to obtain from Tc.
     * @param maxQueries     the cap on maximum number of results
     * @param paging         stepping through the result list
     */
    protected AbstractSavedQuery(final TcSession session, final String savedQueryName, final int maxQueries, final int paging) {
        this.session = session;
        this.savedQueryService = session.getSavedQueryService();
        this.parameters = new HashMap<>();
        this.savedQueryName = savedQueryName;
        this.maxQueries = maxQueries;
        this.paging = paging;
    }

    /**
     * Basic constructor.
     *
     * @param session        Required to setup the search services.
     * @param savedQueryName the name of the request to obtain from Tc.
     */
    protected AbstractSavedQuery(final TcSession session, final String savedQueryName) {
        this.session = session;
        this.savedQueryService = session.getSavedQueryService();
        this.parameters = new HashMap<>();
        this.savedQueryName = savedQueryName;
        this.maxQueries = DEFAULT_MAX_QUERY_RESULTS;
        this.paging = DEFAULT_QUERY_RESULT_PAGING;
    }

    /**
     * Adds some search parameter (Revision, unit, date, owner...).
     *
     * @param aParamName  Le type de parametre a utiliser
     * @param aParamValue La valeur que le parametre impose doit respecter.
     * @return This same request, to chain the requests
     */
    protected abstract QUERY addParam(final String aParamName, final String aParamValue);

    /**
     * Request that the objects match the specified Identifier.
     *
     * @param itemID The requested identifier (ex: "ND0000")
     * @return This same request, to chain the requests.
     */
    public abstract QUERY searchID(final String itemID);

    /**
     * Request that the objects match the specified revision.
     *
     * @param revisionID the requested revision. (ex: "B")
     * @return This same request, to chain the requests.
     */
    public abstract QUERY searchRevision(final String revisionID);

    /**
     * Request that the objects match both the specified Identifier and revision.
     *
     * @param itemID     the requested identifier. (ex: "ND0000")
     * @param revisionID the requested revision. (ex: "B")
     * @return This same request, to chain the requests.
     */
    public QUERY searchIDAndRevision(final String itemID, final String revisionID) {
        return this.searchID(itemID).searchRevision(revisionID);
    }

    /**
     * Request that the objects match the specified object name.
     *
     * @param itemName the requested object name. (ex: "GE90 M10 MOTEUR ASSEMBLE")
     * @return This same request, to chain the requests.
     */
    public abstract QUERY searchName(final String itemName);

    /**
     * Impose a maximum number of returned values.
     *
     * @param maxValues the maximum number of results
     */
    public void changeMaxNumberValues(final int maxValues) {
        maxQueries = maxValues;
    }

    /**
     * Returns the result of the query.
     *
     * @return the matching element.
     */
    public OBJ find() throws KurtException {
        List<OBJ> list = findAll();
        if (!list.isEmpty()) {
            OBJ obj = list.get(0);
            if (obj != null) {
                return obj;
            }
        }
        throw new KurtException(UNABLE_TO_FIND_THE_ITEM);
    }

    /**
     * Returns the result of the query.
     *
     * @return a list containing all matching elements. This list may not be {@code null}, but it may be empty.
     */
    public List<OBJ> findAll() {
        ImanQuery query;
        // Get the service stub.
        try {
            query = getSavedQuery();
        } catch (QueryDoesNotExist e) {
            LOGGER.error("Query does not exist", e);
            return new ArrayList<>();
        } catch (ServiceException e) {
            LOGGER.error("Unable to contact the servers to perform the query", e);
            return new ArrayList<>();
        }
        List<OBJ> foundObjs;
        if (query == null) {
            LOGGER.warn("No query named \"{}\" were found.", savedQueryName);
            foundObjs = new ArrayList<>();
        } else {
            QueryInput[] savedQueryInput = setQueryParameters(query);

            // *****************************
            // Execute the service operation
            // *****************************
            SavedQueriesResponse savedQueryResult = savedQueryService.executeSavedQueries(savedQueryInput);
            if (ServiceDataHelper.hasErrors(savedQueryResult.serviceData)) {
                ServiceDataHelper.displayErrors(savedQueryResult.serviceData);
                LOGGER.warn("The following query has failed: {} with arguments:\n{}", savedQueryName, parameters.toString());
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Accepted properties: {}", Arrays.toString(query.getPropertyNames()));
                    LOGGER.debug("Query clauses: {}", Arrays.toString(query.get_query_clauses()));
                }
            }

            QueryResults found = savedQueryResult.arrayOfResults[0];

            foundObjs = parseResults(found);
        }
        return foundObjs;
    }

    private List<OBJ> parseResults(final QueryResults found) {
        List<OBJ> foundObjs = new ArrayList<>();
        LOGGER.debug("Loading {} UIDs", found.objectUIDS.length);

        for (int pageStart = 0; pageStart < found.objectUIDS.length; pageStart = pageStart + paging) {
            int pageSize = (pageStart + paging < found.objectUIDS.length) ? paging : found.objectUIDS.length - pageStart;
            String[] uidsOnPage = new String[pageSize];
            System.arraycopy(found.objectUIDS, pageStart, uidsOnPage, 0, pageSize);
            if (LOGGER.isTraceEnabled()) {
                int pageEnd = pageStart + pageSize;
                LOGGER.trace("-Loading UID page [{}-{}[\t:  {}", pageStart, pageEnd, Arrays.toString(uidsOnPage));
            }
            foundObjs.addAll(session.<OBJ>loadObjects(uidsOnPage));
        }
        return foundObjs;
    }

    /**
     * Obtain the TC Query Object.
     */
    private ImanQuery getSavedQuery() throws QueryDoesNotExist, ServiceException {
        GetSavedQueriesResponse savedQueries;
        savedQueries = savedQueryService.getSavedQueries();
        if (ServiceDataHelper.hasErrors(savedQueries.serviceData)) {
            ServiceDataHelper.displayErrors(savedQueries.serviceData);
        }
        if (savedQueries.queries.length == 0) {
            throw new QueryDoesNotExist("There are no saved queries in the system.");
        }
        for (SavedQueryObject query : savedQueries.queries) {
            if (savedQueryName.equals(query.name)) {
                return query.query;
            }
        }
        throw new QueryDoesNotExist("No saved queries found with name \"" + savedQueryName + "\"");
    }

    private QueryInput[] setQueryParameters(final ImanQuery query) {
        QueryInput[] savedQueryInput = new QueryInput[1];
        savedQueryInput[0] = new QueryInput();
        savedQueryInput[0].query = query;
        savedQueryInput[0].maxNumToReturn = maxQueries; // -1 for ALL
        savedQueryInput[0].limitList = new ModelObject[0];
        int nbParam = parameters.size();
        String[] paramTypes = new String[nbParam];
        String[] paramValues = new String[nbParam];
        int num = 0;
        for (Entry<String, String> param : parameters.entrySet()) {
            paramTypes[num] = param.getKey();
            paramValues[num] = param.getValue();
            num++;
        }
        savedQueryInput[0].entries = paramTypes;
        savedQueryInput[0].values = paramValues;
        return savedQueryInput;
    }

}
