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:
   *  Piotr Tomiak <piotr@genuitec.com> - initial API and implementation
   *  Angelo Zerr <angelo.zerr@gmail.com> - initial API and implementation
   */
  package tern.resources;
  
  import java.io.IOException;
  import java.text.MessageFormat;
  import java.util.Arrays;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.Map;
  import java.util.Set;
  import java.util.concurrent.locks.ReentrantReadWriteLock;
  import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
  import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
  
  import tern.ITernFileSynchronizer;
  import tern.ITernFile;
  import tern.ITernProject;
  import tern.TernResourcesManager;
  import tern.scriptpath.ITernScriptResource;
  import tern.scriptpath.ITernScriptPath;
  import tern.server.ITernServer;
  import tern.server.protocol.TernDoc;
  import tern.server.protocol.TernFile;
  import tern.server.protocol.TernFile.FileType;
  import tern.server.protocol.TernQuery;
  import tern.server.protocol.completions.TernCompletionsQuery;
  import tern.server.protocol.definition.TernDefinitionQuery;
  import tern.server.protocol.lint.TernLintQuery;
  import tern.server.protocol.type.TernTypeQuery;
  import tern.utils.StringUtils;
  
  import com.eclipsesource.json.JsonArray;
  import com.eclipsesource.json.JsonValue;
  
  /**
   * Tern file synchronizer is used to maintain a cache with indexed files which
   * was already parsed by the tern server to avoid parsing files on each tern
   * request. It is also responsible to keep up-to-date version of those files on
   * the server.
   */
  public class TernFileSynchronizer implements ITernFileSynchronizer {
  
  	// wait 200ms for uploader to finish
  	private static final int TIMEOUT = 200;
  
  	// allow to upload maximum 12MB
  	private static final int MAX_ALLOWED_SIZE = 12 * 1024 * 1024;
  
  	private final ITernProject project;
  
  	private final ITernFileUploader uploader;
  
  	// Access synchronization
  	private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
  	private final ReadLock readLock = lock.readLock();
  	private final WriteLock writeLock = lock.writeLock();
  
  	// Files synchronization
  	private final Map<String, String> sentFiles = new HashMap<String, String>();
  	private final Set<String> toRefresh = new HashSet<String>();
  	private final Map<ITernScriptPath, Set<String>> syncedFilesPerPath = new HashMap<ITernScriptPath, Set<String>>();
  	private ITernServer targetServer;
  
  	/**
  	 * Tern file manager constructor.
  	 */
  	public TernFileSynchronizer(ITernProject project) {
  		this.project = project;
  		this.uploader = createTernFileUploader();
  
  	}
  
  	public ITernFileUploader getTernFileUploader() {
  		return uploader;
  	}
  
  	protected ITernFileUploader createTernFileUploader() {
  		return new SynchronousTernFileUploader(project);
  	}
  
  	@Override
  	public ITernProject getProject() {
  		return project;
  	}
  
  	@Override
  	public void uploadFailed(TernDoc doc) {
  		synchronized (toRefresh) {
  			for (JsonValue val : doc.getFiles()) {
  				if (val instanceof TernFile) {
  					toRefresh.add(((TernFile) val).getName());
  				}
  			}
  		}
  	}
  
  	@Override
  	public void refresh(Object file) {
  		ITernFile tf = TernResourcesManager.getTernFile(file);
  		if (tf != null) {
  			synchronized (toRefresh) {
  				toRefresh.add(tf.getFullName(getProject()));
  			}
  		}
  	}
  
  	@Override
  	public void cleanIndexedFiles() {
  		writeLock.lock();
  		try {
  			sentFiles.clear();
  			syncedFilesPerPath.clear();
  		} finally {
  			writeLock.unlock();
  		}
  		synchronized (toRefresh) {
  			toRefresh.clear();
  		}
  	}
  
  	@Override
  	public void fillSyncedFileNames(JsonArray fileNames, ITernScriptPath path) {
  		readLock.lock();
  		try {
  			Set<String> files;
  			if (path != null) {
  				files = syncedFilesPerPath.get(path);
  			} else {
  				files = sentFiles.keySet();
  			}
  			if (files != null) {
  				for (String file : files) {
  					fileNames.add(file);
  				}
  			}
  		} finally {
  			readLock.unlock();
  		}
  	}
  
  	protected void sizeExceeded() {
  		System.out
  				.println(MessageFormat
  						.format("Size of scripts on {0} script path exceeds 12MB. Content assist might be incomplete.",
  								getProject().getName()));
  	}
  
  	@Override
  	public void ensureSynchronized() {
  		TernDoc doc = new TernDoc();
  		writeLock.lock();
  		try {
  			if (project.getTernServer() != targetServer) {
  				targetServer = project.getTernServer();
  				cleanIndexedFiles();
  			}
  			syncedFilesPerPath.clear();
  
  			Set<String> synced = new HashSet<String>(sentFiles.keySet());
  			Set<String> toRefreshLocal = new HashSet<String>();
  			synchronized (toRefresh) {
  				toRefreshLocal.addAll(toRefresh);
  				toRefresh.clear();
  			}
  			synced.removeAll(toRefreshLocal);
  
  			long totalSize = 0;
  			for (String file : synced) {
  				totalSize += sentFiles.get(file).length();
  			}
  
  			for (ITernScriptPath path : getProject().getScriptPaths()) {
  				Set<String> perPath = new HashSet<String>();
  				syncedFilesPerPath.put(path, perPath);
  				for (ITernScriptResource resource : path.getScriptResources()) {
  					// limit the size of content being sent to the Tern server
  					if (totalSize >= MAX_ALLOWED_SIZE) {
  						sizeExceeded();
  						break;
  					}
  					ITernFile file = resource.getFile();
  					if (file == null) {
  						continue;
  					}
  					String name = file.getFullName(getProject());
  					perPath.add(name);
  					if (!synced.contains(name)) {
  						try {
  							TernFile tf = file.toTernServerFile(getProject());
  							doc.addFile(tf);
  							synced.add(name);
  							totalSize += tf.getText().length();
  						} catch (IOException e) {
  							getProject().handleException(e);
  						}
  					}
  				}
  			}
  
  			toRefreshLocal.removeAll(synced);
  			for (String toRemove : toRefreshLocal) {
  				doc.delFile(toRemove);
  			}
  
  			// perform actual synchronization with the server
  			sendFiles(doc);
  		} finally {
  			writeLock.unlock();
  		}
  	}
  
  	@Override
  	public void synchronizeFile(ITernFile file) throws IOException {
  		writeLock.lock();
  		try {
  			TernFile tf = file.toTernServerFile(getProject());
  			String oldText = sentFiles.get(tf.getName());
  			if (tf.getText().equals(oldText) && !uploader.cancel(tf.getName())) {
  				// no need to synchronize the file, already up-to-date
  				return;
  			}
  			TernDoc doc = new TernDoc();
  			doc.addFile(tf);
  			sendFiles(doc);
  		} finally {
  			writeLock.unlock();
  		}
  	}
  
  	@Override
  	public void synchronizeFile(TernDoc doc, ITernFile file) throws IOException {
  		writeLock.lock();
  		try {
  			try {
  				TernQuery query = doc.getQuery();
  				if (query != null) {
  					if (TernResourcesManager.isJSFile(file)) {
  						addJSFile(doc, file);
  						return;
  					} else if (TernResourcesManager.isHTMLFile(file)) {
  						// This is HTML file case: never keep the file on the server
  						String queryType = query.getType();
  						if (TernCompletionsQuery.isQueryType(queryType) ||
  								TernDefinitionQuery.isQueryType(queryType) ||
  								query instanceof TernLintQuery ) {
  							addHTMLFile(doc, file);
  							return;
  						}	
  					}
  				}
  				TernFile tf = file.toTernServerFile(getProject());
  				String oldText = sentFiles.get(tf.getName());
  				if (tf.getText().equals(oldText)
  						&& !uploader.cancel(tf.getName())) {
  					// no need to synchronize the file, already up-to-date
  					return;
  				}
  				doc.addFile(tf);
  			} finally {
  				updateSentFiles(doc);
  				// as this is
  				// wait a bit for the sync to finish
  				uploader.join(TIMEOUT);
  			}
  		} finally {
  			writeLock.unlock();
  		}
  	}
  
  	protected void addJSFile(TernDoc doc, ITernFile file) throws IOException {
  		TernQuery query = doc.getQuery();
  		String fileName = file.getFullName(getProject());
  		query.setFile(fileName);
  		TernFile tf = file.toTernServerFile(getProject());
  		doc.addFile(tf);
  	}
  
  	protected void addHTMLFile(TernDoc doc, ITernFile file) throws IOException {
  		TernQuery query = doc.getQuery();
  		TernFile tf = file.toTernServerFile(getProject());
  		doc.addFile(tf);
  		query.set("file", "#" + (doc.getFiles().size() - 1)); //$NON-NLS-1$ //$NON-NLS-2$
  	}
  
  	@Override
  	public void synchronizeScriptPath(ITernScriptPath path, String... forced) {
  		TernDoc doc = new TernDoc();
  		writeLock.lock();
  		try {
  			// make sure we do not send duplicate files
  			Set<String> requestedFiles = new HashSet<String>(sentFiles.keySet());
  			Set<String> perPath = new HashSet<String>();
  			syncedFilesPerPath.put(path, perPath);
  
  			requestedFiles.removeAll(Arrays.asList(forced));
  
  			long totalSize = 0;
  			for (String file : requestedFiles) {
  				totalSize += sentFiles.get(file).length();
  			}
  
  			for (ITernScriptResource resource : path.getScriptResources()) {
  				// limit the number of files being sent to the Tern server
  				if (totalSize >= MAX_ALLOWED_SIZE) {
  					sizeExceeded();
  					break;
  				}
  				ITernFile file = resource.getFile();
  				if (file == null) {
  					continue;
  				}
  				String name = file.getFullName(getProject());
  				perPath.add(name);
  				if (!requestedFiles.contains(name)) {
  					try {
  						TernFile tf = file.toTernServerFile(getProject());
  						doc.addFile(tf);
  						totalSize += tf.getText().length();
  						requestedFiles.add(name);
  					} catch (IOException e) {
  						getProject().handleException(e);
  					}
  				}
  			}
  			// perform actual synchronization with the server
  			sendFiles(doc);
  		} finally {
  			writeLock.unlock();
  		}
  	}
  
  	private void updateSentFiles(TernDoc doc) {
  		for (JsonValue value : doc.getFiles()) {
  			if (value instanceof TernFile) {
  				TernFile file = (TernFile) value;
  				if (file.isType(FileType.full)) {
  					String contents = file.getText();
  					if (StringUtils.isEmpty(contents)) {
  						// treat file with empty contents as removed
  						sentFiles.remove(file.getName());
  					} else {
  						sentFiles.put(file.getName(), contents);
  					}
  				}
  			}
  		}
  	}
  
  	protected void sendFiles(TernDoc doc) {
  		if (doc.hasFiles()) {
  			updateSentFiles(doc);
  			// sync is performed asynchronously
  			uploader.request(doc);
  		}
  	}
  	
  	protected String getSentFileContent(String file) {
  		readLock.lock();
  		try {
  			return sentFiles.get(file);
  		} finally {
  			readLock.unlock();
  		}
  	}
  
  	@Override
  	public void dispose() {
  		cleanIndexedFiles();
  	}
  
  }
  