/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.cxf.systest.jaxrs;


import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import javax.ws.rs.GET;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.container.AsyncResponse;
import javax.ws.rs.container.CompletionCallback;
import javax.ws.rs.container.ConnectionCallback;
import javax.ws.rs.container.Suspended;
import javax.ws.rs.container.TimeoutHandler;
import javax.ws.rs.core.Response;

import org.apache.cxf.phase.PhaseInterceptorChain;

@Path("/bookstore")
public class BookContinuationStore implements BookAsyncInterface {

    private Map<String, String> books = new HashMap<>();
    private Executor executor = new ThreadPoolExecutor(5, 5, 0, TimeUnit.SECONDS,
                                        new ArrayBlockingQueue<>(10));

    public BookContinuationStore() {
        init();
    }

    @GET
    @Path("/books/defaulttimeout")
    public void getBookDescriptionWithTimeout(@Suspended AsyncResponse async) {
        async.register(new CallbackImpl());
        async.setTimeout(2000, TimeUnit.MILLISECONDS);
    }

    @GET
    @Path("/books/resume")
    @Produces("text/plain")
    public void getBookDescriptionImmediateResume(@Suspended AsyncResponse async) {
        async.resume("immediateResume");
    }
    @GET
    @Path("/books/resumeFromFastThread")
    @Produces("text/plain")
    public void getBookDescriptionResumeFromFastThread(@Suspended AsyncResponse async) {
        executor.execute(new Runnable() {
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (Exception ex) {
                    // ignore
                }
                async.resume("resumeFromFastThread");
            }
        });
    }

    @GET
    @Path("/books/nocontent")
    @Produces("text/plain")
    public void getBookNoContent(AsyncResponse async) {
        async.resume(null);
    }


    public void getBookNoContentInterface(@Suspended AsyncResponse async) {
        async.resume(Response.status(206).build());
    }

    @GET
    @Path("/books/cancel")
    public void getBookDescriptionWithCancel(@PathParam("id") String id,
                                             @Suspended AsyncResponse async) {
        PhaseInterceptorChain.getCurrentMessage().getClass();
        async.setTimeout(2000, TimeUnit.MILLISECONDS);
        async.setTimeoutHandler(new CancelTimeoutHandlerImpl());
    }

    @GET
    @Path("/books/timeouthandler/{id}")
    public void getBookDescriptionWithHandler(@PathParam("id") String id,
                                              @Suspended AsyncResponse async) {
        async.setTimeout(1000, TimeUnit.MILLISECONDS);
        async.setTimeoutHandler(new TimeoutHandlerImpl(id, false));
    }

    @GET
    @Path("/books/timeouthandlerresume/{id}")
    public void getBookDescriptionWithHandlerResumeOnly(@PathParam("id") String id,
                                                        @Suspended AsyncResponse async) {
        async.setTimeout(1000, TimeUnit.MILLISECONDS);
        async.setTimeoutHandler(new TimeoutHandlerImpl(id, true));
    }

    @GET
    @Path("/books/{id}")
    public void getBookDescription(@PathParam("id") String id,
                                   @Suspended AsyncResponse async) {
        handleContinuationRequest(id, async);
    }

    @Path("/books/subresources/")
    public BookContinuationStore getBookStore() {

        return this;

    }

    @GET
    @Path("{id}")
    public void handleContinuationRequest(@PathParam("id") String id,
                                          @Suspended AsyncResponse response) {
        resumeSuspended(id, response);
    }

    @GET
    @Path("books/notfound")
    @Produces("text/plain")
    public void handleContinuationRequestNotFound(@Suspended AsyncResponse response) {
        response.register(new CallbackImpl());
        resumeSuspendedNotFound(response);
    }

    @GET
    @Path("books/notfound/unmapped")
    @Produces("text/plain")
    public void handleContinuationRequestNotFoundUnmapped(@Suspended AsyncResponse response) {
        response.register(new CallbackImpl());
        resumeSuspendedNotFoundUnmapped(response);
    }

    @GET
    @Path("books/notfound/unmappedImmediate")
    @Produces("text/plain")
    public void handleUnmappedImmediate(@Suspended AsyncResponse response) throws BookNotFoundFault {
        throw new BookNotFoundFault("");
    }
    @GET
    @Path("books/mappedImmediate")
    @Produces("text/plain")
    public void handleMappedImmediate(@Suspended AsyncResponse response) throws BookNotFoundFault {
        throw new WebApplicationException(Response.status(401).build());
    }

    @GET
    @Path("books/unmappedFromFilter")
    @Produces("text/plain")
    public void handleContinuationRequestUnmappedFromFilter(@Suspended AsyncResponse response) {
        response.register(new CallbackImpl());
        response.resume(Response.ok().build());
    }

    @GET
    @Path("books/suspend/unmapped")
    @Produces("text/plain")
    public void handleNotMappedAfterSuspend(@Suspended AsyncResponse response) throws BookNotFoundFault {
        response.setTimeout(2000, TimeUnit.MILLISECONDS);
        response.setTimeoutHandler(new CancelTimeoutHandlerImpl());
        throw new BookNotFoundFault("");
    }

    @GET
    @Path("/disconnect")
    public void handleClientDisconnects(@Suspended AsyncResponse response) {
        response.setTimeout(0, TimeUnit.SECONDS);

        response.register(new ConnectionCallback() {
            @Override
            public void onDisconnect(AsyncResponse disconnected) {
                System.out.println("ConnectionCallback: onDisconnect, client disconnects");
            }
        });

        try {
            Thread.sleep(3000);
        } catch (InterruptedException ex) {
            // ignore
        }

        response.resume(books.values().toString());
    }


    private void resumeSuspended(final String id, final AsyncResponse response) {

        executor.execute(new Runnable() {
            public void run() {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException ex) {
                    // ignore
                }
                response.resume(books.get(id));
            }
        });

    }

    private void resumeSuspendedNotFound(final AsyncResponse response) {

        executor.execute(new Runnable() {
            public void run() {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException ex) {
                    // ignore
                }
                response.resume(new NotFoundException());
            }
        });

    }

    private void resumeSuspendedNotFoundUnmapped(final AsyncResponse response) {
        executor.execute(new Runnable() {
            public void run() {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException ex) {
                    // ignore
                }
                response.resume(new BookNotFoundFault(""));
            }
        });

    }

    private void init() {
        books.put("1", "CXF in Action1");
        books.put("2", "CXF in Action2");
        books.put("3", "CXF in Action3");
        books.put("4", "CXF in Action4");
        books.put("5", "CXF in Action5");
    }

    private class TimeoutHandlerImpl implements TimeoutHandler {
        private boolean resumeOnly;
        private String id;
        private AtomicInteger timeoutExtendedCounter = new AtomicInteger();

        TimeoutHandlerImpl(String id, boolean resumeOnly) {
            this.id = id;
            this.resumeOnly = resumeOnly;
        }

        @Override
        public void handleTimeout(AsyncResponse asyncResponse) {
            if (!resumeOnly && timeoutExtendedCounter.addAndGet(1) <= 2) {
                asyncResponse.setTimeout(1, TimeUnit.SECONDS);
            } else {
                asyncResponse.resume(books.get(id));
            }
        }

    }

    private class CancelTimeoutHandlerImpl implements TimeoutHandler {

        @Override
        public void handleTimeout(AsyncResponse asyncResponse) {
            asyncResponse.cancel(10);

        }

    }

    private class CallbackImpl implements CompletionCallback {

        @Override
        public void onComplete(Throwable throwable) {
            System.out.println("CompletionCallback: onComplete, throwable: " + throwable);
        }

    }


}


