
package org.elasticsearch.action.percolate;

import com.google.common.collect.Lists;
import org.elasticsearch.ElasticsearchGenerationException;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.action.CompositeIndicesRequest;
import org.elasticsearch.action.IndicesRequest;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.support.broadcast.BroadcastOperationRequest;
import org.elasticsearch.client.Requests;
import org.elasticsearch.common.bytes.BytesArray;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.search.builder.SearchSourceBuilderException;

import java.io.IOException;
import java.util.List;
import java.util.Map;

import static org.elasticsearch.action.ValidateActions.addValidationError;

/**
 * A request to execute a percolate operation.
 */
public class PercolateRequest extends BroadcastOperationRequest<PercolateRequest> implements CompositeIndicesRequest {

    private String documentType;
    private String routing;
    private String preference;
    private GetRequest getRequest;
    private boolean onlyCount;

    private BytesReference source;
    private boolean unsafe;

    private BytesReference docSource;

    // Used internally in order to compute tookInMillis, TransportBroadcastOperationAction itself doesn't allow
    // to hold it temporarily in an easy way
    long startTime;

    /**
     * Constructor only for internal usage.
     */
    public PercolateRequest() {
    }

    PercolateRequest(PercolateRequest request, BytesReference docSource) {
        super(request.indices());
        this.documentType = request.documentType();
        this.routing = request.routing();
        this.preference = request.preference();
        this.source = request.source;
        this.docSource = docSource;
        this.onlyCount = request.onlyCount;
        this.startTime = request.startTime;
    }

    @Override
    public List<? extends IndicesRequest> subRequests() {
        List<IndicesRequest> requests = Lists.newArrayList();
        requests.add(this);
        if (getRequest != null) {
            requests.add(getRequest);
        }
        return requests;
    }

    /**
     * Getter for {@link #documentType(String)}
     */
    public String documentType() {
        return documentType;
    }

    /**
     * Sets the type of the document to percolate. This is important as it selects the mapping to be used to parse
     * the document.
     */
    public PercolateRequest documentType(String type) {
        this.documentType = type;
        return this;
    }

    /**
     * Getter for {@link #routing(String)}
     */
    public String routing() {
        return routing;
    }

    /**
     * A comma separated list of routing values to control the shards the search will be executed on.
     */
    public PercolateRequest routing(String routing) {
        this.routing = routing;
        return this;
    }

    /**
     * Getter for {@link #preference(String)}
     */
    public String preference() {
        return preference;
    }

    /**
     * Sets the preference to execute the search. Defaults to randomize across shards. Can be set to
     * <tt>_local</tt> to prefer local shards, <tt>_primary</tt> to execute only on primary shards, or
     * a custom value, which guarantees that the same order will be used across different requests.
     */
    public PercolateRequest preference(String preference) {
        this.preference = preference;
        return this;
    }

    /**
     * Getter for {@link #getRequest(GetRequest)}
     */
    public GetRequest getRequest() {
        return getRequest;
    }

    /**
     * This defines where to fetch the document to be percolated from, which is an alternative of defining the document
     * to percolate in the request body.
     *
     * If this defined than this will override the document specified in the request body.
     */
    public PercolateRequest getRequest(GetRequest getRequest) {
        this.getRequest = getRequest;
        return this;
    }

    /**
     * Before we fork on a local thread, make sure we copy over the bytes if they are unsafe
     */
    @Override
    public void beforeLocalFork() {
        if (unsafe) {
            source = source.copyBytesArray();
            unsafe = false;
        }
    }

    /**
     * @return The request body in its raw form.
     */
    public BytesReference source() {
        return source;
    }

    /**
     * Raw version of {@link #source(PercolateSourceBuilder)}
     */
    public PercolateRequest source(Map document) throws ElasticsearchGenerationException {
        return source(document, Requests.CONTENT_TYPE);
    }

    /**
     * Raw version of {@link #source(PercolateSourceBuilder)}
     */
    @SuppressWarnings("unchecked")
    public PercolateRequest source(Map document, XContentType contentType) throws ElasticsearchGenerationException {
        try {
            XContentBuilder builder = XContentFactory.contentBuilder(contentType);
            builder.map(document);
            return source(builder);
        } catch (IOException e) {
            throw new ElasticsearchGenerationException("Failed to generate [" + document + "]", e);
        }
    }

    /**
     * Raw version of {@link #source(PercolateSourceBuilder)}
     */
    public PercolateRequest source(String document) {
        this.source = new BytesArray(document);
        this.unsafe = false;
        return this;
    }

    /**
     * Raw version of {@link #source(PercolateSourceBuilder)}
     */
    public PercolateRequest source(XContentBuilder documentBuilder) {
        source = documentBuilder.bytes();
        unsafe = false;
        return this;
    }

    /**
     * Raw version of {@link #source(PercolateSourceBuilder)}
     */
    public PercolateRequest source(byte[] document) {
        return source(document, 0, document.length);
    }

    /**
     * Raw version of {@link #source(PercolateSourceBuilder)}
     */
    public PercolateRequest source(byte[] source, int offset, int length) {
        return source(source, offset, length, false);
    }

    /**
     * Raw version of {@link #source(PercolateSourceBuilder)}
     */
    public PercolateRequest source(byte[] source, int offset, int length, boolean unsafe) {
        return source(new BytesArray(source, offset, length), unsafe);
    }

    /**
     * Raw version of {@link #source(PercolateSourceBuilder)}
     */
    public PercolateRequest source(BytesReference source, boolean unsafe) {
        this.source = source;
        this.unsafe = unsafe;
        return this;
    }

    /**
     * Sets the request body definition for this percolate request as raw bytes.
     *
     * This is the preferred way to set the request body.
     */
    public PercolateRequest source(PercolateSourceBuilder sourceBuilder) {
        try {
            XContentBuilder builder = XContentFactory.contentBuilder(Requests.CONTENT_TYPE);
            sourceBuilder.toXContent(builder, ToXContent.EMPTY_PARAMS);
            this.source = builder.bytes();
        } catch (Exception e) {
            throw new SearchSourceBuilderException("Failed to build search source", e);
        }
        this.unsafe = false;
        return this;
    }

    /**
     * Getter for {@link #onlyCount(boolean)}
     */
    public boolean onlyCount() {
        return onlyCount;
    }

    /**
     * Sets whether this percolate request should only count the number of percolator queries that matches with
     * the document being percolated and don't keep track of the actual queries that have matched.
     */
    public PercolateRequest onlyCount(boolean onlyCount) {
        this.onlyCount = onlyCount;
        return this;
    }

    BytesReference docSource() {
        return docSource;
    }

    @Override
    public ActionRequestValidationException validate() {
        ActionRequestValidationException validationException = super.validate();
        if (documentType == null) {
            validationException = addValidationError("type is missing", validationException);
        }
        if (source == null && getRequest == null) {
            validationException = addValidationError("source or get is missing", validationException);
        }
        if (getRequest != null && getRequest.fields() != null) {
            validationException = addValidationError("get fields option isn't supported via percolate request", validationException);
        }
        return validationException;
    }

    @Override
    public void readFrom(StreamInput in) throws IOException {
        super.readFrom(in);
        startTime = in.readVLong();
        documentType = in.readString();
        routing = in.readOptionalString();
        preference = in.readOptionalString();
        unsafe = false;
        source = in.readBytesReference();
        docSource = in.readBytesReference();
        if (in.readBoolean()) {
            getRequest = new GetRequest(null);
            getRequest.readFrom(in);
        }
        onlyCount = in.readBoolean();
    }

    @Override
    public void writeTo(StreamOutput out) throws IOException {
        super.writeTo(out);
        out.writeVLong(startTime);
        out.writeString(documentType);
        out.writeOptionalString(routing);
        out.writeOptionalString(preference);
        out.writeBytesReference(source);
        out.writeBytesReference(docSource);
        if (getRequest != null) {
            out.writeBoolean(true);
            getRequest.writeTo(out);
        } else {
            out.writeBoolean(false);
        }
        out.writeBoolean(onlyCount);
    }
}
