package uk.ac.ebi.biosamples.service;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

import javax.annotation.PostConstruct;

import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import com.mongodb.ErrorCategory;
import com.mongodb.MongoWriteException;

import uk.ac.ebi.biosamples.Messaging;
import uk.ac.ebi.biosamples.WebappProperties;
import uk.ac.ebi.biosamples.model.Sample;
import uk.ac.ebi.biosamples.mongo.model.MongoSample;
import uk.ac.ebi.biosamples.mongo.model.MongoSubmission;
import uk.ac.ebi.biosamples.mongo.repo.MongoSampleRepository;
import uk.ac.ebi.biosamples.mongo.repo.MongoSubmissionRepository;
import uk.ac.ebi.biosamples.mongo.service.MongoSampleToSampleConverter;
import uk.ac.ebi.biosamples.mongo.service.SampleToMongoSampleConverter;
import uk.ac.ebi.biosamples.solr.model.SolrSample;
import uk.ac.ebi.biosamples.solr.repo.SolrSampleRepository;

/**
 * Service layer business logic for centralising repository access and
 * conversions between different controllers. Use this instead of linking to
 * repositories directly.
 * 
 * @author faulcon
 *
 */
@Service
public class SampleService {

	@Autowired
	private MongoSampleRepository mongoSampleRepository;
	@Autowired
	private MongoSubmissionRepository mongoSubmissionRepository;

	@Autowired
	private SolrSampleRepository solrSampleRepository;

	@Autowired
	private InverseRelationshipConverter inverseRelationshipConverter;

	@Autowired
	private AmqpTemplate amqpTemplate;

	@Autowired
	private MongoSampleToSampleConverter mongoSampleToSampleConverter;
	@Autowired
	private SampleToMongoSampleConverter sampleToMongoSampleConverter;
	
	@Autowired
	private WebappProperties webappProperties;

	private BlockingQueue<String> accessionCandidateQueue;;
	private long accessionCandidateCounter;
	
	@PostConstruct
	public void doSetup() {
		accessionCandidateQueue = new LinkedBlockingQueue<>(webappProperties.getAcessionQueueSize());
		accessionCandidateCounter = webappProperties.getAccessionMinimum();
	}

	/**
	 * Throws an IllegalArgumentException of no sample with that accession exists
	 * 
	 * @param accession
	 * @return
	 * @throws IllegalArgumentException
	 */
	public Sample fetch(String accession) throws IllegalArgumentException {
		// return the raw sample from the repository
		MongoSample mongoSample = mongoSampleRepository.findOne(accession);
		if (mongoSample == null) {
			throw new IllegalArgumentException("Unable to find sample (" + accession + ")");
		}

		// convert it into the format to return
		Sample sample = mongoSampleToSampleConverter.convert(mongoSample);
		
		// add any additional inverse relationships
		sample = inverseRelationshipConverter.convert(sample);
		
		return sample;
	}

	public Page<Sample> fetchFindAll(Pageable pageable) {
		// return the samples from solr that match the query
		Page<SolrSample> pageSolrSample = solrSampleRepository.findPublic(pageable);
		// for each result fetch the version from Mongo and add inverse relationships
		Page<Sample> pageSample = pageSolrSample.map(s -> fetch(s.getAccession()));
		return pageSample;
	}

	public Page<Sample> fetchFindByText(String text, Pageable pageable) {
		// return the samples from solr that match the query
		Page<SolrSample> pageSolrSample = solrSampleRepository.findByTextAndPublic(text, pageable);
		// for each result fetch the version from Mongo and add inverse relationships
		Page<Sample> pageSample = pageSolrSample.map(s -> fetch(s.getAccession()));
		return pageSample;
	}

	public Sample store(Sample sample) {

		// TODO check if there is an existing copy and if there are any changes
		
		// save the submission in the repository
		mongoSubmissionRepository.save(new MongoSubmission(sample));

		// TODO validate that relationships have this sample as the source 

		// convert it to the storage specific version
		MongoSample mongoSample = sampleToMongoSampleConverter.convert(sample);
		// save the sample in the repository
		if (mongoSample.hasAccession()) {
			//update the existing accession
			mongoSampleRepository.save(mongoSample);
		} else {
			//assign it an accession
			accessionAndInsert(mongoSample);
			//update the sample object with the assigned accession
			sample = Sample.build(sample.getName(), mongoSample.getAccession(), sample.getRelease(), sample.getUpdate(),
					sample.getAttributes(), sample.getRelationships());
		}
		// send a message for further processing
		amqpTemplate.convertAndSend(Messaging.exchangeForIndexing, "", sample);
		//return the sample in case we have modified it i.e accessioned
		return sample;
	}

	private void accessionAndInsert(MongoSample sample) {
		// inspired by Optimistic Loops of
		// https://docs.mongodb.com/v3.0/tutorial/create-an-auto-incrementing-field/
		boolean success = false;
		// TODO limit number of tries
		while (!success) {
			// TODO add a timeout here
			try {
				sample.accession = accessionCandidateQueue.take();
			} catch (InterruptedException e) {
				throw new RuntimeException(e);
			}

			try {
				mongoSampleRepository.insertNew(sample);
				success = true;
			} catch (MongoWriteException e) {
				if (e.getError().getCategory() == ErrorCategory.DUPLICATE_KEY) {
					success = false;
					sample.accession = null;
				} else {
					throw e;
				}
			}
		}
	}

	@Scheduled(fixedDelay = 100)
	public void prepareAccessions() {
		while (accessionCandidateQueue.remainingCapacity() > 0) {
			String accessionCandidate = webappProperties.getAccessionPrefix() + accessionCandidateCounter;
			// if the accession already exists, skip it
			if (mongoSampleRepository.exists(accessionCandidate)) {
				accessionCandidateCounter += 1;
				// if the accession can't be put in the queue at this time
				// (queue full), stop
			} else if (!accessionCandidateQueue.offer(accessionCandidate)) {
				return;
			} else {
				//put it into the queue, move on to next
				accessionCandidateCounter += 1;
			}
		}
	}
}

@@ -326,24 +326,83 @@
 .Autowired;%0A
+import org.springframework.core.convert.ConversionService;%0A
 import org.s
@@ -1781,32 +1781,110 @@
 pleRepository;%0A%0A
+%09//this has the same in and out class, so can't be used via conversionservice%0A
 %09@Autowired%0A%09pri
@@ -1996,16 +1996,17 @@
 mplate;%0A
+%09
 %0A%09@Autow
@@ -2023,145 +2023,43 @@
 ate 
-MongoSampleToSampleConverter mongoSampleToSampleConverter;%0A%09@Autowired%0A%09private SampleToMongoSampleConverter sampleToMongoSampleConverter
+ConversionService conversionService
 ;%0A%09%0A
@@ -2962,36 +2962,25 @@
 e = 
-mongoSampleToSampleConverter
+conversionService
 .con
@@ -2991,24 +2991,38 @@
 (mongoSample
+, Sample.class
 );%0A%09%09%0A%09%09// a
@@ -4291,36 +4291,25 @@
 e = 
-sampleToMongoSampleConverter
+conversionService
 .con
@@ -4315,24 +4315,43 @@
 nvert(sample
+, MongoSample.class
 );%0A%09%09// save

use conversion service