/*
 * © 2013-2019 by Zappware. 
 * All Rights Reserved. No part of this application or any of its contents may be reproduced, 
 * copied, modified or adapted, without the prior written consent of Zappware.
 *
 * Website: http://www.zappware.com
 * E-Mail: info@zappware.com
 */
package com.zappware.graphlperftest;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.module.mrbean.MrBeanModule;
import com.zappware.graphlperftest.datafetchers.QueryOneDataFetcher;
import com.zappware.graphlperftest.datafetchers.QueryThreeDataFetcher;
import com.zappware.graphlperftest.datafetchers.QueryTwoDataFetcher;
import com.zappware.graphlperftest.model.ModelGenerator;
import com.zappware.graphlperftest.processing.CPULoadGenerator;

import graphql.ExecutionInput;
import graphql.ExecutionResult;
import graphql.GraphQL;
import graphql.TypeResolutionEnvironment;
import graphql.schema.GraphQLObjectType;
import graphql.schema.GraphQLSchema;
import graphql.schema.TypeResolver;
import graphql.schema.idl.InterfaceWiringEnvironment;
import graphql.schema.idl.RuntimeWiring;
import graphql.schema.idl.SchemaGenerator;
import graphql.schema.idl.SchemaParser;
import graphql.schema.idl.TypeDefinitionRegistry;
import graphql.schema.idl.WiringFactory;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.MediaType;

/**
 * The Servlet.
 */
@WebServlet("/graphql/*")
public class GraphQLServlet extends HttpServlet
{
	/** The serial version UID. */
	private static final long serialVersionUID = -8870822796905085361L;
	
	/** The JSON objectMapper. */
	private ObjectMapper objectMapper;
	/** The GraphQL schema. */
	private GraphQLSchema graphqlSchema;
	
	/** Allows the simulation of actual processing by generating some CPU load using lists. */
	private CPULoadGenerator cpuLoadGenerator;

	/**
	 * @see javax.servlet.GenericServlet#init()
	 */
	@Override
	public void init() throws ServletException
	{
		try
		{
			createLoadGenerator();
			createObjectMapper();
			loadSchema();
		}
		catch (IOException ioe)
		{
			throw new ServletException("An error occurred...", ioe);
		}
	}
	
	/**
	 * @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
	 */
	@Override
	protected void doGet(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws IOException
	{
		// Allow updating of the processing settings
		String returnData = httpServletRequest.getParameter("returnData");
		String returnResponse = httpServletRequest.getParameter("returnResponse");
		String cpuLoadSize = httpServletRequest.getParameter("cpuLoadSize");
		
		if (returnData != null)
			ProcessingSettings.returnFromDataFetcher = Boolean.parseBoolean(returnData);
		if (returnResponse != null)
			ProcessingSettings.returnResponse = Boolean.parseBoolean(returnResponse);
		if (cpuLoadSize != null)
		{
			ProcessingSettings.cpuLoadSize = Integer.parseInt(cpuLoadSize);
			cpuLoadGenerator.setLoadSize(ProcessingSettings.cpuLoadSize);
		}
		
		ProcessingSettings.printProcessingSettings();
		
		httpServletResponse.getWriter().print("OK");
	}
	
	/**
	 * @see javax.servlet.http.HttpServlet#doPost(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
	 */
	@Override
	protected void doPost(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws IOException
	{
		String query;
		
		String path = httpServletRequest.getPathInfo();
		if (path == null)
			path = httpServletRequest.getServletPath();
		
		// Allow retrieval of the schema.json file for API reflection
		if (path.contentEquals("/schema.json")) 
		{
			query = getSchemaQuery();
		}
		// Execute as a normal query
		else
		{
			InputStream inputStream = httpServletRequest.getInputStream();
			
			JsonNode requestNode = this.objectMapper.readTree(inputStream);
			JsonNode queryNode = requestNode.get("query");
			query = (queryNode != null) ? queryNode.asText() : null;
		}
		
		GraphQL graphQL = GraphQL.newGraphQL(this.graphqlSchema).build();
		
		ExecutionResult executionResult = graphQL.execute(ExecutionInput.newExecutionInput()
				.query(query)
				.build());
		
		writeResponse(executionResult, httpServletResponse);
	}
	
	/**
	 * Write a graphQL response to the HTTP servlet response.
	 * @param executionResult
	 * @param httpServletResponse
	 */
	private void writeResponse(ExecutionResult executionResult, HttpServletResponse httpServletResponse) throws IOException
	{
		httpServletResponse.setStatus(HttpServletResponse.SC_OK);

		if (ProcessingSettings.returnResponse)
		{
			Map<String, Object> data = new HashMap<>();
			
			httpServletResponse.setContentType(MediaType.APPLICATION_JSON);
			httpServletResponse.setCharacterEncoding("UTF-8");

			// Generate the body data
			if (executionResult.getData() != null)
			{
				// According to the GraphQL spec, if processing had already started on the request and no data is present due to a processing error,
				// an empty data object should be included instead of no data object. In here, we use the Optional java object to indicate that no
				// data is present, but the data tag should be included in the response.
				if ((executionResult.getData() instanceof Optional) && (!((Optional<?>) executionResult.getData()).isPresent()))
				{
					data.put("data", null);
				}
				else
				{
					data.put("data", executionResult.getData());
				}
			}
			if ((executionResult.getErrors() != null) && (!executionResult.getErrors().isEmpty()))
			{
				data.put("errors", executionResult.getErrors());
			}
	
			String responseString = this.objectMapper.writeValueAsString(data);
			
			httpServletResponse.getWriter().write(responseString);
		}
	}
	
	/**
	 * Create the CPU load generator.
	 */
	private void createLoadGenerator()
	{
		this.cpuLoadGenerator = new CPULoadGenerator();
		cpuLoadGenerator.setLoadSize(ProcessingSettings.cpuLoadSize);
	}
	
	/**
	 * Create the JSON object mapper.
	 */
	private void createObjectMapper()
	{
		this.objectMapper = new ObjectMapper();
		this.objectMapper.registerModule(new MrBeanModule());
	}
	
	/**
	 * Load the GraphQL schema.
	 */
	private void loadSchema() throws IOException
	{
		ModelGenerator modelGenerator = new ModelGenerator();
		SchemaParser schemaParser = new SchemaParser();
		SchemaGenerator schemaGenerator = new SchemaGenerator();

		TypeDefinitionRegistry typeRegistry;
		try (
				InputStream is = this.getClass().getResourceAsStream("/schema.graphql");
				InputStreamReader isr = new InputStreamReader(is);
				BufferedReader br = new BufferedReader(isr);
			)
		{
			typeRegistry = schemaParser.parse(br);
		}
		
		RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring()
				.type("TheQueries", builder -> builder.dataFetcher("queryOne", new QueryOneDataFetcher(modelGenerator, cpuLoadGenerator)))
				.type("TheQueries", builder -> builder.dataFetcher("queryTwo", new QueryTwoDataFetcher(modelGenerator, cpuLoadGenerator)))
				.type("TheQueries", builder -> builder.dataFetcher("queryThree", new QueryThreeDataFetcher(modelGenerator, cpuLoadGenerator)))
				.wiringFactory(new WiringFactory() {
				    public boolean providesTypeResolver(InterfaceWiringEnvironment environment)
				    {
				    	return (getTypeResolver(environment) != null);
				    }
				    
				    public TypeResolver getTypeResolver(InterfaceWiringEnvironment environment)
				    {
				    	return new TypeResolver() {

							@Override
							public GraphQLObjectType getType(TypeResolutionEnvironment env)
							{
								return (GraphQLObjectType) env.getSchema().getType(env.getObject().getClass().getSimpleName());
							}
				    	};
				    };
				})
				.build();
		
		this.graphqlSchema = schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring);
	}
	
	/**
	 * Retrieve the data to execute an introspection query.
	 * @return
	 * @throws IOException
	 */
	private String getSchemaQuery() throws IOException
	{
		StringBuilder resultBuilder = new StringBuilder();
		
		try (
				InputStream is = GraphQLServlet.class.getResourceAsStream("introspectionQuery");
				InputStreamReader isr = new InputStreamReader(is);
				BufferedReader br = new BufferedReader(isr);
			)
		{
			String readLine;
			while((readLine = br.readLine()) != null)
			{
				resultBuilder.append(readLine);
			}
		}
		
		return resultBuilder.toString();
	}
}
