/*******************************************************************************
 * Copyright (c) 2004, 2016 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM - Initial API and implementation
 *     Markus Schorn (Wind River Systems)
 *     Gerhard Schaber (Wind River Systems)
 *******************************************************************************/
package org.eclipse.cdt.make.internal.core.scannerconfig.util;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import org.eclipse.cdt.internal.core.SafeStringInterner;
import org.eclipse.cdt.internal.core.resources.ResourceLookup;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

/**
 * Class that represents a compiler command and related scanner configuration
 *
 * @author vhirsl
 */
public class CCommandDSC {
	protected final static String SINGLE_SPACE = " "; //$NON-NLS-1$
	protected final static String CMD_DESCRIPTION_ELEM = "commandDescription"; //$NON-NLS-1$
	protected final static String CMD_SI_ELEM = "commandScannerInfo"; //$NON-NLS-1$
	protected final static String OPTION_ELEM = "option"; //$NON-NLS-1$
	protected final static String SI_ITEM_ELEM = "siItem"; //$NON-NLS-1$
	protected final static String KEY_ATTR = "key"; //$NON-NLS-1$
	protected final static String VALUE_ATTR = "value"; //$NON-NLS-1$
	protected final static String QUOTE_INCLUDE_ATTR = "quote"; //$NON-NLS-1$
	protected final static String KIND_ATTR = "kind"; //$NON-NLS-1$

	protected int commandId;
	protected List<KVStringPair> compilerCommand; // members are KVStringPair objects
	protected boolean discovered;
	protected boolean cppFileType; // C or C++ file type
	protected IProject project;

	protected List<String> symbols;
	protected List<String> includes;
	protected List<String> quoteIncludes;

	public CCommandDSC(boolean cppFileType) {
		this(cppFileType, null);
	}

	public CCommandDSC(boolean cppFileType, IProject project) {
		compilerCommand = new ArrayList<>();
		discovered = false;
		this.cppFileType = cppFileType;

		symbols = new ArrayList<>();
		includes = new ArrayList<>();
		quoteIncludes = new ArrayList<>();
		this.project = project;
	}

	public boolean appliesToCPPFileType() {
		return cppFileType;
	}

	public void addSCOption(KVStringPair option) {
		if (project != null && (option.getKey().equals(SCDOptionsEnum.INCLUDE_FILE.toString())
				|| option.getKey().equals(SCDOptionsEnum.INCLUDE.toString())
				|| option.getKey().equals(SCDOptionsEnum.ISYSTEM.toString())
				|| option.getKey().equals(SCDOptionsEnum.IMACROS_FILE.toString())
				|| option.getKey().equals(SCDOptionsEnum.IQUOTE.toString()))) {
			String value = option.getValue();
			value = CygpathTranslator.translateIncludePaths(project, Collections.singletonList(value)).get(0);
			value = SafeStringInterner.safeIntern(makeRelative(project, new Path(value)).toOSString());
			option = new KVStringPair(option.getKey(), value);
		}
		compilerCommand.add(option);
	}

	public Integer getCommandIdAsInteger() {
		return Integer.valueOf(getCommandId());
	}

	/**
	 * @return Returns the commandId.
	 */
	public int getCommandId() {
		return commandId;
	}

	/**
	 * @param commandId The commandId to set.
	 */
	public void setCommandId(int commandId) {
		this.commandId = commandId;
	}

	@Override
	public String toString() {
		String commandAsString = ""; //$NON-NLS-1$
		for (Iterator<KVStringPair> i = compilerCommand.iterator(); i.hasNext();) {
			KVStringPair optionPair = i.next();
			String value = optionPair.getValue();
			commandAsString += optionPair.getKey() + SINGLE_SPACE + value + SINGLE_SPACE;
		}
		return commandAsString.trim();
	}

	public int getId() {
		return commandId;
	}

	/**
	 * Returns a command where -imacros and -include options have been removed
	 * @param quoteIncludePaths whether or not paths for includes must be put inside double quotes.
	 * @return the command line to run the scanner discovery.
	 */
	public String getSCDRunnableCommand(boolean quoteIncludePaths, boolean quoteDefines) {
		String commandAsString = ""; //$NON-NLS-1$
		for (Iterator<KVStringPair> i = compilerCommand.iterator(); i.hasNext();) {
			KVStringPair optionPair = i.next();
			if (optionPair.getKey().equals(SCDOptionsEnum.COMMAND.toString())) {
				commandAsString += optionPair.getValue() + SINGLE_SPACE;
			} else {
				// skip -include and -imacros options
				if (optionPair.getKey().equals(SCDOptionsEnum.IMACROS_FILE.toString())
						|| optionPair.getKey().equals(SCDOptionsEnum.INCLUDE_FILE.toString()))
					continue;
				String value = optionPair.getValue();
				if (optionPair.getKey().equals(SCDOptionsEnum.INCLUDE.toString())
						|| optionPair.getKey().equals(SCDOptionsEnum.ISYSTEM.toString())
						|| optionPair.getKey().equals(SCDOptionsEnum.IQUOTE.toString())) {
					value = makeAbsolute(project, value);
					if (quoteIncludePaths) {
						value = "\"" + value + "\""; //$NON-NLS-1$ //$NON-NLS-2$
					}
				} else if (quoteDefines && optionPair.getKey().equals(SCDOptionsEnum.DEFINE.toString())) {
					if (value.indexOf('\'') == -1) {
						value = "'" + value + "'"; //$NON-NLS-1$//$NON-NLS-2$
					} else {
						value = value.replaceAll("\"", "\\\\\""); //$NON-NLS-1$//$NON-NLS-2$
						value = "\"" + value + "\""; //$NON-NLS-1$ //$NON-NLS-2$
					}
				}
				commandAsString += optionPair.getKey() + SINGLE_SPACE + value + SINGLE_SPACE;
			}
		}
		return commandAsString.trim();
	}

	/**
	 * @return the compiler command
	 */
	public String getCompilerName() {
		String compiler = ""; //$NON-NLS-1$
		for (Iterator<KVStringPair> i = compilerCommand.iterator(); i.hasNext();) {
			KVStringPair optionPair = i.next();
			if (optionPair.getKey().equals(SCDOptionsEnum.COMMAND.toString())) {
				compiler = optionPair.getValue();
				break;
			}
		}
		return compiler.trim();
	}

	/**
	 * @return list of strings
	 */
	public List<String> getImacrosFile() {
		List<String> imacrosFiles = new ArrayList<>();
		for (Iterator<KVStringPair> i = compilerCommand.iterator(); i.hasNext();) {
			KVStringPair optionPair = i.next();
			if (optionPair.getKey().equals(SCDOptionsEnum.IMACROS_FILE.toString())) {
				imacrosFiles.add(makeAbsolute(project, optionPair.getValue()));
			}
		}
		return imacrosFiles;
	}

	/**
	 * @return list of strings
	 */
	public List<String> getIncludeFile() {
		List<String> includeFiles = new ArrayList<>();
		for (Iterator<KVStringPair> i = compilerCommand.iterator(); i.hasNext();) {
			KVStringPair optionPair = i.next();
			if (optionPair.getKey().equals(SCDOptionsEnum.INCLUDE_FILE.toString())) {
				includeFiles.add(makeAbsolute(project, optionPair.getValue()));
			}
		}
		return includeFiles;
	}

	//	public List getFilesList() {
	//		return files;
	//	}

	/* (non-Javadoc)
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object arg0) {
		if (arg0 != null && arg0.getClass().equals(this.getClass())) {
			CCommandDSC other = (CCommandDSC) arg0;
			return (compilerCommand.equals(other.compilerCommand) && cppFileType == other.cppFileType);
		}
		return false;
	}

	/* (non-Javadoc)
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode() {
		return compilerCommand.hashCode();
	}

	/**
	 * @return Returns the includes as strings.
	 */
	public List<String> getIncludes() {
		return makeAbsolute(project, includes);
	}

	/**
	 * @param includes The includes to set.
	 */
	public void setIncludes(List<String> includes) {
		this.includes = includes;
	}

	/**
	 * @return Returns the quote include paths as strings (for #include "...")
	 */
	public List<String> getQuoteIncludes() {
		return makeAbsolute(project, quoteIncludes);
	}

	/**
	 * @param includes - quote include paths (for #include "...")
	 */
	public void setQuoteIncludes(List<String> includes) {
		quoteIncludes = includes;
	}

	/**
	 * @return Returns the symbols.
	 */
	public List<String> getSymbols() {
		return symbols;
	}

	/**
	 * @param symbols The symbols to set.
	 */
	public void setSymbols(List<String> symbols) {
		this.symbols = symbols;
	}

	/**
	 * @return Returns the discovered.
	 */
	public boolean isDiscovered() {
		return discovered;
	}

	/**
	 * @param discovered The discovered to set.
	 */
	public void setDiscovered(boolean discovered) {
		this.discovered = discovered;
	}

	public void serialize(Element cmdElem) {
		Document doc = cmdElem.getOwnerDocument();
		// serialize the command
		Element cmdDescElem = doc.createElement(CMD_DESCRIPTION_ELEM);
		for (Iterator<KVStringPair> i = compilerCommand.iterator(); i.hasNext();) {
			Element optionElem = doc.createElement(OPTION_ELEM);
			KVStringPair option = i.next();
			optionElem.setAttribute(KEY_ATTR, option.getKey());
			optionElem.setAttribute(VALUE_ATTR, option.getValue());
			cmdDescElem.appendChild(optionElem);
		}
		cmdElem.appendChild(cmdDescElem);
		// serialize includes and symbols
		Element siElem = doc.createElement(CMD_SI_ELEM);
		for (Iterator<String> j = quoteIncludes.iterator(); j.hasNext();) {
			Element siItem = doc.createElement(SI_ITEM_ELEM);
			siItem.setAttribute(KIND_ATTR, "INCLUDE_PATH"); //$NON-NLS-1$
			siItem.setAttribute(VALUE_ATTR, j.next());
			siItem.setAttribute(QUOTE_INCLUDE_ATTR, "true"); //$NON-NLS-1$
			siElem.appendChild(siItem);
		}
		for (Iterator<String> j = includes.iterator(); j.hasNext();) {
			Element siItem = doc.createElement(SI_ITEM_ELEM);
			siItem.setAttribute(KIND_ATTR, "INCLUDE_PATH"); //$NON-NLS-1$
			siItem.setAttribute(VALUE_ATTR, j.next());
			siElem.appendChild(siItem);
		}
		for (Iterator<String> j = symbols.iterator(); j.hasNext();) {
			Element siItem = doc.createElement(SI_ITEM_ELEM);
			siItem.setAttribute(KIND_ATTR, "SYMBOL_DEFINITION"); //$NON-NLS-1$
			siItem.setAttribute(VALUE_ATTR, j.next());
			siElem.appendChild(siItem);
		}
		cmdElem.appendChild(siElem);
	}

	public void deserialize(Element cmdElem) {
		// read command options
		NodeList descList = cmdElem.getElementsByTagName(CMD_DESCRIPTION_ELEM);
		if (descList.getLength() > 0) {
			Element descElem = (Element) descList.item(0);
			NodeList optionList = descElem.getElementsByTagName(OPTION_ELEM);
			for (int i = 0; i < optionList.getLength(); ++i) {
				Element optionElem = (Element) optionList.item(i);
				String key = SafeStringInterner.safeIntern(optionElem.getAttribute(KEY_ATTR));
				String value = SafeStringInterner.safeIntern(optionElem.getAttribute(VALUE_ATTR));
				KVStringPair option = new KVStringPair(key, value);
				addSCOption(option);
			}
		}
		// read associated scanner info
		NodeList siList = cmdElem.getElementsByTagName(CMD_SI_ELEM);
		if (siList.getLength() > 0) {
			Element siElem = (Element) siList.item(0);
			NodeList siItemList = siElem.getElementsByTagName(SI_ITEM_ELEM);
			for (int i = 0; i < siItemList.getLength(); ++i) {
				Element siItemElem = (Element) siItemList.item(i);
				String kind = siItemElem.getAttribute(KIND_ATTR);
				String value = siItemElem.getAttribute(VALUE_ATTR);
				String quote = siItemElem.getAttribute(QUOTE_INCLUDE_ATTR);
				if (kind.equals("INCLUDE_PATH")) { //$NON-NLS-1$
					if (quote.equals("true")) { //$NON-NLS-1$
						quoteIncludes.add(SafeStringInterner.safeIntern(value));
					} else {
						includes.add(value);
					}
				} else if (kind.equals("SYMBOL_DEFINITION")) { //$NON-NLS-1$
					symbols.add(SafeStringInterner.safeIntern(value));
				}
			}
			setDiscovered(true);
		}
	}

	public void resolveOptions(IProject project) {
		if (!isDiscovered()) {
			// that's wrong for sure, options cannot be resolved fron the optionPairs??
			ArrayList<String> symbols = new ArrayList<>();
			ArrayList<String> includes = new ArrayList<>();
			ArrayList<String> quoteincludes = new ArrayList<>();
			for (Iterator<KVStringPair> options = compilerCommand.iterator(); options.hasNext();) {
				KVStringPair optionPair = options.next();
				String key = optionPair.getKey();
				String value = optionPair.getValue();
				if (key.equals(SCDOptionsEnum.INCLUDE.toString()) || key.equals(SCDOptionsEnum.ISYSTEM.toString())) {
					includes.add(SafeStringInterner.safeIntern(value));
				} else if (key.equals(SCDOptionsEnum.IQUOTE.toString())) {
					quoteincludes.add(SafeStringInterner.safeIntern(value));
				} else if (key.equals(SCDOptionsEnum.DEFINE.toString())) {
					symbols.add(SafeStringInterner.safeIntern(value));
				}
			}
			setIncludes(includes);
			setQuoteIncludes(quoteincludes);
			setSymbols(symbols);
		}
		setDiscovered(true);
	}

	public static IPath makeRelative(IProject project, IPath path) {
		IResource resource = findResource(project, path);
		if (resource != null) {
			if (resource.getProject() == project) {
				path = resource.getProjectRelativePath();
			}
			//			else {
			//				path = resource.getFullPath();
			//			}
		}
		return path;
	}

	protected static IResource findResource(IProject project, IPath path) {
		IResource resource = project.findMember(path, false);
		if (resource == null) {
			IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
			resource = root.findMember(path, false);
			if (resource == null) {
				resource = ResourceLookup.selectFileForLocation(path, project);
			}
		}
		return resource;
	}

	public static List<String> makeRelative(IProject project, List<String> paths) {
		List<String> list = new ArrayList<>(paths.size());
		for (Iterator<String> iter = paths.iterator(); iter.hasNext();) {
			String path = iter.next();
			path = makeRelative(project, new Path(path)).toOSString();
			list.add(SafeStringInterner.safeIntern(path));
		}
		return list;
	}

	public static final String makeAbsolute(IProject project, String path) {
		IPath ppath = new Path(path);
		if (project != null && !ppath.isAbsolute()) {
			IResource res = project.findMember(ppath);
			if (res == null) {
				// To calculate path only; this does not create any file
				res = project.getFile(path);
			}
			if (res != null) {
				ppath = res.getLocation();
				if (ppath != null) {
					path = ppath.toOSString();
				}
			}
			//			path = new File(project.getLocation().toOSString(), path).getAbsolutePath();
		}
		return SafeStringInterner.safeIntern(path);
	}

	public static List<String> makeAbsolute(IProject project, List<String> paths) {
		List<String> list = new ArrayList<>(paths.size());
		for (Iterator<String> iter = paths.iterator(); iter.hasNext();) {
			String path = iter.next();
			path = makeAbsolute(project, path);
			list.add(SafeStringInterner.safeIntern(path));
		}
		return list;
	}
}
