package com.atlassian.stash.internal.rest.pull;

import com.atlassian.bitbucket.rest.content.RestDiff;
import com.atlassian.bitbucket.rest.enrich.AvatarEnricher;
import com.atlassian.bitbucket.rest.RestErrors;
import com.atlassian.bitbucket.rest.util.*;
import com.atlassian.plugins.rest.common.security.AnonymousAllowed;
import com.atlassian.bitbucket.avatar.AvatarRequest;
import com.atlassian.bitbucket.comment.Comment;
import com.atlassian.bitbucket.content.DiffContentFilter;
import com.atlassian.bitbucket.content.DiffWhitespace;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.stash.internal.rest.content.JsonDiffContentCallback;
import com.atlassian.bitbucket.pull.PullRequest;
import com.atlassian.bitbucket.pull.PullRequestDiffRequest;
import com.atlassian.bitbucket.pull.PullRequestService;
import com.atlassian.bitbucket.rest.comment.RestComment;
import com.sun.jersey.spi.container.ContainerRequest;
import com.sun.jersey.spi.resource.Singleton;

import javax.ws.rs.*;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.util.function.Function;

import static com.atlassian.bitbucket.rest.util.CachePolicies.noCache;

@AnonymousAllowed
@Consumes(MediaType.APPLICATION_JSON)
@Path(ResourcePatterns.PULL_REQUEST_URI + "/diff")
@Produces(RestUtils.APPLICATION_JSON_UTF8)
@Singleton
public class PullRequestDiffResource extends AbstractPullRequestResource {

    private final AvatarEnricher avatarEnricher;

    public PullRequestDiffResource(I18nService i18nService, PullRequestService pullRequestService,
                                   AvatarEnricher avatarEnricher) {
        super(i18nService, pullRequestService);

        this.avatarEnricher = avatarEnricher;
    }

    /**
     * Streams a diff within a pull request.
     * <p>
     * If the specified file has been copied, moved or renamed, the <code>srcPath</code> must also be specified to
     * produce the correct diff.
     * <p>
     * Note: This RESTful endpoint is currently <i>not paged</i>. The server will internally apply a hard cap to the
     * streamed lines, and it is not possible to request subsequent pages if that cap is exceeded.
     * <p>
     * The authenticated user must have <strong>REPO_READ</strong> permission for the repository that this pull request
     * targets to call this resource.
     *
     * @param path         the path to the file which should be diffed (optional)
     * @param avatarSize   if present the service adds avatar URLs for comment authors where the provided value
     *                     specifies the desired avatar size in pixels
     * @param avatarScheme the security scheme for avatar URLs. If the scheme is not present then it is inherited
     *                     from the request. It can be set to "https" to force the use of secure URLs
     * @param contextLines the number of context lines to include around added/removed lines in the diff
     * @param filter       text used to filter files and lines (optional)
     * @param srcPath      the previous path to the file, if the file has been copied, moved or renamed
     * @param whitespace   optional whitespace flag which can be set to <code>ignore-all</code>
     * @param withComments <code>true</code> to embed comments in the diff (the default); otherwise, <code>false</code>
     *                     to stream the diff without comments
     *
     * @response.representation.200.mediaType application/json
     * @response.representation.200.qname diffs
     * @response.representation.200.example {@link RestDiff#PAGE_WITH_COMMENTS_EXAMPLE}
     * @response.representation.200.doc a page of differences from a pull request.
     *
     * @response.representation.400.mediaType application/json
     * @response.representation.400.qname errors
     * @response.representation.400.example {@link RestErrors#VALIDATION_EXAMPLE}
     * @response.representation.400.doc If the request was malformed.
     *
     * @response.representation.401.mediaType application/json
     * @response.representation.401.qname errors
     * @response.representation.401.example {@link RestErrors#EXAMPLE}
     * @response.representation.401.doc The currently authenticated user has insufficient permissions
     *                                  to view the repository or pull request.
     *
     * @response.representation.404.mediaType application/json
     * @response.representation.404.qname errors
     * @response.representation.404.example {@link RestErrors#EXAMPLE}
     * @response.representation.404.doc The repository or pull request does not exist.
     */
    @GET
    @Path("{path:.*}")
    public Response streamDiff(@Context PullRequest pullRequest,
                               @PathParam("path") String path,
                               @QueryParam("contextLines") @DefaultValue("-1") int contextLines,
                               @QueryParam("srcPath") String srcPath,
                               @QueryParam("whitespace") String whitespace,
                               @QueryParam("withComments") @DefaultValue("true") boolean withComments,
                               @Context DiffContentFilter filter,
                               @Context ContainerRequest request) {
        PullRequestDiffRequest diffRequest = new PullRequestDiffRequest.Builder(pullRequest, path)
                .contextLines(contextLines)
                .filter(filter)
                .srcPath(srcPath)
                .whitespace(DiffWhitespace.fromString(whitespace))
                .withComments(withComments)
                .build();
        Function<Comment, RestComment> commentTransformer = createAvatarCommentTransformer(request);

        return ResponseFactory.ok(new JsonStreamingOutput() {

            @Override
            public void write(StatefulJsonWriter writer) throws IOException, WebApplicationException {
                pullRequestService.streamDiff(diffRequest, new JsonDiffContentCallback(writer, commentTransformer));
            }
        }, noCache()).build(); //Pull request diffs can _never_ be cached, even without comments, because they move
    }

    //This method is intentionally undocumented. The REST doclet generator takes the docs from the first method
    //for overloaded methods and applies it to both.
    @GET
    public Response streamDiff(@Context PullRequest pullRequest,
                               @QueryParam("contextLines") @DefaultValue("-1") int contextLines,
                               @QueryParam("srcPath") String sourcePath,
                               @QueryParam("whitespace") String whitespace,
                               @QueryParam("withComments") @DefaultValue("true") boolean withComments,
                               @Context DiffContentFilter filter,
                               @Context ContainerRequest request) {
        //This method _only_ exists to get a "nicer" error when no path is specified. Otherwise, the error is
        //something about mappings, rather than an indicator that a path is required
        return streamDiff(pullRequest, null, contextLines, sourcePath, whitespace, withComments, filter, request);
    }

    /**
     * Returns a {@code Function} which translates a {@link Comment} to a {@link RestComment}.
     * <p>
     * If avatars have been requested, as determined by examining the provided {@code ContainerRequest}, the returned
     * function will automatically populate avatar URLs for comment authors.
     *
     * @param request the container request, used to determine whether avatars were requested
     * @return a function for transforming comments for REST
     */
    private Function<Comment, RestComment> createAvatarCommentTransformer(ContainerRequest request) {
        AvatarRequest avatarRequest = RestUtils.makeAvatarRequest(request);
        if (avatarRequest == null) {
            //If avatars have not been requested, a simple function transforming to a RestComment is enough
            return RestComment.REST_TRANSFORM;
        }

        //If avatars have been requested, return a function which, in addition to transforming to a RestComment, also
        //populates the avatar URL on the comment's author
        return new Function<Comment, RestComment>() {

            @Override
            public RestComment apply(Comment comment) {
                RestComment restComment = new RestComment(comment, this);
                avatarEnricher.enrich(restComment, avatarRequest);

                return restComment;
            }
        };
    }
}
