/*
 * Copyright 2012-2017 the original author or authors.
 *
 * Licensed 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 io.spring.initializr.web.project;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import com.samskivert.mustache.Mustache;
import io.spring.initializr.generator.BasicProjectRequest;
import io.spring.initializr.generator.CommandLineHelpGenerator;
import io.spring.initializr.generator.ProjectGenerator;
import io.spring.initializr.generator.ProjectRequest;
import io.spring.initializr.metadata.DependencyMetadata;
import io.spring.initializr.metadata.DependencyMetadataProvider;
import io.spring.initializr.metadata.InitializrMetadata;
import io.spring.initializr.metadata.InitializrMetadataProvider;
import io.spring.initializr.util.Agent;
import io.spring.initializr.util.TemplateRenderer;
import io.spring.initializr.util.Version;
import io.spring.initializr.web.mapper.DependencyMetadataV21JsonMapper;
import io.spring.initializr.web.mapper.InitializrMetadataJsonMapper;
import io.spring.initializr.web.mapper.InitializrMetadataV21JsonMapper;
import io.spring.initializr.web.mapper.InitializrMetadataV2JsonMapper;
import io.spring.initializr.web.mapper.InitializrMetadataVersion;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.taskdefs.Tar;
import org.apache.tools.ant.taskdefs.Zip;
import org.apache.tools.ant.types.TarFileSet;
import org.apache.tools.ant.types.ZipFileSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.http.CacheControl;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.ResponseEntity.BodyBuilder;
import org.springframework.stereotype.Controller;
import org.springframework.util.DigestUtils;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.resource.ResourceUrlProvider;

import static io.spring.initializr.util.Agent.AgentId.CURL;
import static io.spring.initializr.util.Agent.AgentId.HTTPIE;
import static io.spring.initializr.util.Agent.AgentId.SPRING_BOOT_CLI;

/**
 * The main initializr controller provides access to the configured metadata and serves as
 * a central endpoint to generate projects or build files.
 *
 * @author Dave Syer
 * @author Stephane Nicoll
 */
@Controller
public class MainController extends AbstractInitializrController {

	private static final Logger log = LoggerFactory.getLogger(MainController.class);

	public static final MediaType HAL_JSON_CONTENT_TYPE = MediaType
			.parseMediaType("application/hal+json");

	private final ProjectGenerator projectGenerator;
	private final DependencyMetadataProvider dependencyMetadataProvider;
	private final CommandLineHelpGenerator commandLineHelpGenerator;

	public MainController(InitializrMetadataProvider metadataProvider,
			TemplateRenderer templateRenderer, ResourceUrlProvider resourceUrlProvider,
			ProjectGenerator projectGenerator,
			DependencyMetadataProvider dependencyMetadataProvider) {
		super(metadataProvider, resourceUrlProvider);
		this.projectGenerator = projectGenerator;
		this.dependencyMetadataProvider = dependencyMetadataProvider;
		this.commandLineHelpGenerator = new CommandLineHelpGenerator(templateRenderer);
	}

	@ModelAttribute
	public BasicProjectRequest projectRequest(
			@RequestHeader Map<String, String> headers) {
		ProjectRequest request = new ProjectRequest();
		request.getParameters().putAll(headers);
		request.initialize(metadataProvider.get());
		return request;
	}

	@RequestMapping(value = "/metadata/config", produces = "application/json")
	@ResponseBody
	public InitializrMetadata config() {
		return metadataProvider.get();
	}

	@RequestMapping(value = "/metadata/client")
	public String client() {
		return "redirect:/";
	}

	@RequestMapping(value = "/", produces = "text/plain")
	public ResponseEntity<String> serviceCapabilitiesText(
			@RequestHeader(value = HttpHeaders.USER_AGENT, required = false) String userAgent) {
		String appUrl = generateAppUrl();
		InitializrMetadata metadata = metadataProvider.get();

		BodyBuilder builder = ResponseEntity.ok().contentType(MediaType.TEXT_PLAIN);
		if (userAgent != null) {
			Agent agent = Agent.fromUserAgent(userAgent);
			if (agent != null) {
				if (CURL.equals(agent.getId())) {
					String content = commandLineHelpGenerator
							.generateCurlCapabilities(metadata, appUrl);
					return builder.eTag(createUniqueId(content)).body(content);
				}
				if (HTTPIE.equals(agent.getId())) {
					String content = commandLineHelpGenerator
							.generateHttpieCapabilities(metadata, appUrl);
					return builder.eTag(createUniqueId(content)).body(content);
				}
				if (SPRING_BOOT_CLI.equals(agent.getId())) {
					String content = commandLineHelpGenerator
							.generateSpringBootCliCapabilities(metadata, appUrl);
					return builder.eTag(createUniqueId(content)).body(content);
				}
			}
		}
		String content = commandLineHelpGenerator.generateGenericCapabilities(metadata,
				appUrl);
		return builder.eTag(createUniqueId(content)).body(content);
	}

	@RequestMapping(value = "/", produces = "application/hal+json")
	public ResponseEntity<String> serviceCapabilitiesHal() {
		return serviceCapabilitiesFor(InitializrMetadataVersion.V2_1,
				HAL_JSON_CONTENT_TYPE);
	}

	@RequestMapping(value = "/", produces = { "application/vnd.initializr.v2.1+json",
			"application/json" })
	public ResponseEntity<String> serviceCapabilitiesV21() {
		return serviceCapabilitiesFor(InitializrMetadataVersion.V2_1);
	}

	@RequestMapping(value = "/", produces = "application/vnd.initializr.v2+json")
	public ResponseEntity<String> serviceCapabilitiesV2() {
		return serviceCapabilitiesFor(InitializrMetadataVersion.V2);
	}

	private ResponseEntity<String> serviceCapabilitiesFor(
			InitializrMetadataVersion version) {
		return serviceCapabilitiesFor(version, version.getMediaType());
	}

	private ResponseEntity<String> serviceCapabilitiesFor(
			InitializrMetadataVersion version, MediaType contentType) {
		String appUrl = generateAppUrl();
		String content = getJsonMapper(version).write(metadataProvider.get(), appUrl);
		return ResponseEntity.ok().contentType(contentType).eTag(createUniqueId(content))
				.cacheControl(CacheControl.maxAge(7, TimeUnit.DAYS)).body(content);
	}

	private static InitializrMetadataJsonMapper getJsonMapper(
			InitializrMetadataVersion version) {
		switch (version) {
			case V2:
				return new InitializrMetadataV2JsonMapper();
			default:
				return new InitializrMetadataV21JsonMapper();
		}
	}

	@RequestMapping(value = "/dependencies", produces = {
			"application/vnd.initializr.v2.1+json", "application/json" })
	public ResponseEntity<String> dependenciesV21(
			@RequestParam(required = false) String bootVersion) {
		return dependenciesFor(InitializrMetadataVersion.V2_1, bootVersion);
	}

	private ResponseEntity<String> dependenciesFor(InitializrMetadataVersion version,
			String bootVersion) {
		InitializrMetadata metadata = metadataProvider.get();
		Version v = bootVersion != null ? Version.parse(bootVersion)
				: Version.parse(metadata.getBootVersions().getDefault().getId());
		DependencyMetadata dependencyMetadata = dependencyMetadataProvider.get(metadata,
				v);
		String content = new DependencyMetadataV21JsonMapper().write(dependencyMetadata);
		return ResponseEntity.ok().contentType(version.getMediaType())
				.eTag(createUniqueId(content))
				.cacheControl(CacheControl.maxAge(7, TimeUnit.DAYS)).body(content);
	}

	@ModelAttribute("linkTo")
	public Mustache.Lambda linkTo() {
		return (frag, out) -> out.write(this.getLinkTo().apply(frag.execute()));
	}

	@RequestMapping(value = "/", produces = "text/html")
	public String home(Map<String, Object> model) {
		renderHome(model);
		return "home";
	}

	@RequestMapping("/spring")
	public String spring() {
		String url = metadataProvider.get().createCliDistributionURl("zip");
		return "redirect:" + url;
	}

	@RequestMapping(value = { "/spring.tar.gz", "spring.tgz" })
	public String springTgz() {
		String url = metadataProvider.get().createCliDistributionURl("tar.gz");
		return "redirect:" + url;
	}

	@RequestMapping("/pom")
	@ResponseBody
	public ResponseEntity<byte[]> pom(BasicProjectRequest request) {
		request.setType("maven-build");
		byte[] mavenPom = projectGenerator.generateMavenPom((ProjectRequest) request);
		return createResponseEntity(mavenPom, "application/octet-stream", "pom.xml");
	}

	@RequestMapping("/build")
	@ResponseBody
	public ResponseEntity<byte[]> gradle(BasicProjectRequest request) {
		request.setType("gradle-build");
		byte[] gradleBuild = projectGenerator
				.generateGradleBuild((ProjectRequest) request);
		return createResponseEntity(gradleBuild, "application/octet-stream",
				"build.gradle");
	}

	@RequestMapping("/starter.zip")
	@ResponseBody
	public ResponseEntity<byte[]> springZip(BasicProjectRequest basicRequest)
			throws IOException {
		ProjectRequest request = (ProjectRequest) basicRequest;
		File dir = projectGenerator.generateProjectStructure(request);

		File download = projectGenerator.createDistributionFile(dir, ".zip");

		String wrapperScript = getWrapperScript(request);
		new File(dir, wrapperScript).setExecutable(true);
		Zip zip = new Zip();
		zip.setProject(new Project());
		zip.setDefaultexcludes(false);
		ZipFileSet set = new ZipFileSet();
		set.setDir(dir);
		set.setFileMode("755");
		set.setIncludes(wrapperScript);
		set.setDefaultexcludes(false);
		zip.addFileset(set);
		set = new ZipFileSet();
		set.setDir(dir);
		set.setIncludes("**,");
		set.setExcludes(wrapperScript);
		set.setDefaultexcludes(false);
		zip.addFileset(set);
		zip.setDestFile(download.getCanonicalFile());
		zip.execute();
		return upload(download, dir, generateFileName(request, "zip"), "application/zip");
	}

	@RequestMapping(value = "/starter.tgz", produces = "application/x-compress")
	@ResponseBody
	public ResponseEntity<byte[]> springTgz(BasicProjectRequest basicRequest)
			throws IOException {
		ProjectRequest request = (ProjectRequest) basicRequest;
		File dir = projectGenerator.generateProjectStructure(request);

		File download = projectGenerator.createDistributionFile(dir, ".tar.gz");

		String wrapperScript = getWrapperScript(request);
		new File(dir, wrapperScript).setExecutable(true);
		Tar zip = new Tar();
		zip.setProject(new Project());
		zip.setDefaultexcludes(false);
		TarFileSet set = zip.createTarFileSet();
		set.setDir(dir);
		set.setFileMode("755");
		set.setIncludes(wrapperScript);
		set.setDefaultexcludes(false);
		set = zip.createTarFileSet();
		set.setDir(dir);
		set.setIncludes("**,");
		set.setExcludes(wrapperScript);
		set.setDefaultexcludes(false);
		zip.setDestFile(download.getCanonicalFile());
		Tar.TarCompressionMethod method = new Tar.TarCompressionMethod();
		method.setValue("gzip");
		zip.setCompression(method );
		zip.execute();
		return upload(download, dir, generateFileName(request, "tar.gz"),
				"application/x-compress");
	}

	private static String generateFileName(ProjectRequest request, String extension) {
		String tmp = request.getArtifactId().replaceAll(" ", "_");
		try {
			return URLEncoder.encode(tmp, "UTF-8") + "." + extension;
		}
		catch (UnsupportedEncodingException e) {
			throw new IllegalStateException("Cannot encode URL", e);
		}
	}

	private static String getWrapperScript(ProjectRequest request) {
		String script = "gradle".equals(request.getBuild()) ? "gradlew" : "mvnw";
		return request.getBaseDir() != null
				? request.getBaseDir() + "/" + script : script;
	}

	private ResponseEntity<byte[]> upload(File download, File dir, String fileName,
			String contentType) throws IOException {
		byte[] bytes = StreamUtils.copyToByteArray(new FileInputStream(download));
		log.info("Uploading: {} ({} bytes)", download, bytes.length);
		ResponseEntity<byte[]> result = createResponseEntity(bytes, contentType,
				fileName);
		projectGenerator.cleanTempFiles(dir);
		return result;
	}

	private ResponseEntity<byte[]> createResponseEntity(byte[] content,
			String contentType, String fileName) {
		String contentDispositionValue = "attachment; filename=\"" + fileName + "\"";
		return ResponseEntity.ok().header("Content-Type", contentType)
				.header("Content-Disposition", contentDispositionValue).body(content);
	}

	private String createUniqueId(String content) {
		StringBuilder builder = new StringBuilder();
		DigestUtils.appendMd5DigestAsHex(content.getBytes(StandardCharsets.UTF_8),
				builder);
		return builder.toString();
	}

}


