package de.ids_mannheim.korap.response;

import com.fasterxml.jackson.annotation.*;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.ArrayNode;

import de.ids_mannheim.korap.index.PositionsToOffset;

// Remove:
import de.ids_mannheim.korap.response.SearchContext;

import de.ids_mannheim.korap.response.Response;
import de.ids_mannheim.korap.response.Match;
import de.ids_mannheim.korap.Krill;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

/*
  TODO: Reuse the Krill code for data serialization!
*/
/**
 * Response class for search results.
 * 
 * TODO: Synopsis and let it base on KoralQuery
 * 
 * @author diewald
 * @see Response
 */
@JsonInclude(Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
public final class Result extends Krill {
    ObjectMapper mapper = new ObjectMapper();

    @JsonIgnore
    public static final short ITEMS_PER_PAGE = 25;
    public static final short ITEMS_PER_PAGE_MAX = 100;

    private int startIndex = 0;
    private String serialQuery;

    private List<Match> matches;

    private SearchContext context;

    private short itemsPerPage = ITEMS_PER_PAGE, itemsPerResource = 0;

    private JsonNode request;

    // Logger
    // This is Match instead of Result!
    private final static Logger log = LoggerFactory.getLogger(Match.class);


    /**
     * Construct a new Result object.
     */
    public Result () {
        mapper.enable(SerializationFeature.INDENT_OUTPUT);
    };


    /**
     * Construct a new Result object.
     * 
     * @param serialQuery
     *            Query representation as a string.
     * @param startIndex
     *            Offset position in match array.
     * @param itemsPerPage
     *            Number of matches per page.
     * @param context
     *            Requested {@link SearchContext}
     */
    public Result (String query, int startIndex, short itemsPerPage,
                   SearchContext context) {

        mapper.enable(SerializationFeature.INDENT_OUTPUT);
        // mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
        // mapper.disable(SerializationFeature.WRITE_NULL_MAP_VALUES);

        this.matches = new ArrayList<>(itemsPerPage);
        this.serialQuery = query;
        this.startIndex = startIndex;
        this.itemsPerPage = (itemsPerPage > ITEMS_PER_PAGE_MAX
                || itemsPerPage < 0) ? ITEMS_PER_PAGE : itemsPerPage;
        this.context = context;
    };


    /**
     * Add a new match to the result set.
     * 
     * @param match
     *            A {@link Match} to add.
     */
    public void add (Match km) {
        this.matches.add(km);
    };


    /**
     * Get the number of items (documents) shown per page.
     * 
     * @return Number of items shown per page.
     */
    public short getItemsPerPage () {
        return this.itemsPerPage;
    };


    /**
     * Set the number of items (documents) shown per page.
     * 
     * @param count
     *            Number of items shown per page.
     * @return {@link Result} object for chaining.
     */
    public Result setItemsPerPage (short count) {
        this.itemsPerPage = count;
        return this;
    };


    /**
     * Get serialized query as a {@link JsonNode}.
     * 
     * @return {@link JsonNode} representation of the query object.
     */
    public JsonNode getRequest () {
        return this.request;
    };


    /**
     * Set serialized query as a {@link JsonNode}.
     * 
     * @param request
     *            {@link JsonNode} representation of the query object.
     * @return {@link Result} object for chaining.
     */
    public Result setRequest (JsonNode request) {
        this.request = request;
        return this;
    };


    /**
     * Get the number of items shown per resource (document).
     * Defaults to <tt>0</tt>, which is infinite.
     * 
     * @return The number of items shown per resource.
     */
    public short getItemsPerResource () {
        return this.itemsPerResource;
    };


    /**
     * Set the number of items (matches) shown per resource (text).
     * Defaults to <tt>0</tt>, which is infinite.
     * 
     * @param value
     *            The number of items shown per resource.
     * @return {@link Result} object for chaining.
     */
    public Result setItemsPerResource (short value) {
        this.itemsPerResource = value;
        return this;
    };


    /**
     * Set the number of items (matches) shown per resource
     * (document).
     * Defaults to <tt>0</tt>, which is infinite.
     * 
     * @param value
     *            The number of items shown per resource.
     * @return {@link Result} object for chaining.
     */
    public Result setItemsPerResource (int value) {
        this.itemsPerResource = (short) value;
        return this;
    };


    /**
     * Get the string representation of the search query.
     * 
     * @return The string representation of the search query.
     */
    public String getSerialQuery () {
        return this.serialQuery;
    };


    /**
     * Get a certain {@link Match} by index.
     * 
     * @param index
     *            The numerical index of the match,
     *            starts with <tt>0</tt>.
     * @return The {@link Match} object.
     */
    @JsonIgnore
    public Match getMatch (int index) {
        return this.matches.get(index);
    };


    /**
     * Get the list of {@link Match} matches.
     * 
     * @return The list of {@link Match} objects.
     */
    public List<Match> getMatches () {
        return this.matches;
    };


    /**
     * Get the number of the first match in the result set
     * (<i>aka</i> the offset). Starts with <tt>0</tt>.
     * 
     * @return The index number of the first match in the result set.
     */
    public int getStartIndex () {
        return startIndex;
    };


    /**
     * Get the context parameters of the search by means of a
     * {@link SearchContext} object.
     * 
     * @return The {@link SearchContext} object.
     */
    public SearchContext getContext () {
        return this.context;
    };


    /**
     * Set the context parameters of the search by means of a
     * {@link SearchContext} object.
     * 
     * @param context
     *            The {@link SearchContext} object providing
     *            search context parameters.
     * @return {@link Result} object for chaining.
     */
    public Result setContext (SearchContext context) {
        this.context = context;
        return this;
    };


    /**
     * Serialize the result set as a {@link JsonNode}.
     * 
     * @return {@link JsonNode} representation of the search results.
     */
    public JsonNode toJsonNode () {
        ObjectNode json = (ObjectNode) mapper.valueToTree(super.toJsonNode());

        this._addMeta(json);

        // Add matches
        if (this.matches != null) {

            // Initiate matches
            ArrayNode matches = json.putArray("matches");

            // Add matches
            for (Match km : this.getMatches()) {
                matches.add(km.toJsonNode());
            };
        };

        return json;
    };


    /**
     * Stringifies the matches to give a brief overview on
     * the result. Mainly used for testing.
     * 
     * @return The stringified matches
     */
    public String getOverview () {
        StringBuilder sb = new StringBuilder();

        sb.append("Search for: ").append(this.serialQuery).append("\n");

        int i = 1;

        // Add matches as bracket strings
        for (Match km : this.getMatches())
            sb.append(i++).append(": ").append(km.getSnippetBrackets())
                    .append(" (Doc ").append(km.getLocalDocID()).append(")\n");

        return sb.toString();
    };


    // For Collocation Analysis API
    @Deprecated
    public String toTokenListJsonString () {
        ObjectNode json = (ObjectNode) mapper.valueToTree(super.toJsonNode());

        // Meta data for paging
        ObjectNode meta = json.has("meta") ? (ObjectNode) json.get("meta")
                : (ObjectNode) json.putObject("meta");

        if (this.context != null)
            meta.put("context", this.getContext().toJsonNode());

        meta.put("itemsPerPage", this.itemsPerPage);
        meta.put("startIndex", this.startIndex);

        if (this.itemsPerResource > 0)
            meta.put("itemsPerResource", this.itemsPerResource);

        if (this.serialQuery != null)
            meta.put("serialQuery", this.serialQuery);

        ArrayNode array = json.putArray("matches");

        // Add matches as token lists
        for (Match km : this.getMatches())
            array.add(km.toTokenList());

        try {
            return mapper.writeValueAsString(json);
        }
        catch (Exception e) {
            log.warn(e.getLocalizedMessage());
        };

        return "{}";
    };


    private void _addMeta (ObjectNode json) {
        ObjectNode meta = (ObjectNode) this.getMeta().toJsonNode();

        // Lucene query for debugging purposes
        if (this.serialQuery != null)
            meta.put("serialQuery", this.serialQuery);

        // This may override count
        meta.put("itemsPerPage", this.itemsPerPage);

        if (json.has("meta")) {
            ((ObjectNode) json.get("meta")).setAll(meta);
        }
        else {
            json.set("meta", meta);
        };
    };
};
