/*
 * Copyright (C) 2011 lightcouch.org
 *
 * 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.
 */

package org.lightcouch;

import static java.lang.String.format;
import static org.lightcouch.CouchDbUtil.assertNotEmpty;
import static org.lightcouch.CouchDbUtil.listResources;
import static org.lightcouch.CouchDbUtil.readFile;
import static org.lightcouch.CouchDbUtil.removeExtension;
import static org.lightcouch.URIBuilder.buildUri;

import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.lightcouch.DesignDocument.MapReduce;

import com.google.gson.JsonArray;
import com.google.gson.JsonObject;

/**
 * Provides API to work with design documents. 
 * <h3>Usage Example:</h3>
 * <pre>
 * // read from system files
 * DesignDocument design1 = dbClient.design().getFromDesk("example");
 * 
 * // sync with the database
 * dbClient.design().synchronizeWithDb(design1);
 * 
 * // sync all with the database
 * dbClient.syncDesignDocsWithDb();
 * 
 * // read from the database
 * DesignDocument design2 = dbClient.design().getFromDb("_design/example");
 * </pre>
 * @see DesignDocument
 * @since 0.0.2
 * @author Ahmed Yehia
 */
public class CouchDbDesign {
	
	private static final String DESIGN_DOCS_DIR = "design-docs";
	private static final String JAVASCRIPT      = "javascript";
	private static final String DESIGN_PREFIX   = "_design/";
	private static final String VALIDATE_DOC    = "validate_doc_update";
	private static final String VIEWS           = "views";
	private static final String FILTERS         = "filters";
	private static final String SHOWS           = "shows";
	private static final String LISTS           = "lists";
	private static final String UPDATES         = "updates";
	private static final String REWRITES        = "rewrites";
	private static final String FULLTEXT        = "fulltext";
	private static final String INDEXES         = "indexes";
	private static final String MAP_JS          = "map.js";
	private static final String REDUCE_JS       = "reduce.js";
	
	private CouchDbClientBase dbc;
	
	CouchDbDesign(CouchDbClientBase dbc) {
		this.dbc = dbc;
	}
	
	/**
	 * Synchronizes a design document to the Database.
	 * <p>This method will first try to find a document in the database with the same id
	 * as the given document, if it is not found then the given document will be saved to the database.
	 * <p>If the document was found in the database, it will be compared with the given document using
	 *  {@code equals()}. If both documents are not equal, then the given document will be saved to the 
	 *  database and updates the existing document.
	 * @param document The design document to synchronize
	 * @return {@link Response} as a result of a document save or update, or returns {@code null} if no 
	 * action was taken and the document in the database is up-to-date with the given document.
	 */
	public Response synchronizeWithDb(DesignDocument document) {
		assertNotEmpty(document, "Document");
		DesignDocument documentFromDb = null;
		try {
			documentFromDb = getFromDb(document.getId());
		} catch (NoDocumentException e) {
			return dbc.save(document);
		}
		if(!document.equals(documentFromDb)) { 
			document.setRevision(documentFromDb.getRevision());
			return dbc.update(document);
		}
		return null;
	}
	
	/**
	 * Synchronize all design documents on desk to the database.
	 * @see #synchronizeWithDb(DesignDocument)
	 * @see CouchDbClient#syncDesignDocsWithDb()
	 */
	public void synchronizeAllWithDb() {
		List<DesignDocument> documents = getAllFromDesk();
		for (DesignDocument dd : documents) {
			synchronizeWithDb(dd);
		}
	}
	
	/**
	 * Gets a design document from the database.
	 * @param id The document id
	 * @return {@link DesignDocument}
	 */
	public DesignDocument getFromDb(String id) {
		assertNotEmpty(id, "id");
		final URI uri = buildUri(dbc.getDBUri()).path(id).build();
		return dbc.get(uri, DesignDocument.class);
	}
	
	/**
	 * Gets a design document from the database.
	 * @param id The document id
	 * @param rev The document revision
	 * @return {@link DesignDocument}
	 */
	public DesignDocument getFromDb(String id, String rev) {
		assertNotEmpty(id, "id");
		assertNotEmpty(id, "rev");
		final URI uri = buildUri(dbc.getDBUri()).path(id).query("rev", rev).build();
		return dbc.get(uri, DesignDocument.class);
	}
	
	/**
	 * Gets all design documents from desk.
	 * @see #getFromDesk(String)
	 * @return a list of all design documents
	 */
	public List<DesignDocument> getAllFromDesk() {
		final List<DesignDocument> designDocsList = new ArrayList<DesignDocument>();
		for (String docName : listResources(format("%s/", DESIGN_DOCS_DIR))) {
			designDocsList.add(getFromDesk(docName));
		} 
		return designDocsList;
	}
	
	/**
	 * Gets a design document from desk.
	 * @param id The document id to get.
	 * @return {@link DesignDocument}
	 */
	public DesignDocument getFromDesk(String id) {
		assertNotEmpty(id, "id");
		final DesignDocument dd = new DesignDocument();
		final String rootPath = format("%s/%s/", DESIGN_DOCS_DIR, id);
		final List<String> elements = listResources(rootPath);
		if(elements == null) {
			throw new IllegalArgumentException("Design docs directory cannot be empty.");
		}
		// Views
		Map<String, MapReduce> views = null;
		if(elements.contains(VIEWS)) { 
			views = new HashMap<String, MapReduce>();
			final String viewsPath = format("%s%s/", rootPath, VIEWS);
			for (String viewDirName : listResources(viewsPath)) { // views sub-dirs
				final MapReduce mr = new MapReduce();
				final String viewPath = format("%s%s/", viewsPath, viewDirName);
				final List<String> dirList = listResources(viewPath);
				for (String fileName : dirList) { // view files
					final String def = readFile(format("/%s%s", viewPath, fileName));
					if(MAP_JS.equals(fileName))
						mr.setMap(def);
					else if(REDUCE_JS.equals(fileName))
						mr.setReduce(def);
				} // /foreach view files
				views.put(viewDirName, mr);
			} // /foreach views sub-dirs
		} // /views
		dd.setId(DESIGN_PREFIX + id); 
		dd.setLanguage(JAVASCRIPT);
		dd.setViews(views);
		dd.setFilters(populateMap(rootPath, elements, FILTERS));
		dd.setShows(populateMap(rootPath, elements, SHOWS));
		dd.setLists(populateMap(rootPath, elements, LISTS));
		dd.setUpdates(populateMap(rootPath, elements, UPDATES));
		dd.setValidateDocUpdate(readContent(elements, rootPath, VALIDATE_DOC));
		dd.setRewrites(dbc.getGson().fromJson(readContent(elements, rootPath, REWRITES), JsonArray.class));
		dd.setFulltext(dbc.getGson().fromJson(readContent(elements, rootPath, FULLTEXT), JsonObject.class));
		dd.setIndexes(dbc.getGson().fromJson(readContent(elements, rootPath, INDEXES), JsonObject.class));
		return dd;
	}
	
	private Map<String, String> populateMap(String rootPath, List<String> elements, String element) {
		Map<String, String> functionsMap = null;
		if(elements.contains(element)) {
			functionsMap = new HashMap<String, String>();
			String path = format("%s%s/", rootPath, element);
			for (String fileName : listResources(path)) {
				String contents = readFile(format("/%s%s", path, fileName));
				functionsMap.put(removeExtension(fileName), contents);
			}
		}
		return functionsMap;
	}
	
	public String readContent(List<String> elements, String rootPath, String element) {
		if(elements.contains(element)) { 
			String path = format("%s%s/", rootPath, element);
			List<String> dirList = listResources(path);
			for (String file : dirList) {
				String contents = readFile(format("/%s%s", path, file));
				return contents;
			}
		} 
		return null;
	}
}
