/*******************************************************************************
 * Copyright (c) 2007, 2013 Chase Technology Ltd - http://www.chasetechnology.co.uk
 * 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:
 *     Doug Satchwell (Chase Technology Ltd) - initial API and implementation
 *     David Carver (STAR) -  bug 243577 - Added retrieving all called-templates.
 *     David Carver (STAR) -  bug 246503 - Handled nested circular includes.
 *     Jesper S Moller     -  bug 406262 - StackOverflow when checking for circular includes on next level import
 *******************************************************************************/
package org.eclipse.wst.xsl.core.model;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.eclipse.core.resources.IFile;
import org.eclipse.wst.xsl.core.XSLCore;
import org.eclipse.wst.xsl.core.internal.util.Debug;

/**
 * The composed stylesheet, consisting of all templates and variables available
 * via imports and includes.
 * 
 * <p>
 * The <code>fix()</code> method does the actual work of populating the fields
 * of this, so it must be called before calling any of the other methods.
 * </p>
 * 
 * <p>
 * Note that this model may not be valid - for instance there may be more than
 * one named template for a given name or more than one global variable with a
 * given name.
 * </p>
 * 
 * @author Doug Satchwell
 * @since 1.0
 */
public class StylesheetModel extends XSLModelObject {
	private final Stylesheet stylesheet;
	boolean circularReference;
	final Set<IFile> files = new HashSet<IFile>();
	final Set<Stylesheet> stylesheets = new HashSet<Stylesheet>();
	final List<Include> includeModel = new ArrayList<Include>();
	final List<Import> importModel = new ArrayList<Import>();
	final Set<Template> templateSet = new HashSet<Template>();
	final List<Template> templates = new ArrayList<Template>();
	final List<Variable> globalVariables = new ArrayList<Variable>();
	final List<CallTemplate> callTemplates = new ArrayList<CallTemplate>();
	final List<Function> functions = new ArrayList<Function>();

	/**
	 * Create a new instance of this.
	 * 
	 * @param stylesheet
	 *            the stylesheet that this is the model for
	 */
	public StylesheetModel(Stylesheet stylesheet) {
		this.stylesheet = stylesheet;
	}

	/**
	 * Get all stylesheets that are included in this stylesheet anywhere in the
	 * hierarchy via either import or include.
	 * 
	 * @return the set of stylesheets in the entire hierarchy
	 */
	public List<Include> getIncludes() {
		return includeModel;
	}

	/**
	 * Get all files that are included in this stylesheet anywhere in the
	 * hierarchy via either import or include.
	 * 
	 * @return the set of files in the entire hierarchy
	 */
	public Set<IFile> getFileDependencies() {
		return files;
	}

	/**
	 * Get the stylesheet that this is the model for.
	 * 
	 * @return the stylesheet that this is the model for
	 */
	public Stylesheet getStylesheet() {
		return this.stylesheet;
	}

	/**
	 * Get all global variables that are included in this stylesheet anywhere in
	 * the hierarchy via either import or include.
	 * 
	 * @return the set of files in the entire hierarchy
	 */
	public List<Variable> getGlobalVariables() {
		return globalVariables;
	}

	/**
	 * Get all templates that are included in this stylesheet anywhere in the
	 * hierarchy via either import or include.
	 * 
	 * @return the set of templates in the entire hierarchy
	 */
	public List<Template> getTemplates() {
		return templates;
	}

	/**
	 * A utility method that traverses all stylesheet in the hierarchy of
	 * stylesheets (not including the current stylesheet), and adds all their
	 * templates to the returned list. Therefore the returned list has no regard
	 * for whether a template is 'visible' (i.e. whether it might be overridden
	 * since it was included via an import). The order of the templates in the
	 * list is arbitrary.
	 * 
	 * @return an unordered list of all templates from all stylesheets.
	 */
	public List<Template> findAllNestedTemplates() {
		List<Template> allTemplates = new ArrayList<Template>();
		for (Stylesheet stylesheet : stylesheets) {
			allTemplates.addAll(stylesheet.getTemplates());
		}
		return allTemplates;
	}

	/**
	 * Get all named templates that are included in this stylesheet anywhere in
	 * the hierarchy via either import or include which have the given name.
	 * 
	 * @param name
	 *            the template name
	 * @return the set of named templates with the given name
	 */
	public List<Template> getTemplatesByName(String name) {
		List<Template> matching = new ArrayList<Template>(templates.size());
		for (Template template : templates) {
			if (name.equals(template.getName()))
				matching.add(template);
		}
		return matching;
	}

	/**
	 * Get all templates that match the given template (determined from
	 * <code>Template.equals()</code>).
	 * 
	 * @param toMatch
	 *            the template to match
	 * @return the set of templates that match
	 */
	public List<Template> findMatching(Template toMatch) {
		List<Template> matching = new ArrayList<Template>(templates.size());
		for (Template template : templates) {
			if (template.equals(toMatch))
				matching.add(template);
		}
		return matching;
	}

	/**
	 * Get whether this has a circular reference anywhere in its import/included
	 * hierarchy.
	 * 
	 * @return <code>true</code> if this has a circular reference
	 */
	public boolean hasCircularReference() {
		return circularReference;
	}

	/**
	 * Perform the process of traversing the hierarchy to determine all of the
	 * properties of this. Note that this method may force other
	 * <code>StylesheetModel</code>'s to be built during the process of fixing.
	 */
	public void fix() {
		long start = System.currentTimeMillis();

		if (Debug.debugXSLModel) {
			System.out.println("Fixing " + stylesheet.getFile() + "..."); //$NON-NLS-1$ //$NON-NLS-2$
		}
		templates.addAll(stylesheet.getTemplates());
		templateSet.addAll(stylesheet.getTemplates());
		globalVariables.addAll(stylesheet.globalVariables);
		callTemplates.addAll(stylesheet.getCalledTemplates());
		functions.addAll(stylesheet.getFunctions());
		for (Include inc : stylesheet.getIncludes()) {
			handleInclude(inc);
		}
		for (Import inc : stylesheet.getImports()) {
			handleInclude(inc);
		}
		circularReference = checkCycles();
		if (Debug.debugXSLModel) {
			long end = System.currentTimeMillis();
			System.out
					.println("FIX " + stylesheet.getFile() + " in " + (end - start) + "ms"); //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$
		}
	}

	private boolean checkCycles() {
		Set<IFile> seen = new HashSet<IFile>();
		IFile mainFile = getStylesheet().getFile();
		if (checkCycles(mainFile, seen)) return true;

		// For the remaining files, assume 
		seen.add(mainFile);
		for (IFile file : files) {
			if (file.equals(mainFile)) continue;
			if (checkCycles(file, seen)) return true;
		}
		return false;
	}
	
	public static boolean checkCycles(IFile included, Set<IFile> seen) {
		if (seen.contains(included)) return true;
		seen.add(included);
		
		StylesheetModel includedModel = XSLCore.getInstance().getStylesheet(
				included);

		for (Include inc : includedModel.getIncludes()) {
			IFile includedFile = inc.getHrefAsFile();
			if (checkCycles(includedFile, seen)) return true;
		}

		seen.remove(included);
		return false;
	}
	
	private void handleInclude(Include include) {
		IFile file = include.getHrefAsFile();

		if (file == null || !file.exists()) {
			return;
		}

		files.add(file);

		StylesheetModel includedModel = XSLCore.getInstance().getStylesheet(
				file);
		if (includedModel == null)
			return;
		stylesheets.add(includedModel.getStylesheet());
		globalVariables.addAll(includedModel.globalVariables);
		callTemplates.addAll(includedModel.getCallTemplates());
		if (include.getIncludeType() == Include.INCLUDE) {
			includeModel.add(include);
			templates.addAll(includedModel.getTemplates());
			templateSet.addAll(includedModel.getTemplates());
		} else {
			importModel.add((Import) include);
			for (Template includedTemplate : includedModel.getTemplates()) {
				if (!templateSet.contains(includedTemplate)) {
					templates.add(includedTemplate);
					templateSet.add(includedTemplate);
				}
			}
		}
	}

	@Override
	public Type getModelType() {
		return Type.STYLESHEET_MODEL;
	}

	public List<CallTemplate> getCallTemplates() {
		return callTemplates;
	}

	/**
	 * Get a List of all functions that are known.
	 * @return 
	 * @since 1.1
	 */
	public List<Function> getFunctions() {
		return functions;
	}

	/**
	 * Get all functions that are included in this stylesheet anywhere in the
	 * hierarchy via either import or include which have the given name.
	 * 
	 * @param name
	 *            the template name
	 * @return the set of named templates with the given name
	 * @since 1.1
	 */
	public List<Function> getFunctionByName(String name) {
		List<Function> matching = new ArrayList<Function>(functions.size());
		for (Function function : functions) {
			if (name.equals(function.getName()))
				matching.add(function);
		}
		return matching;
	}

}