package com.shin1ogawa;

/*
 * Copyright  2001,2009 gae-j-samples project.
 *     http://sourceforge.jp/projects/gae-j-samples
 *
 * 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.
 */

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import org.apache.commons.lang.StringUtils;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.archiver.ArchiverException;
import org.codehaus.plexus.archiver.manager.NoSuchArchiverException;
import org.codehaus.plexus.archiver.zip.ZipUnArchiver;
import org.codehaus.plexus.logging.Logger;

/**
 * start the DevAppServerMain via KickStart class.
 * 
 * @goal start
 * @phase pre-integration-test
 * 
 * @author shin1ogawa
 */
public class StartMojo extends AbstractMojo {

	/**
	 * The server to use to determine the latest.
	 * 
	 * @parameter expression="${appengine-server}"
	 */
	private String server;

	/**
	 * The address of the interface on the local machine.
	 * 
	 * @parameter default-value="localhost" expression="${appengine-address}"
	 */
	private String address;

	/**
	 * The port number to bind to on the local machine.
	 * 
	 * @parameter default-value = "8080" expression="${appengine-port}"
	 * @required
	 */
	private int port;

	/**
	 * @parameter expression="${appengine-java-home}"
	 */
	private String javaHome;

	/**
	 * Overrides where the SDK is located.
	 * 
	 * @parameter expression="${appengine-sdk-directory}"
	 */
	private String sdkRootDirectory;

	/**
	 * url of zip file of Google Appengine SDK.
	 * 
	 * @parameter expression="${appengine-sdk-zip-file}" default-value=
	 *            "http://googleappengine.googlecode.com/files/appengine-java-sdk-1.2.2.zip"
	 */
	private String sdkZipFile;

	/**
	 * @parameter expression="${appengine-sdk-saveas}"
	 */
	private String sdkSaveAs;

	/**
	 * extract folder of Google Appengine SDK.
	 * 
	 * @parameter expression="${appengine-sdk-unzip-directory}"
	 */
	private String sdkUnZipDirectory;

	/**
	 * Disable the check for newer SDK versions.
	 * 
	 * @parameter default-value = "true"
	 */
	private boolean disableUpdateCheck;

	/**
	 * @parameter default-value = "war" expression="${appengine-war-directory}"
	 * @required
	 * @see #checkWarDirectory()
	 */
	private String warDirectory;

	/**
	 * wait.
	 * 
	 * @parameter default-value = "true" expression="${appengine-wait}"
	 */
	private boolean wait;

	/**
	 * @parameter expression="${project}"
	 * @required
	 * @readonly
	 * @see #getProject()
	 */
	@SuppressWarnings("unused")
	private MavenProject project;

	private static Process process;

	public void execute() throws MojoExecutionException, MojoFailureException {
		checkWarDirectory();

		sdkRootDirectory = getSdkRoot();
		getLog().info("sdkRootDirectory=" + sdkRootDirectory);

		StringBuilder b = new StringBuilder();
		String java = "java";
		java = getJavaHome(java);
		b.append(java);
		b.append(" -cp ").append(sdkRootDirectory).append(
				"lib/appengine-tools-api.jar");
		b.append(" com.google.appengine.tools.KickStart");
		b.append(" com.google.appengine.tools.development.DevAppServerMain");
		b.append(" ").append(StringUtils.join(createProgramArguments(), " "));
		getLog().info("command=" + b.toString());

		try {
			process = Runtime.getRuntime().exec(b.toString());
			try {
				// コンテナが立ち上がるまで待つ.
				// TODO pingUrlとtimeOutパラメータを設定してもらう事で立ち上がるのを待つ？
				// とりあえず適当な秒数待たせているが…。
				getLog().info("wait for container.");
				Thread.sleep(5000);
			} catch (InterruptedException e) {
				throw new MojoExecutionException("fail to start the process.");
			}
			if (wait) {
				getLog().info("wait = true. press CTRL+C to stop.");
				waitExit();
			} else {
				getLog().info("wait = false.");
			}
		} catch (IOException e) {
			getLog().warn(e);
			throw new MojoExecutionException("fail to start the process.");
		}
	}

	private String getJavaHome(String java) {
		File javaHomeDir = null;
		if (StringUtils.isEmpty(javaHome)) {
			javaHomeDir = new File(System.getProperty("java.home"));
		} else {
			javaHomeDir = new File(javaHome);
		}
		if (javaHomeDir.exists() && javaHomeDir.isDirectory()) {
			java = javaHomeDir.getAbsolutePath() + File.separator + "bin"
					+ File.separator + "java";
		}
		return java;
	}

	private String getSdkRoot() throws MojoExecutionException {
		if (StringUtils.isEmpty(sdkRootDirectory)) {
			if (StringUtils.isEmpty(sdkZipFile)
					|| StringUtils.isEmpty(sdkUnZipDirectory)) {
				throw new MojoExecutionException(
						"sdkUnZipDirectory is not specified.");
			}
			try {
				File dstDirectory = new File(sdkUnZipDirectory);
				if (!dstDirectory.exists()) {
					boolean mkdirs = dstDirectory.mkdirs();
					if (mkdirs) {
						getLog()
								.info("mkdir:" + dstDirectory.getAbsolutePath());
					} else {
						throw new MojoExecutionException("can not create dir: "
								+ dstDirectory.getAbsolutePath());
					}
				}
				File sdkZip = null;
				if (sdkZipFile.startsWith("http://")
						|| sdkZipFile.startsWith("https://")) {
					sdkZip = downloadZipFile(dstDirectory);
				} else {
					sdkZip = new File(sdkZipFile);
				}
				File unpackSdk = unpackSdk(sdkZip, dstDirectory);
				sdkRootDirectory = unpackSdk.getAbsolutePath();
			} catch (IOException e) {
				throw new MojoExecutionException("failure to unpack sdk.", e);
			} catch (NoSuchArchiverException e) {
				throw new MojoExecutionException("failure to unpack sdk.", e);
			} catch (ArchiverException e) {
				throw new MojoExecutionException("failure to unpack sdk.", e);
			}
		}
		if (!sdkRootDirectory.endsWith("/")) {
			sdkRootDirectory = sdkRootDirectory + "/";
		}
		return sdkRootDirectory;
	}

	private File downloadZipFile(File dstDirectory)
			throws MalformedURLException, IOException, FileNotFoundException {
		URL url = new URL(sdkZipFile);
		URLConnection connection = url.openConnection();
		InputStream in = connection.getInputStream();
		File saveAs = null;
		if (StringUtils.isEmpty(sdkSaveAs)) {
			saveAs = File.createTempFile("temp", "", dstDirectory);
		} else {
			saveAs = new File(dstDirectory, sdkSaveAs);
			if (saveAs.exists()) {
				getLog().info(
						"downloaded sdk zip file already exsits: "
								+ saveAs.getAbsolutePath());
				return saveAs;
			}
		}
		OutputStream out = new FileOutputStream(saveAs);
		getLog().info(
				"downloading sdk zip file from " + sdkZipFile + " to "
						+ saveAs.getAbsolutePath());
		try {
			byte[] bytes = new byte[8192];
			int len = -1;
			while ((len = in.read(bytes)) > 0) {
				out.write(bytes, 0, len);
			}
		} finally {
			in.close();
			out.close();
		}
		return saveAs;
	}

	private void waitExit() {
		Callable<Integer> callable = new Callable<Integer>() {

			public Integer call() throws Exception {
				Integer exitValue = null;
				boolean alive = true;
				while (alive) {
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e1) {
						//
					}
					try {
						exitValue = process.exitValue();
						alive = false;
					} catch (IllegalThreadStateException e) {
						alive = true;
					}
				}
				return exitValue;
			}
		};
		ExecutorService executor = Executors.newSingleThreadExecutor();
		Future<Integer> submit = executor.submit(callable);
		try {
			Integer exitValue = submit.get();
			getLog().info("exitValue=" + exitValue);
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (ExecutionException e) {
			e.printStackTrace();
		}
	}

	/**
	 * check speciefied war directory state.
	 * 
	 * @throws MojoExecutionException
	 *             if specified war directory is not exist. <br />
	 *             if specified war directory does not have WEB-INF/lib
	 */
	private void checkWarDirectory() throws MojoExecutionException {
		File warDir = new File(warDirectory);
		String webInfoLibPath = warDirectory.endsWith("/") ? warDirectory
				+ "WEB-INF/lib" : warDirectory + "/WEB-INF/lib";
		File webInfoLibDir = new File(webInfoLibPath);
		if (!warDir.exists()) {
			throw new MojoExecutionException("directory \"" + warDirectory
					+ "\" is not exists.");
		} else if (!webInfoLibDir.exists()) {
			throw new MojoExecutionException("directory \"" + webInfoLibPath
					+ "\" is not exists.");
		}
	}

	private List<String> createProgramArguments() {
		List<String> args = new ArrayList<String>(3);
		if (!StringUtils.isEmpty(server)) {
			args.add("--server=" + server);
		}
		if (!StringUtils.isEmpty(address)) {
			args.add("--address=" + address);
		}
		args.add("--port=" + port);
		if (!StringUtils.isEmpty(sdkRootDirectory)) {
			args.add("--sdk_root=" + sdkRootDirectory);
		}
		if (disableUpdateCheck) {
			args.add("--disable_update_check");
		}
		args.add(warDirectory);
		return args;
	}

	/**
	 * unpack sdk zip file and return the unpacked sdk root directory.
	 * 
	 * @param srcFile
	 * @param dstDirectory
	 * @return unpacked sdk root directory
	 * @throws IOException
	 * @throws NoSuchArchiverException
	 * @throws ArchiverException
	 * @throws MojoExecutionException
	 */
	private File unpackSdk(File srcFile, File dstDirectory) throws IOException,
			NoSuchArchiverException, ArchiverException, MojoExecutionException {
		unpack(srcFile, dstDirectory);
		File sdkRoot = findSdkRoot(dstDirectory);
		if (sdkRoot == null) {
			throw new MojoExecutionException("can not find sdk root under "
					+ dstDirectory.getAbsolutePath());
		}
		return sdkRoot;
	}

	private File findSdkRoot(File dir) {
		if (!dir.exists()) {
			throw new IllegalStateException(dir.getAbsolutePath()
					+ " is not exists.");
		}
		if (dir.getName().startsWith("appengine-java-sdk")) {
			return dir;
		}
		File[] listFiles = dir.listFiles();
		if (listFiles != null) {
			for (File file : listFiles) {
				if (file.isDirectory()) {
					File findSdkRoot = findSdkRoot(file);
					if (findSdkRoot != null) {
						return findSdkRoot;
					}
				}
			}
		}
		return null;
	}

	private void unpack(File srcFile, File dstDirectory) throws IOException,
			NoSuchArchiverException, ArchiverException, MojoExecutionException {
		ZipUnArchiver unArchiver = new ZipUnArchiver();
		unArchiver.enableLogging(new Logger() {

			public void warn(String message, Throwable throwable) {
				getLog().warn(message, throwable);
			}

			public void warn(String message) {
				getLog().warn(message);
			}

			public boolean isWarnEnabled() {
				return getLog().isWarnEnabled();
			}

			public boolean isInfoEnabled() {
				return getLog().isInfoEnabled();
			}

			public boolean isFatalErrorEnabled() {
				return getLog().isErrorEnabled();
			}

			public boolean isErrorEnabled() {
				return getLog().isErrorEnabled();
			}

			public boolean isDebugEnabled() {
				return getLog().isDebugEnabled();
			}

			public void info(String message, Throwable throwable) {
				getLog().info(message, throwable);
			}

			public void info(String message) {
				getLog().info(message);
			}

			public int getThreshold() {
				return 100;
			}

			public String getName() {
				return getLog().toString();
			}

			public Logger getChildLogger(String name) {
				return null;
			}

			public void fatalError(String message, Throwable throwable) {
				getLog().error(message, throwable);
			}

			public void fatalError(String message) {
				getLog().error(message);
			}

			public void error(String message, Throwable throwable) {
				getLog().error(message, throwable);
			}

			public void error(String message) {
				getLog().error(message);
			}

			public void debug(String message, Throwable throwable) {
				getLog().debug(message, throwable);
			}

			public void debug(String message) {
				getLog().debug(message);
			}
		});
		unArchiver.setSourceFile(srcFile);
		unArchiver.setDestDirectory(dstDirectory);
		unArchiver.extract();
	}

	/**
	 * @return the process
	 */
	public static Process getProcess() {
		return process;
	}
}
