package org.jboss.resteasy.test.cdi.injection.resource;

import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.jboss.resteasy.test.cdi.util.Constants;
import org.jboss.resteasy.test.cdi.util.Counter;
import org.jboss.resteasy.test.cdi.util.CounterBinding;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.ejb.EJB;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.context.Dependent;
import javax.enterprise.context.RequestScoped;
import javax.enterprise.context.SessionScoped;
import javax.enterprise.inject.Instance;
import javax.enterprise.inject.New;
import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.BeanManager;
import javax.inject.Inject;
import javax.jms.DeliveryMode;
import javax.jms.JMSException;
import javax.jms.MessageConsumer;
import javax.jms.MessageProducer;
import javax.jms.Queue;
import javax.jms.Session;
import javax.jms.TextMessage;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.GenericEntity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.lang.annotation.Annotation;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Logger;

@Path("/")
@RequestScoped
public class CDIInjectionBookResource {
   public static final String BOOK_READER = "BookReader";
   public static final String BOOK_WRITER = "BookWriter";
   public static final String BOOK_RESOURCE = "BookResource";
   public static final String BOOK_READER_DEPENDENT = "BookReaderDependent";
   public static final String BOOK_WRITER_DEPENDENT = "BookWriterDependent";
   public static final String BOOK_RESOURCE_DEPENDENT = "BookResourceDependent";
   public static final String BOOK_READER_STATEFUL = "BookReaderStateless";
   public static final String BOOK_WRITER_STATEFUL = "BookWriterStateless";
   public static final String BOOK_RESOURCE_STATEFUL = "BookResourceStateless";
   public static final String BOOK_RESOURCE_STATEFUL2 = "BookResourceStateless2";
   public static final String COUNTER = "counter";
   public static final String COLLECTION = "collection";
   public static final String BOOK_BAG = "bookBag";
   public static final String NEW_BEAN_APPLICATION_SCOPED = "newBeanApplicationScoped";
   public static final String NEW_BEAN_DEPENDENT_SCOPED = "newBeanDependentScoped";
   public static final String STEREOTYPED_APPLICATION_SCOPED = "stereotypedApplicationScoped";
   public static final String STEREOTYPED_DEPENDENT_SCOPED = "stereotypedDependentScoped";

   private static HashMap<String, Object> store;
   private static AtomicInteger constructCounter = new AtomicInteger();
   private static AtomicInteger destroyCounter = new AtomicInteger();
   private static CountDownLatch latch = new CountDownLatch(2);
   @Inject
   @CDIInjectionResourceBinding
   @PersistenceContext(unitName = "test")
   EntityManager em;
   private HashSet<CDIInjectionBook> set = new HashSet<CDIInjectionBook>();
   @Inject
   private BeanManager beanManager;
   @Inject
   private int secret;                 // used to determine identity
   @Inject
   private CDIInjectionDependentScoped dependent;  // dependent scoped managed bean
   @Inject
   @CounterBinding
   private Counter counter;  // application scoped singleton: injected as Weld proxy
   @EJB
   private CDIInjectionBookCollection collection;           // application scoped singleton: injected as EJB proxy
   // @Note: stateful and stateful2 are two very different objects.
   // stateful is an EJB proxy and stateful2 is a Weld proxy.
   @EJB
   private CDIInjectionStatefulEJB stateful;       // dependent scoped SLSB
   @Inject
   private CDIInjectionStatefulEJB stateful2;      // dependent scoped SLSB
   @Inject
   private CDIInjectionBookBagLocal bookBag;       // session scoped SFSB
   @Inject
   @CDIInjectionResourceBinding
   private Session session;
   @Inject
   @CDIInjectionResourceBinding
   private Queue bookQueue;
   @Inject
   private CDIInjectionNewBean newBean1;
   @Inject
   @New
   private CDIInjectionNewBean newBean2;
   @Inject
   private CDIInjectionStereotypedApplicationScope stereotypeApplicationScoped;
   @Inject
   private CDIInjectionStereotypedDependentScope stereotypedRequestScoped;
   private Logger log;

   public static HashMap<String, Object> getStore() {
      return store;
   }

   public static void setStore(HashMap<String, Object> store) {
      CDIInjectionBookResource.store = store;
   }

   public HashSet<CDIInjectionBook> getSet() {
      return set;
   }

   public void setSet(HashSet<CDIInjectionBook> set) {
      this.set = set;
   }

   @PreDestroy
   public void preDestroy() {
      destroyCounter.incrementAndGet();
      log.info("preDestroy(): destroyCounter: " + destroyCounter.get());
   }

   @PostConstruct
   public void postConstruct() {
      constructCounter.incrementAndGet();
      log.info("postConstruct(): constructCounter: " + constructCounter.get());
   }

   public CountDownLatch getCountDownLatch() {
      log.info("latch.getCount(): " + latch.getCount());
      return latch;
   }

   @Inject
   public void init(Instance<Logger> logInstance) {
      this.log = logInstance.get();
   }

   @GET
   @Path("verifyScopes")
   @Produces(MediaType.TEXT_PLAIN)
   public Response verifyScopes() {
      log.info("entering verifyScopes()");
      ResteasyProviderFactory factory = ResteasyProviderFactory.getInstance();
      CDIInjectionBookReader reader = CDIInjectionBookReader.class.cast(factory.getMessageBodyReader(CDIInjectionBook.class, null, null, Constants.MEDIA_TYPE_TEST_XML_TYPE));
      CDIInjectionBookWriter writer = CDIInjectionBookWriter.class.cast(factory.getMessageBodyWriter(CDIInjectionBook.class, null, null, Constants.MEDIA_TYPE_TEST_XML_TYPE));

      if (store == null) {
         log.info("Counter scope:          " + getScope(Counter.class));
         log.info("BookCollection scope:   " + getScope(CDIInjectionBookCollection.class));
         log.info("BookResource scope:     " + getScope(CDIInjectionBookResource.class));
         log.info("BookReader scope:       " + getScope(CDIInjectionBookReader.class));
         log.info("BookWriter scope:       " + getScope(CDIInjectionBookWriter.class));
         log.info("UnscopedResource scope: " + getScope(CDIInjectionUnscopedResource.class));
         log.info("DependentScoped scope:  " + getScope(CDIInjectionDependentScoped.class));
         log.info("StatelessEJB scope:     " + getScope(CDIInjectionStatefulEJB.class));
         log.info("BookBagLocal scope:     " + getScope(CDIInjectionBookBagLocal.class));
         log.info("NewBean scope:          " + getScope(CDIInjectionNewBean.class));
         log.info("stereotypeApplicationScoped: " + getScope(CDIInjectionStereotypedApplicationScope.class));
         log.info("stereotypeRequestScoped:     " + getScope(CDIInjectionStereotypedDependentScope.class));

         store = new HashMap<String, Object>();
         store.put(BOOK_READER, reader);
         store.put(BOOK_WRITER, writer);
         store.put(BOOK_RESOURCE, this);
         store.put(BOOK_READER_DEPENDENT, reader.getDependent());
         store.put(BOOK_WRITER_DEPENDENT, writer.getDependent());
         store.put(BOOK_RESOURCE_DEPENDENT, dependent);
         store.put(BOOK_READER_STATEFUL, reader.getStateful());
         store.put(BOOK_WRITER_STATEFUL, writer.getStateful());
         store.put(BOOK_RESOURCE_STATEFUL, stateful);
         store.put(BOOK_RESOURCE_STATEFUL2, stateful2);
         store.put(COUNTER, counter);
         store.put(COLLECTION, collection);
         store.put(BOOK_BAG, bookBag);
         store.put(NEW_BEAN_APPLICATION_SCOPED, newBean1);
         store.put(NEW_BEAN_DEPENDENT_SCOPED, newBean2);
         store.put(STEREOTYPED_APPLICATION_SCOPED, stereotypeApplicationScoped);
         store.put(STEREOTYPED_DEPENDENT_SCOPED, stereotypedRequestScoped);
         return Response.ok().build();
      }

      if (isApplicationScoped(Counter.class) &&
            isApplicationScoped(CDIInjectionBookCollection.class) &&
            isApplicationScoped(CDIInjectionBookReader.class) &&
            isApplicationScoped(CDIInjectionBookWriter.class) &&
            isRequestScoped(CDIInjectionBookResource.class) &&
            isDependentScoped(CDIInjectionDependentScoped.class) &&
            isDependentScoped(CDIInjectionStatefulEJB.class) &&
            isRequestScoped(CDIInjectionUnscopedResource.class) &&
            isSessionScoped(CDIInjectionBookBagLocal.class) &&
            isApplicationScoped(CDIInjectionNewBean.class) &&
            isApplicationScoped(CDIInjectionStereotypedApplicationScope.class) &&
            isDependentScoped(CDIInjectionStereotypedDependentScope.class) &&
            store.get(BOOK_READER) == reader &&
            store.get(BOOK_WRITER) == writer &&
            store.get(BOOK_RESOURCE) != this &&
            store.get(BOOK_READER_DEPENDENT) == reader.getDependent() &&
            store.get(BOOK_WRITER_DEPENDENT) == writer.getDependent() &&
            store.get(BOOK_RESOURCE_DEPENDENT) != dependent &&
            store.get(BOOK_READER_STATEFUL).equals(reader.getStateful()) &&
            store.get(BOOK_WRITER_STATEFUL).equals(writer.getStateful()) &&
            !store.get(BOOK_RESOURCE_STATEFUL).equals(stateful) &&
            !store.get(BOOK_RESOURCE_STATEFUL2).equals(stateful2) &&
            store.get(COUNTER).equals(counter) &&
            store.get(COLLECTION).equals(collection) &&
            store.get(BOOK_BAG).equals(bookBag) &&
            store.get(NEW_BEAN_APPLICATION_SCOPED).equals(newBean1) &&
            !store.get(NEW_BEAN_DEPENDENT_SCOPED).equals(newBean2) &&
            !newBean1.equals(newBean2) &&
            store.get(STEREOTYPED_APPLICATION_SCOPED).equals(stereotypeApplicationScoped) &&
            !store.get(STEREOTYPED_DEPENDENT_SCOPED).equals(stereotypedRequestScoped)
      ) {
         return Response.ok().build();
      } else {
         return Response.serverError().build();
      }
   }

   @POST
   @Path("empty")
   public void empty() {
      collection.empty();
   }

   @POST
   @Path("create")
   @Consumes("application/test+xml")
   @Produces(MediaType.TEXT_PLAIN)
   public Response createBook(CDIInjectionBook book) {
      log.info("entering createBook()");
      int id = counter.getNext();
      book.setId(id);
      collection.addBook(book);
      log.info("stored: " + id + "->" + book);
      return Response.ok(id).build();
   }

   @GET
   @Produces(MediaType.APPLICATION_XML)
   @Path("books")
   public Collection<CDIInjectionBook> listAllMembers() {
      log.info("entering listAllMembers()");
      log.info("this.theSecret(): " + this.theSecret());
      Collection<CDIInjectionBook> books = collection.getBooks();
      log.info("listAllMembers(): " + books);
      return books;
   }

   @GET
   @Path("book/{id:[0-9][0-9]*}")
   @Produces("application/test+xml")
   public CDIInjectionBook lookupBookById(@PathParam("id") int id) {
      log.info("entering lookupBookById(" + id + ")");
      log.info("books: " + collection.getBooks());
      CDIInjectionBook book = collection.getBook(id);
      if (book == null) {
         throw new WebApplicationException(Response.Status.NOT_FOUND);
      }
      return book;
   }

   @POST
   @Path("entityManager")
   public Response testEntityManager() {
      log.info("entering testEntityManager()");
      CDIInjectionBook book1 = collection.getBook(Counter.INITIAL_VALUE);
      CDIInjectionBook book2 = em.find(CDIInjectionBook.class, Counter.INITIAL_VALUE);
      return book1.equals(book2) ? Response.ok().build() : Response.serverError().build();

   }

   @POST
   @Path("session/add")
   public Response sessionAdd(@Context HttpServletRequest request, CDIInjectionBook book) {
      log.info("entering sessionAdd()");
      log.info("new session: " + request.getSession().isNew());
      bookBag.addBook(book);
      return Response.ok().build();
   }

   @GET
   @Path("session/get")
   @Produces(MediaType.APPLICATION_XML)
   public Collection<CDIInjectionBook> sessionGetBag(@Context HttpServletRequest request) {
      log.info("entering sessionGetBag()");
      log.info("new session: " + request.getSession().isNew());
      Collection<CDIInjectionBook> books = bookBag.getContents();
      log.info("sessionGetBag(): " + books);
      request.getSession().invalidate();
      return books;
   }

   @POST
   @Path("session/test")
   public Response sessionTest(@Context HttpServletRequest request) {
      log.info("entering sessionTest()");
      log.info("new session: " + request.getSession().isNew());
      Collection<CDIInjectionBook> contents = bookBag.getContents();
      log.info("bookBag: " + contents);
      if (request.getSession().isNew() && contents.isEmpty()) {
         return Response.ok().build();
      } else {
         return Response.serverError().build();
      }
   }

   @GET
   @Path("mdb/get")
   @Produces(MediaType.APPLICATION_XML)
   public GenericEntity<Collection<CDIInjectionBook>> mdbGetBag() {
      log.info("entering mdbGetBag()");
      Collection<CDIInjectionBook> books = bookBag.getContents();
      log.info("sessionGetBag(): " + books);
      return new GenericEntity<Collection<CDIInjectionBook>>(books) {
      };
   }

   @GET
   @Path("getCounters")
   public String getCounters() {
      return Integer.toString(constructCounter.get()) + ":" + Integer.toString(destroyCounter.get()) + ":";
   }

   @GET
   @Path("disposer")
   public Response testDisposer() {
      log.info("entering testDisposer()");
      return CDIInjectionResourceProducer.isDisposed() ? Response.ok().build() : Response.serverError().build();
   }

   @POST
   @Path("produceMessage")
   @Consumes("application/test+xml")
   @Produces(MediaType.TEXT_PLAIN)
   public Response produceBookMessage(CDIInjectionBook book) {
      log.info("entering produceBookMessage()");
      try {
         log.info("queue: " + bookQueue);
         log.info("ResourceProducer scope: " + getScope(CDIInjectionResourceProducer.class));
         log.info("queue scope: " + getScope(Queue.class));
         MessageProducer producer = session.createProducer(bookQueue);
         TextMessage message = session.createTextMessage(book.getName());
         producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
         producer.send(message);
         log.info("sent: " + message.getText());
         return Response.ok().build();
      } catch (JMSException e) {
         log.info(String.format("Stacktrace: %s", (Object[]) e.getStackTrace()));
         return Response.serverError().entity("JMS error: " + e.getMessage()).build();
      }
   }

   @GET
   @Path("queue/consumeMessage")
   @Produces(MediaType.TEXT_PLAIN)
   public Response consumeBookMessageFromQueue() {
      log.info("entering consumeBookMessageFromQueue() xxx");
      log.info("getting consumer");
      try {
         MessageConsumer consumer = session.createConsumer(bookQueue);
         log.info("got consumer");
         TextMessage message = (TextMessage) consumer.receiveNoWait();
         log.info("message: " + message);
         if (message == null) {
            return Response.serverError().build();
         }
         log.info("message text: " + message.getText());
         return Response.ok(message.getText()).build();
      } catch (JMSException e) {
         return Response.serverError().entity("JMS error: " + e.getMessage()).build();
      }
   }

   @GET
   @Path("mdb/consumeMessage")
   @Produces(MediaType.TEXT_PLAIN)
   public Response consumeBookMessageFromMDB() throws InterruptedException {
      log.info("entering consumeBookMessageFromMDB()");
      getCountDownLatch().await(5000, TimeUnit.MILLISECONDS); // Wait until BookMDB has stored book in BookCollection.
      CDIInjectionBookCollection collection = getBookCollection();
      log.info("consumeBookMessageFromMDB(): collection.size(): " + collection.getBooks().size());
      if (collection.getBooks().size() == 1) {
         String name = collection.getBooks().iterator().next().getName();
         log.info("got book name: " + name);
         return Response.ok(name).build();
      } else {
         return Response.serverError().entity("Collection size: " + collection.getBooks().size()).build();
      }
   }

   @GET
   @Produces(MediaType.APPLICATION_XML)
   @Path("mdb/books")
   public Collection<CDIInjectionBook> getBooksMDB() throws InterruptedException {
      log.info("entering getBooksMDB()");
      log.info("getBooksMDB(): waiting on latch");
      latch.await();
      log.info("this.theSecret(): " + this.theSecret());
      Collection<CDIInjectionBook> books = collection.getBooks();
      log.info("getBooksMDB(): " + books);
      return books;
   }

   CDIInjectionBookCollection getBookCollection() {
      log.info("entering getBookCollection()");
      return collection;
   }

   Counter getCounter() {
      log.info("returning: " + counter);
      return counter;
   }

   boolean isApplicationScoped(Class<?> c) {
      return testScope(c, ApplicationScoped.class);
   }

   boolean isDependentScoped(Class<?> c) {
      return testScope(c, Dependent.class);
   }

   boolean isRequestScoped(Class<?> c) {
      return testScope(c, RequestScoped.class);
   }

   boolean isSessionScoped(Class<?> c) {
      return testScope(c, SessionScoped.class);
   }

   boolean testScope(Class<?> c, Class<?> scopeClass) {
      Class<? extends Annotation> annotation = getScope(c);
      if (annotation == null) {
         return false;
      }
      return annotation.isAssignableFrom(scopeClass);
   }

   Class<? extends Annotation> getScope(Class<?> c) {
      Set<Bean<?>> beans = beanManager.getBeans(c);
      Iterator<Bean<?>> it = beans.iterator();
      if (it.hasNext()) {
         Bean<?> bean = beans.iterator().next();
         return bean.getScope();
      }
      return null;
   }

   public boolean theSame(CDIInjectionBookResource that) {
      return this.secret == that.secret;
   }

   public int theSecret() {
      return secret;
   }
}
