content: /**
   *  Copyright (c) 2013-2016 Angelo ZERR and Genuitec LLC.
   *  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:
   *  Angelo Zerr <angelo.zerr@gmail.com> - initial API and implementation
   *  Piotr Tomiak <piotr@genuitec.com> - refactoring of file management API
  - *  								  - asynchronous file upload
   */
  package tern.resources;
  
  import java.io.File;
  import java.io.FileReader;
  import java.io.FileWriter;
  import java.io.IOException;
  import java.io.Writer;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collection;
  import java.util.Collections;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.Map.Entry;
  import java.util.Set;
  
  import org.w3c.dom.Document;
  import org.w3c.dom.Node;
  
  import com.eclipsesource.json.Json;
  import com.eclipsesource.json.JsonArray;
  import com.eclipsesource.json.JsonObject;
  import com.eclipsesource.json.JsonValue;
  import com.eclipsesource.json.ParseException;
  import com.eclipsesource.json.WriterConfig;
  
  import tern.EcmaVersion;
  import tern.ITernFile;
  import tern.ITernFileSynchronizer;
  import tern.ITernProject;
  import tern.TernException;
  import tern.internal.resources.InternalTernResourcesManager;
  import tern.repository.ITernRepository;
  import tern.scriptpath.ITernScriptPath;
  import tern.scriptpath.impl.dom.DOMElementsScriptPath;
  import tern.server.ITernDef;
  import tern.server.ITernPlugin;
  import tern.server.ITernServer;
  import tern.server.TernDef;
  import tern.server.protocol.ITernResultsCollector;
  import tern.server.protocol.JsonHelper;
  import tern.server.protocol.TernDoc;
  import tern.server.protocol.TernQuery;
  import tern.server.protocol.lint.ITernLintCollector;
  import tern.server.protocol.push.IMessageHandler;
  import tern.utils.IOUtils;
  
  /**
   * Tern project configuration.
   * 
   * <pre>
   * A .tern-project file is a JSON file in a format like this:
   * 
   * {
   *   "libs": [
   *     "browser",
   *     "jquery"
   *   ],
   *   "loadEagerly": [
   *     "importantfile.js"
   *   ],
   *   "plugins": {
   *     "requirejs": {
   *       "baseURL": "./",
   *       "paths": {}
   *     }
   *   }
   * }
   * </pre>
   * 
   * @see http://ternjs.net/doc/manual.html#configuration
   */
  public class TernProject extends JsonObject implements ITernProject {
  
  	private static final long serialVersionUID = 1L;
  
  	private static final String ECMA_VERSION_FIELD_NAME = "ecmaVersion"; //$NON-NLS-1$
  	private static final String PLUGINS_FIELD_NAME = "plugins"; //$NON-NLS-1$
  	private static final String LIBS_FIELD_NAME = "libs"; //$NON-NLS-1$
  	private static final String LOAD_EAGERLY_FIELD_NAME = "loadEagerly"; //$NON-NLS-1$
  
  	private final File projectDir;
  	private File ternProjectFile;
  	private ITernRepository repository;
  
  	protected final Object serverLock = new Object();
  	private ITernPlugin[] linters;
  
  	/**
  	 * tern file synchronizer.
  	 */
  	private ITernFileSynchronizer fileSynchronizer;
  
  	private JsonObject lastTernProject;
  
  	private Object libLock = new Object();
  
  	private List<ITernPlugin> lastLinters;
  
  	private final Map<String, List<IMessageHandler>> messageListeners;
  
  	/**
  	 * Tern project constructor.
  	 * 
  	 * @param projectDir
  	 *            the project base dir.
  	 */
  	public TernProject(File projectDir) {
  		this.projectDir = projectDir;
  		this.ternProjectFile = new File(projectDir, TERN_PROJECT_FILE);
  		this.fileSynchronizer = InternalTernResourcesManager.getInstance().createTernFileSynchronizer(this);
  		this.messageListeners = new HashMap<String, List<IMessageHandler>>();
  	}
  
  	@Override
  	public String getName() {
  		return projectDir.getName();
  	}
  
  	/**
  	 * Returns the project base dir.
  	 * 
  	 * @return the project base dir.
  	 */
  	@Override
  	public File getProjectDir() {
  		return projectDir;
  	}
  
  	@Override
  	public File getTernProjectFile() {
  		return ternProjectFile;
  	}
  
  	@Override
  	public void setEcmaVersion(EcmaVersion ecmaVersion) {
  		super.set(ECMA_VERSION_FIELD_NAME, ecmaVersion.getVersion());
  	}
  
  	@Override
  	public EcmaVersion getEcmaVersion() {
  		int version = super.getInt(ECMA_VERSION_FIELD_NAME, -1);
  		return EcmaVersion.get(version);
  	}
  
  	/**
  	 * Returns true if lib or plugins exists and false otheriwse.
  	 * 
  	 * @return true if lib or plugins exists and false otheriwse.
  	 */
  	public boolean hasModules() {
  		return hasLibs() || hasPlugins();
  	}
  
  	/**
  	 * Add JSON Type Definition.
  	 * 
  	 * @param lib
  	 *            the JSON Type Definition.
  	 */
  	@Override
  	public void addLib(ITernDef lib) {
  		addLib(lib.getName());
  	}
  
  	/**
  	 * Add JSON Type Definition.
  	 * 
  	 * @param lib
  	 *            the JSON Type Definition.
  	 */
  	@Override
  	public void addLib(String lib) {
  		synchronized (libLock) {
  			if (!hasLib(lib)) {
  				getLibs().add(lib);
  			}
  		}
  	}
  
  	public boolean hasLib(TernDef lib) {
  		return hasLib(lib.getName());
  	}
  
  	/**
  	 * Returns true if the given lib exists and false otherwise.
  	 * 
  	 * @param lib
  	 * @return true if the given lib exists and false otherwise.
  	 */
  	@Override
  	public boolean hasLib(String lib) {
  		synchronized (libLock) {
  			JsonArray libs = getLibs();
  			if (libs != null) {
  				for (JsonValue l : libs) {
  					if (l.isString() && l.asString().equals(lib))
  						return true;
  				}
  			}
  			return false;
  		}
  	}
  
  	/**
  	 * Returns true if lib exist and false otherwise.
  	 * 
  	 * @return
  	 */
  	public boolean hasLibs() {
  		return super.get(LIBS_FIELD_NAME) != null;
  	}
  
  	/**
  	 * Returns true if the given lib exists and false otherwise.
  	 * 
  	 * @param lib
  	 * @return true if the given lib exists and false otherwise.
  	 */
  	@Override
  	public boolean hasLib(ITernDef lib) {
  		return hasLib(lib.getName());
  	}
  
  	/**
  	 * Return the JSON Type Definitions of the tern project.
  	 * 
  	 * @return the JSON Type Definitions of the tern project.
  	 */
  	@Override
  	public JsonArray getLibs() {
  		synchronized (libLock) {
  			JsonArray libs = (JsonArray) super.get(LIBS_FIELD_NAME);
  			if (libs == null) {
  				libs = new JsonArray();
  				add(LIBS_FIELD_NAME, libs);
  			}
  			return libs;
  		}
  	}
  
  	/**
  	 * Clear JSON Type Definitions.
  	 */
  	@Override
  	public void clearLibs() {
  		synchronized (libLock) {
  			remove(LIBS_FIELD_NAME);
  		}
  	}
  
  	/**
  	 * Returns true if plugins exist and false otherwise.
  	 * 
  	 * @return
  	 */
  	public boolean hasPlugins() {
  		return super.get(PLUGINS_FIELD_NAME) != null;
  	}
  
  	/**
  	 * Add Tern plugin.
  	 * 
  	 * @param plugin
  	 *            the tern plugin to add.
  	 * @return true if plugin to add, replace an existing plugin and false
  	 *         otherwise.
  	 */
  	@Override
  	public void addPlugin(ITernPlugin plugin) {
  		addPlugin(plugin, null);
  	}
  
  	/**
  	 * Add Tern plugin with options.
  	 * 
  	 * @param plugin
  	 *            the tern plugin to add.
  	 * @param options
  	 *            plugin options.
  	 */
  	@Override
  	public void addPlugin(ITernPlugin plugin, JsonValue options) {
  		JsonObject plugins = getPlugins();
  		if (options == null)
  			options = new JsonObject();
  		if (!hasPlugin(plugin)) {
  			plugins.add(plugin.getName(), options);
  		} else {
  			if (!JsonHelper.isSameJson(plugins.get(plugin.getName()), options)) {
  				plugins.set(plugin.getName(), options);
  			}
  		}
  	}
  
  	/**
  	 * Returns true if the given plugin exists and false otherwise.
  	 * 
  	 * @param plugin
  	 * @return true if the given plugin exists and false otherwise.
  	 */
  	@Override
  	public boolean hasPlugin(ITernPlugin plugin) {
  		return hasPlugin(plugin.getName());
  	}
  
  	/**
  	 * Returns true if the given plugin exists and false otherwise.
  	 * 
  	 * @param plugin
  	 * @return true if the given plugin exists and false otherwise.
  	 */
  	@Override
  	public boolean hasPlugin(String plugin) {
  		return getPlugin(this, plugin) != null;
  	}
  
  	private static JsonValue getPlugin(JsonObject json, String plugin) {
  		JsonObject plugins = (JsonObject) json.get(PLUGINS_FIELD_NAME);
  		return plugins == null ? null : plugins.get(plugin);
  	}
  
  	/**
  	 * Return the JSON plugins of the tern project.
  	 * 
  	 * @return the JSON plugins of the tern project.
  	 */
  	@Override
  	public JsonObject getPlugins() {
  		JsonObject plugins = (JsonObject) super.get(PLUGINS_FIELD_NAME);
  		if (plugins == null) {
  			plugins = new JsonObject();
  			add(PLUGINS_FIELD_NAME, plugins);
  		}
  		return plugins;
  	}
  
  	/**
  	 * Clear plugins.
  	 */
  	@Override
  	public void clearPlugins() {
  		remove(PLUGINS_FIELD_NAME);
  		this.linters = null;
  	}
  
  	@Override
  	public ITernPlugin[] getLinters() {
  		if (linters == null) {
  			Collection<ITernPlugin> plugins = new ArrayList<ITernPlugin>();
  			collectLinters(plugins);
  			linters = plugins.toArray(ITernPlugin.EMPTY_PLUGIN);
  		}
  		return linters;
  	}
  
  	protected void collectLinters(Collection<ITernPlugin> plugins) {
  		// dynamic linter coming from repository
  		ITernRepository repository = getRepository();
  		if (repository != null) {
  			addLinter(plugins, repository.getLinters());
  		}
  	}
  
  	private void addLinter(Collection<ITernPlugin> plugins, ITernPlugin[] knownLintPlugins) {
  		ITernPlugin knownLintPlugin;
  		for (int i = 0; i < knownLintPlugins.length; i++) {
  			knownLintPlugin = knownLintPlugins[i];
  			if (hasPlugin(knownLintPlugin) && !plugins.contains(knownLintPlugin)) {
  				plugins.add(knownLintPlugin);
  			}
  		}
  	}
  
  	public void addLoadEagerlyPattern(String pattern) {
  		JsonArray patterns = (JsonArray) super.get(LOAD_EAGERLY_FIELD_NAME);
  		if (patterns == null) {
  			patterns = new JsonArray();
  			add(LOAD_EAGERLY_FIELD_NAME, patterns);
  		}
  		patterns.add(pattern);
  	}
  
  	/**
  	 * Save the tern project in the file .tern-project of the project base dir.
  	 * 
  	 * @throws IOException
  	 */
  	@Override
  	public final void save() throws IOException {
  		try {
  			doSave();
  		} finally {
  			// reset();
  		}
  	}
  
  	/**
  	 * Save the tern project in the file .tern-project of the project base dir.
  	 * 
  	 * @throws IOException
  	 */
  	protected void doSave() throws IOException {
  		if (isDirty()) {
  			getProjectDir().mkdirs();
  			Writer writer = null;
  			try {
  				writer = new FileWriter(ternProjectFile);
  				super.writeTo(writer, WriterConfig.PRETTY_PRINT);
  			} finally {
  				if (writer != null) {
  					IOUtils.closeQuietly(writer);
  				}
  			}
  			reset();
  		}
  	}
  
  	/**
  	 * Load the tern project from the .tern-project of the project base dir.
  	 * 
  	 * @throws IOException
  	 */
  	public final void load() throws IOException {
  		try {
  			Collection<ITernPlugin> lastLinters = getLastLinters();
  			JsonObject lastTernProject = this.lastTernProject;
  			doLoad();
  			if (lastLinters != null) {
  				ITernPlugin[] newLinters = getLinters();
  				if (isLintersChanged(Arrays.asList(newLinters), this, lastLinters, lastTernProject)) {
  					onLintersChanged();
  				}
  			}
  		} finally {
  			reset();
  			this.lastLinters = new ArrayList<ITernPlugin>();
  			collectLinters(lastLinters);
  		}
  	}
  
  	/**
  	 * Listener lanched when linter is added or removed from the .tern-project.
  	 */
  	protected void onLintersChanged() {
  
  	}
  
  	/**
  	 * Returns true if linters has changed and false otherwise.
  	 * 
  	 * @param oldLinters
  	 * @param newLinters
  	 * @param lastTernProject
  	 * @return true if linters has changed and false otherwise.
  	 */
  	private boolean isLintersChanged(Collection<ITernPlugin> newLinters, JsonObject newTernProject,
  			Collection<ITernPlugin> lastLinters, JsonObject lastTernProject) {
  		if (!lastLinters.equals(newLinters)) {
  			// some linter was added/removed
  			return true;
  		}
  		if (lastTernProject == null) {
  			return false;
  		}
  		// check if linter options has changed.
  		JsonValue lastOptions = null;
  		JsonValue newOptions = null;
  		for (ITernPlugin linter : lastLinters) {
  			lastOptions = getPlugin(lastTernProject, linter.getName());
  			newOptions = getPlugin(newTernProject, linter.getName());
  			if (!JsonHelper.isSameJson(lastOptions, newOptions)) {
  				return true;
  			}
  		}
  		return false;
  	}
  
  	protected void reset() {
  		this.lastTernProject = Json.parse(toString()).asObject();
  		this.linters = null;
  	}
  
  	public List<ITernPlugin> getLastLinters() {
  		return lastLinters;
  	}
  
  	/**
  	 * Load the tern project from the .tern-project of the project base dir.
  	 * 
  	 * @throws IOException
  	 */
  	protected void doLoad() throws IOException {
  		if (ternProjectFile.exists()) {
  			try {
  				JsonHelper.readFrom(new FileReader(ternProjectFile), this);
  			} catch (ParseException e) {
  				throw new IOException(e);
  			}
  		} else {
  			createEmptyTernProjectFile();
  		}
  		reset();
  	}
  
  	/**
  	 * Create an empty a .tern-project file.
  	 * 
  	 * @throws IOException
  	 */
  	protected void createEmptyTernProjectFile() throws IOException {
  		save();
  	}
  
  	@Override
  	public void handleException(Throwable t) {
  		t.printStackTrace();
  	}
  
  	/**
  	 * Returns file cache manager.
  	 * 
  	 * @return
  	 */
  	@Override
  	public ITernFileSynchronizer getFileSynchronizer() {
  		return fileSynchronizer;
  	}
  
  	@Override
  	public ITernServer getTernServer() {
  		return null;
  	}
  
  	@Override
  	public ITernFile getFile(String name) {
  		return InternalTernResourcesManager.getInstance().getTernFile(this, name);
  	}
  
  	@Override
  	public ITernFile getFile(Object fileObject) {
  		return InternalTernResourcesManager.getInstance().getTernFile(fileObject);
  	}
  
  	@Override
  	public List<ITernScriptPath> getScriptPaths() {
  		return Collections.emptyList();
  	}
  
  	@Override
  	public Object getAdapter(@SuppressWarnings("rawtypes") Class adapterClass) {
  		if (adapterClass == File.class) {
  			return getProjectDir();
  		}
  		return null;
  	}
  
  	protected void synchronize(TernDoc doc, JsonArray names, ITernScriptPath scriptPath, Node domNode, ITernFile file) {
  		ITernFileSynchronizer synchronizer = getFileSynchronizer();
  		synchronizer.ensureSynchronized();
  		if (file != null) {
  			if (doc.getQuery() != null) {
  				doc.getQuery().setFile(file.getFullName(this));
  			}
  			if (domNode != null) {
  				DOMElementsScriptPath domPath = createDOMElementsScriptPath(domNode, file);
  				synchronizer.synchronizeScriptPath(domPath, file.getFullName(this));
  			} else {
  				try {
  					synchronizer.synchronizeFile(doc, file);
  				} catch (IOException e) {
  					handleException(e);
  				}
  			}
  		}
  		if (names != null) {
  			synchronizer.fillSyncedFileNames(names, scriptPath);
  		}
  	}
  
  	protected DOMElementsScriptPath createDOMElementsScriptPath(Node domNode, ITernFile file) {
  		final Document doc = domNode.getOwnerDocument();
  		return new DOMElementsScriptPath(this, file, null) {
  			@Override
  			protected Document getDocument() {
  				return doc;
  			}
  		};
  	}
  
  	@Override
  	public void request(TernQuery query, ITernFile file, ITernResultsCollector collector)
  			throws IOException, TernException {
  		request(query, null, null, null, file, collector);
  	}
  
  	@Override
  	public void request(TernQuery query, JsonArray names, ITernScriptPath scriptPath, Node domNode, ITernFile file,
  			ITernResultsCollector collector) throws IOException, TernException {
  		TernDoc doc = new TernDoc(query);
  		synchronize(doc, names, scriptPath, domNode, file);
  		ITernServer server = getTernServer();
  		server.request(doc, collector);
  	}
  
  	@Override
  	public void request(TernQuery query, ITernFile file, boolean synch, ITernLintCollector collector)
  			throws IOException, TernException {
  		TernDoc doc = new TernDoc(query);
  		if (synch) {
  			synchronize(doc, null, null, null, file);
  		} else if (file != null) {
  			if (doc.getQuery() != null) {
  				doc.getQuery().setFile(file.getFullName(this));
  			}
  		}
  		ITernServer server = getTernServer();
  		server.request(doc, collector);
  	}
  
  	@Override
  	public void request(TernQuery query, ITernLintCollector collector) throws IOException, TernException {
  		request(query, null, true, collector);
  	}
  
  	@Override
  	public ITernRepository getRepository() {
  		return repository;
  	}
  
  	@Override
  	public void setRepository(ITernRepository repository) {
  		this.repository = repository;
  	}
  
  	public boolean isDirty() {
  		return !JsonHelper.isSameJson(this, lastTernProject);
  	}
  
  	@Override
  	public void on(String type, IMessageHandler handler) {
  		synchronized (messageListeners) {
  			List<IMessageHandler> handlers = messageListeners.get(type);
  			if (handlers == null) {
  				handlers = new ArrayList<IMessageHandler>();
  				messageListeners.put(type, handlers);
  			}
  			if (!handlers.contains(handler)) {
  				handlers.add(handler);
  			}
  		}
  		synchronized (serverLock) {
  			ITernServer ternServer = getTernServer();
  			if (ternServer != null) {
  				ternServer.on(type, handler);
  			}
  		}
  	}
  
  	@Override
  	public void off(String type, IMessageHandler handler) {
  		synchronized (messageListeners) {
  			List<IMessageHandler> handlers = messageListeners.get(type);
  			if (handlers != null) {
  				handlers.remove(handler);
  			}
  		}
  		synchronized (serverLock) {
  			ITernServer ternServer = getTernServer();
  			if (ternServer != null) {
  				ternServer.off(type, handler);
  			}
  		}
  	}
  
  	protected void copyMessageListeners() {
  		synchronized (serverLock) {
  			ITernServer ternServer = getTernServer();
  			if (ternServer != null) {
  				Set<Entry<String, List<IMessageHandler>>> entries = messageListeners.entrySet();
  				String type = null;
  				List<IMessageHandler> handlers;
  				for (Entry<String, List<IMessageHandler>> entry : entries) {
  					type = entry.getKey();
  					handlers = entry.getValue();
  					for (IMessageHandler handler : handlers) {
  						ternServer.on(type, handler);
  					}
  				}
  
  			}
  		}
  	}
  }
  