/*******************************************************************************
 * Copyright (c) 2014, 2015 IBM Corporation and others 
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Contributors:
 * IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.orion.server.cf.manifest.v2.utils;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;

import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.filesystem.IFileInfo;
import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.orion.server.cf.manifest.v2.Analyzer;
import org.eclipse.orion.server.cf.manifest.v2.AnalyzerException;
import org.eclipse.orion.server.cf.manifest.v2.InvalidAccessException;
import org.eclipse.orion.server.cf.manifest.v2.ManifestParseTree;
import org.eclipse.orion.server.cf.manifest.v2.ParserException;
import org.eclipse.osgi.util.NLS;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

public class ManifestUtils {

	private static final Pattern NON_SLUG_PATTERN = Pattern.compile("[^\\w-]"); //$NON-NLS-1$
	private static final Pattern WHITESPACE_PATTERN = Pattern.compile("[\\s]"); //$NON-NLS-1$

	/* global defaults */
	public static final String DEFAULT_MEMORY = "512M"; //$NON-NLS-1$
	public static final String DEFAULT_INSTANCES = "1"; //$NON-NLS-1$
	public static final String DEFAULT_PATH = "."; //$NON-NLS-1$

	public static final String[] RESERVED_PROPERTIES = {//
	"env", // //$NON-NLS-1$
			"inherit", // //$NON-NLS-1$
			"applications" // //$NON-NLS-1$
	};

	public static boolean isReserved(ManifestParseTree node) {
		String value = node.getLabel();
		for (String property : RESERVED_PROPERTIES)
			if (property.equals(value))
				return true;

		return false;
	}

	public static final String[] APPLICATION_PROPERTIES = {//
	"name", "memory", "host", "buildpack", "command", //   //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$//$NON-NLS-4$ //$NON-NLS-5$
			"domain", "instances", "path", "timeout", "no-route", "services"// //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$
	};

	public static boolean isApplicationProperty(ManifestParseTree node) {
		String value = node.getLabel();
		for (String property : APPLICATION_PROPERTIES)
			if (property.equals(value))
				return true;

		return false;
	}

	/**
	 * Inner helper method parsing single manifests with additional semantic analysis.
	 */
	protected static ManifestParseTree parseManifest(InputStream inputStream, String targetBase, Analyzer analyzer) throws IOException, ParserException, AnalyzerException {

		/* run parser */
		ManifestParser parser = new ManifestParser();
		ManifestParseTree parseTree = parser.parse(inputStream);

		/* perform inheritance transformations */
		ManifestTransformator transformator = new ManifestTransformator();
		transformator.apply(parseTree);

		/* resolve symbols */
		SymbolResolver symbolResolver = new SymbolResolver(targetBase);
		symbolResolver.apply(parseTree);

		/* validate common field values */
		Analyzer applicationAnalyzer = analyzer != null ? analyzer : new ApplicationSanizator();
		applicationAnalyzer.apply(parseTree);
		return parseTree;
	}

	/**
	 * Inner helper method parsing single manifests with additional semantic analysis.
	 */
	protected static ManifestParseTree parseManifest(IFileStore manifestFileStore, String targetBase, Analyzer analyzer) throws CoreException, IOException, ParserException, AnalyzerException {

		/* basic sanity checks */
		IFileInfo manifestFileInfo = manifestFileStore.fetchInfo();
		if (!manifestFileInfo.exists() || manifestFileInfo.isDirectory())
			throw new IOException(ManifestConstants.MISSING_OR_INVALID_MANIFEST);

		if (manifestFileInfo.getLength() == EFS.NONE)
			throw new IOException(ManifestConstants.EMPTY_MANIFEST);

		if (manifestFileInfo.getLength() > ManifestConstants.MANIFEST_SIZE_LIMIT)
			throw new IOException(ManifestConstants.MANIFEST_FILE_SIZE_EXCEEDED);

		InputStream inputStream = manifestFileStore.openInputStream(EFS.NONE, null);
		ManifestParseTree manifestTree = null;
		try {
			manifestTree = parseManifest(inputStream, targetBase, analyzer);
		} finally {
			if (inputStream != null) {
				inputStream.close();
			}
		}
		return manifestTree;
	}

	/**
	 * Utility method wrapping manifest parse process including inheritance and additional semantic analysis.
	 * @param sandbox The file store used to limit manifest inheritance, i.e. each parent manifest has to be a
	 *  transitive child of the sandbox.
	 * @param manifestStore Manifest file store used to fetch the manifest contents.
	 * @param targetBase Cloud foundry target base used to resolve manifest symbols.
	 * @param manifestList List of forbidden manifest paths considered in the recursive inheritance process.
	 * Used to detect inheritance cycles.
	 * @return An intermediate manifest tree representation.
	 * @throws CoreException
	 * @throws IOException
	 * @throws TokenizerException
	 * @throws ParserException
	 * @throws AnalyzerException
	 * @throws InvalidAccessException
	 */
	public static ManifestParseTree parse(IFileStore sandbox, IFileStore manifestStore, String targetBase, Analyzer analyzer, List<IPath> manifestList) throws CoreException, IOException, ParserException, AnalyzerException, InvalidAccessException {
		ManifestParseTree manifest = parseManifest(manifestStore, targetBase, analyzer);

		if (!manifest.has(ManifestConstants.INHERIT))
			/* nothing to do */
			return manifest;

		/* check if the parent manifest is within the given sandbox */
		IPath parentLocation = new Path(manifest.get(ManifestConstants.INHERIT).getValue());
		if (!InheritanceUtils.isWithinSandbox(sandbox, manifestStore, parentLocation))
			throw new AnalyzerException(NLS.bind(ManifestConstants.FORBIDDEN_ACCESS_ERROR, manifest.get(ManifestConstants.INHERIT).getValue()));

		/* detect inheritance cycles */
		if (manifestList.contains(parentLocation))
			throw new AnalyzerException(ManifestConstants.INHERITANCE_CYCLE_ERROR);

		manifestList.add(parentLocation);

		IFileStore parentStore = manifestStore.getParent().getFileStore(parentLocation);
		ManifestParseTree parentManifest = parse(sandbox, parentStore, targetBase, analyzer, manifestList);
		InheritanceUtils.inherit(parentManifest, manifest);

		/* perform additional inheritance transformations */
		ManifestTransformator transformator = new ManifestTransformator();
		transformator.apply(manifest);
		return manifest;
	}

	/**
	 * Helper method for {@link #parse(IFileStore, IFileStore, String, List<IPath>)}
	 * @param sandbox
	 * @param manifestStore
	 * @return
	 * @throws CoreException
	 * @throws IOException
	 * @throws TokenizerException
	 * @throws ParserException
	 * @throws AnalyzerException
	 * @throws InvalidAccessException
	 */
	public static ManifestParseTree parse(IFileStore sandbox, IFileStore manifestStore) throws CoreException, IOException, ParserException, AnalyzerException, InvalidAccessException {
		return parse(sandbox, manifestStore, null, null, new ArrayList<IPath>());
	}

	/**
	 * Helper method for {@link #parse(IFileStore, IFileStore, String, List<IPath>)}
	 * @param sandbox
	 * @param manifestStore
	 * @param targetBase
	 * @return
	 * @throws CoreException
	 * @throws IOException
	 * @throws TokenizerException
	 * @throws ParserException
	 * @throws AnalyzerException
	 * @throws InvalidAccessException
	 */
	public static ManifestParseTree parse(IFileStore sandbox, IFileStore manifestStore, String targetBase) throws CoreException, IOException, ParserException, AnalyzerException, InvalidAccessException {
		return parse(sandbox, manifestStore, targetBase, null, new ArrayList<IPath>());
	}

	/**
	 * Helper method for {@link #parse(IFileStore, IFileStore, String, List<IPath>)}
	 * @param sandbox
	 * @param manifestStore
	 * @param targetBase
	 * @return
	 * @throws CoreException
	 * @throws IOException
	 * @throws TokenizerException
	 * @throws ParserException
	 * @throws AnalyzerException
	 * @throws InvalidAccessException
	 */
	public static ManifestParseTree parse(IFileStore sandbox, IFileStore manifestStore, String targetBase, Analyzer analyzer) throws CoreException, IOException, ParserException, AnalyzerException, InvalidAccessException {
		return parse(sandbox, manifestStore, targetBase, analyzer, new ArrayList<IPath>());
	}

	/**
	 * Normalizes the string memory measurement to a MB integer value.
	 * @param memory Manifest memory measurement.
	 * @return Normalized MB integer value.
	 */
	public static int normalizeMemoryMeasure(String memory) {

		if (memory.toLowerCase().endsWith("m")) //$NON-NLS-1$
			return Integer.parseInt(memory.substring(0, memory.length() - 1));

		if (memory.toLowerCase().endsWith("mb")) //$NON-NLS-1$
			return Integer.parseInt(memory.substring(0, memory.length() - 2));

		if (memory.toLowerCase().endsWith("g")) //$NON-NLS-1$
			return (1024 * Integer.parseInt(memory.substring(0, memory.length() - 1)));

		if (memory.toLowerCase().endsWith("gb")) //$NON-NLS-1$
			return (1024 * Integer.parseInt(memory.substring(0, memory.length() - 2)));

		/* return default memory value, i.e. 1024 MB */
		return 1024;
	}

	/**
	 * Slugifies the given input to be reusable as URL pattern.
	 * @param input Input to be slugified.
	 * @return Slugified input
	 */
	public static String slugify(String input) {
		input = WHITESPACE_PATTERN.matcher(input).replaceAll(""); //$NON-NLS-1$
		return NON_SLUG_PATTERN.matcher(input).replaceAll(""); //$NON-NLS-1$
	}

	/**
	 * Parses a manifest from the given JSON representation.
	 *  Note: no cross-manifest inheritance is allowed.
	 * @param manifestJSON
	 * @return
	 * @throws IllegalArgumentException
	 * @throws JSONException
	 * @throws IOException
	 * @throws TokenizerException
	 * @throws ParserException
	 * @throws AnalyzerException
	 */
	public static ManifestParseTree parse(JSONObject manifestJSON) throws IllegalArgumentException, JSONException, IOException, ParserException, AnalyzerException {

		StringBuilder sb = new StringBuilder();
		sb.append("---").append(System.getProperty("line.separator")); //$NON-NLS-1$ //$NON-NLS-2$
		append(sb, manifestJSON, 0, false);

		String manifestYAML = sb.toString();
		InputStream inputStream = new ByteArrayInputStream(manifestYAML.getBytes("UTF-8")); //$NON-NLS-1$
		ManifestParseTree manifestTree = null;
		try {
			manifestTree = parseManifest(inputStream, null, null);
		} finally {
			if (inputStream != null) {
				inputStream.close();
			}
		}
		return manifestTree;
	}

	private static void appendIndentation(StringBuilder sb, int indentation) {

		/* print indentation */
		for (int i = 0; i < indentation; ++i)
			sb.append(" "); //$NON-NLS-1$
	}

	private static void append(StringBuilder sb, JSONArray arr, int indentation) throws JSONException {

		for (int i = 0; i < arr.length(); ++i) {
			appendIndentation(sb, indentation);
			sb.append("-").append(" "); //$NON-NLS-1$ //$NON-NLS-2$

			Object val = arr.get(i);
			if (val instanceof String) {

				sb.append((String) val);
				sb.append(System.getProperty("line.separator")); //$NON-NLS-1$

			} else if (val instanceof JSONObject) {

				JSONObject objVal = (JSONObject) val;
				append(sb, objVal, indentation + 2, false);

			} else
				throw new IllegalArgumentException("Arrays may contain only JSON objects or string literals.");
		}
	}

	private static void append(StringBuilder sb, JSONObject obj, int indentation, boolean indentFirst) throws JSONException {

		String[] names = JSONObject.getNames(obj);
		if (names == null) {
			return;
		}
		for (int i = 0; i < names.length; ++i) {

			String prop = names[i];
			if (i != 0 || indentFirst)
				appendIndentation(sb, indentation);

			sb.append(prop).append(":"); //$NON-NLS-1$

			Object val = obj.get(prop);
			if (val instanceof String) {
				sb.append(" ").append((String) val); //$NON-NLS-1$
				sb.append(System.getProperty("line.separator")); //$NON-NLS-1$
			} else if (val instanceof Boolean) {
				sb.append(" ").append(val.toString()); //$NON-NLS-1$
				sb.append(System.getProperty("line.separator")); //$NON-NLS-1$
			} else if (val instanceof JSONObject) {
				JSONObject objVal = (JSONObject) val;
				sb.append(System.getProperty("line.separator")); //$NON-NLS-1$
				append(sb, objVal, indentation + 2, true);
			} else if (val instanceof JSONArray) {
				JSONArray arr = (JSONArray) val;
				sb.append(System.getProperty("line.separator")); //$NON-NLS-1$
				append(sb, arr, indentation);
			} else
				throw new IllegalArgumentException("Objects may contain only JSON objects, arrays or string literals.");
		}
	}

	/**
	 * Creates a manifest boilerplate consisting of one application with the given name.
	 * @param applicationName
	 * @return
	 * @throws IllegalArgumentException
	 * @throws JSONException
	 * @throws IOException
	 * @throws TokenizerException
	 * @throws ParserException
	 * @throws AnalyzerException
	 */
	public static ManifestParseTree createBoilerplate(String applicationName) throws IllegalArgumentException, JSONException, IOException, ParserException, AnalyzerException {

		JSONObject application = new JSONObject();
		application.put(ManifestConstants.NAME, applicationName);

		JSONArray applications = new JSONArray();
		applications.put(application);

		JSONObject manifest = new JSONObject();
		manifest.put(ManifestConstants.APPLICATIONS, applications);

		return parse(manifest);
	}

	/**
	 * Helper method for deciding whether a manifest has multiple applications or not.
	 * @param manifest
	 * @return
	 */
	public static boolean hasMultipleApplications(ManifestParseTree manifest) {
		if (!manifest.has(ManifestConstants.APPLICATIONS))
			return false;

		ManifestParseTree applications = manifest.getOpt(ManifestConstants.APPLICATIONS);
		if (!applications.isList())
			return false;

		return applications.getChildren().size() > 1;
	}

	/**
	 * Instruments application properties by copying values from the instrumentation JSON.
	 * Note, that this method will perform a shallow instrumentation of single string properties.
	 * @param manifest
	 * @param instrumentation
	 * @throws JSONException
	 * @throws InvalidAccessException 
	 */
	public static void instrumentManifest(ManifestParseTree manifest, JSONObject instrumentation) throws JSONException, InvalidAccessException {

		if (instrumentation == null || !manifest.has(ManifestConstants.APPLICATIONS))
			return;

		List<ManifestParseTree> applications = manifest.get(ManifestConstants.APPLICATIONS).getChildren();

		if (instrumentation == null || instrumentation.length() == 0)
			return;

		for (String key : JSONObject.getNames(instrumentation)) {
			Object value = instrumentation.get(key);
			for (ManifestParseTree application : applications) {

				if (ManifestConstants.MEMORY.equals(key) && !updateMemory(application, (String) value))
					continue;

				if (value instanceof String) {
					application.put(key, (String) value);
				} else if (value instanceof JSONObject) {
					application.put(key, (JSONObject) value);
				}
			}
		}
	}

	private static boolean updateMemory(ManifestParseTree application, String value) {
		if (!application.has(ManifestConstants.MEMORY))
			return true;

		try {
			String appMemoryString = application.get(ManifestConstants.MEMORY).getValue();
			int appMemory = normalizeMemoryMeasure(appMemoryString);
			int instrumentationMemory = normalizeMemoryMeasure(value);

			return instrumentationMemory > appMemory;
		} catch (InvalidAccessException e) {
			return true;
		}
	}
}