/*******************************************************************************
 * Copyright (c) 2004, 2017 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 Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.ui.internal.intro.impl.model;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Vector;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.help.UAContentFilter;
import org.eclipse.help.internal.UAElementFactory;
import org.eclipse.help.internal.util.ProductPreferences;
import org.eclipse.jface.util.SafeRunnable;
import org.eclipse.ui.IPropertyListener;
import org.eclipse.ui.internal.intro.impl.FontSelection;
import org.eclipse.ui.internal.intro.impl.IntroPlugin;
import org.eclipse.ui.internal.intro.impl.model.loader.IntroContentParser;
import org.eclipse.ui.internal.intro.impl.model.loader.ModelLoaderUtil;
import org.eclipse.ui.internal.intro.impl.model.util.BundleUtil;
import org.eclipse.ui.internal.intro.impl.model.util.ModelUtil;
import org.eclipse.ui.internal.intro.impl.util.IntroEvaluationContext;
import org.eclipse.ui.internal.intro.impl.util.Log;
import org.eclipse.ui.intro.config.IntroConfigurer;
import org.osgi.framework.Bundle;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

/**
 * The root class for the OOBE model. It loads the configuration into the
 * appropriate classes.
 *
 * Model rules:
 * <ol>
 * <li>if an attribute is not included in the markup, its value will be null in
 * the model.</li>
 * <li>Resources in plugin.xml are not implicitly resolved against $nl$.
 * Resources in pages are implicitly resolved against $nl$
 * <li>the current page id is set silently when loading the model. You do not
 * need the event notification on model load.</li>
 * <li>Children of a given parent (ie: model root, page, or group) *must* have
 * distinctive IDs otherwise resolving includes and extensions may fail.</li>
 * <li>Containers have the concept of loading children and resolving children.
 * At the model root level, resolving children means resolving ALL extensions of
 * model. At the container level, resolving children means resolving includes.
 * </li>
 * <li>Extensions are resolved before includes at the container level to avoid
 * race conditions. eg: if a page includes a shared group and an extension
 * extends this shared group, you want the include to get the extended group and
 * not the original group.</li>
 * <li>Resolving extensions should not resolve includes. No need to load other
 * models when we dont have to. Plus, extensions can only reference anchors, and
 * so no need to resolve includes.</li>
 * <li>Extensions can not target containers *after* they are resolved. For
 * example, an extension can not target a shared group after it has been
 * included in a page. It can target the initial shared group as a path, but not
 * the group in the page as a path. Again this is because extensions extends
 * anchors that already have a path, not a resolved path.</li>
 * <li>Pages and shared groups that are contributed through extensions become
 * children of the atrget configuration, and so any includes they may have will
 * be resolved correctly.</li>
 * <li>An infinite loop can occur if page A includes from page B and page B in
 * turn includes from page A. ie: cyclic includes. For performnace, accept.
 * </li>
 * <li>When resolving includes, if the target is a container, it must be
 * resolved to resolve its includes correctly. Otherwise, included includes will
 * fail due to reparenting.</li>
 * <li>unresolved includes are left as children of the parent container.</li>
 * <li>Unresolved extensions are left as children of the targetted model.</li>
 * <li>For dynamic awarness, the model is nulled and then reloaded. However, we
 * need to preserve the presentation instance since the UI is already loaded.
 * This is done by reloading the model, and directly resetting the presentation
 * to what it was.</li>
 * <li>Model classes should not have DOM classes as instance vars, and if this
 * is a must, null the DOM class instance the minute you are done. This is
 * because you want the VM to garbage collect the DOM model. Keeping a reference
 * to the DOM model from the Intro model will prevent that.</li>
 * </ol>
 * <li>(since 3.0.2) several passes are used to resolve contributions to
 * anchors that themselves where contributed through an extension. Each time a
 * contribution is resolved, the model tries to resolve all unresolved
 * contribution, recursively.
 * </ul>
 */
public class IntroModelRoot extends AbstractIntroContainer {

    /**
     * Model constants that fire property change event when they are changed in
     * the model.
     */
    public static final int CURRENT_PAGE_PROPERTY_ID = 1;

    private static final String ATT_CONTENT = "content"; //$NON-NLS-1$
    private static final String ATT_CONFIGURER = "configurer"; //$NON-NLS-1$
    private static final String VAR_THEME = "theme";  //$NON-NLS-1$
    private static final String VAR_DIRECTION = "direction";  //$NON-NLS-1$

    // False if there is no valid contribution to the
    // org.eclipse.ui.intro.config extension point. Start off with true, and set
    // to false whenever something bad happens.
    private boolean hasValidConfig = true;
    private IntroConfigurer configurer;
    private IntroTheme theme;
    private IntroPartPresentation introPartPresentation;
    private IntroHomePage rootPage;
    private String currentPageId;
    private String startPageId;
    private AbstractIntroPage standbyPage;
    private AbstractIntroPage homePage;
	private String modelStandbyPageId;

    // the config extensions for this model.
    private IConfigurationElement[] configExtensionElements;

    // maintain listener list for model changes.
	public ListenerList<IPropertyListener> propChangeListeners = new ListenerList<>();

    // a list to hold all loaded DOMs until resolving all configExtensions
    // is done.
	private List<ExtensionContent> unresolvedConfigExt = new ArrayList<>();


    private class ExtensionContent {
    	Element element;
        IConfigurationElement configExtElement;
        ExtensionContent(Element element,
                IConfigurationElement configExtElement) {
            this.element = element;
            this.configExtElement = configExtElement;
        }
    }

    /**
     * Model root. Takes a configElement that represents <config>in the
     * plugin.xml markup AND all the extension contributed to this model through
     * the configExtension point.
     */
    public IntroModelRoot(IConfigurationElement configElement,
            IConfigurationElement[] configExtensionElements) {
        // the config element that represents the correct model root.
        super(configElement);
        this.configExtensionElements = configExtensionElements;

    }

    public void loadModel() {
        getChildren();
        determineHomePage();
    }

    /**
     * Loads the full model. The children of a model root are the presentation,
     * followed by all pages, and all shared groups. Then if the model has
     * extension, its the unresolved container extensions, followed by all
     * extension pages and groups. The presentation is loaded from the
     * IConfiguration element representing the config. All else is loaded from
     * xml content file.
     *
     */
    @Override
	protected void loadChildren() {
		children = new Vector<>();
        if (Log.logInfo)
            Log.info("Creating Intro plugin model...."); //$NON-NLS-1$

        // load presentation first and create the model class for it. If there
        // is more than one presentation, load first one, and log rest.
        IConfigurationElement presentationElement = loadPresentation();
        if (presentationElement == null) {
            // no presentations at all, exit.
            setModelState(true, false);
            Log.warning("Could not find presentation element in intro config."); //$NON-NLS-1$
            return;
        }

        loadTheme();
        loadConfigurer();

        introPartPresentation = new IntroPartPresentation(presentationElement);
        children.add(introPartPresentation);
        // set parent.
        introPartPresentation.setParent(this);

        // now load all children of the config. There should only be pages and
        // groups here. And order is not important. These elements are loaded
        // from the content file DOM.
        Document document = loadDOM(getCfgElement());
        if (document == null) {
            // we failed to parse the content file. Intro Parser would have
            // logged the fact. Parser would also have checked to see if the
            // content file has the correct root tag.
            setModelState(true, false);
            return;
        }

        // set base for this model.
        this.base = getBase(getCfgElement());

        // now load content.
        loadPages(document, getBundle());
        loadSharedGroups(document, getBundle());

        // Attributes of root page decide if we have a static or dynamic case.
        setModelState(true, true);

		if (configurer != null) {
			// The configurer may vary its returned results based on the theme
			// properties
			configurer.bind(this);
		}
    }

    /**
     * Sets the presentation to the given presentation. The model always has the
     * presentation as the first child, so use that fact. This method is used
     * for dynamic awarness to enable replacing the new presentation with the
     * existing one after a model refresh.
     *
     * @param presentation
     */
    public void setPresentation(IntroPartPresentation presentation) {
        this.introPartPresentation = presentation;
        presentation.setParent(this);
        children.set(0, presentation);
    }

    /**
     * Resolve contributions into this container's children.
     */
    @Override
	protected void resolveChildren() {
        // now handle config extension.
        resolveConfigExtensions();
        resolved = true;
    }

    private IConfigurationElement loadPresentation() {
        // If there is more than one presentation, load first one, and log
        // rest.
        IConfigurationElement[] presentationElements = getCfgElement()
            .getChildren(IntroPartPresentation.TAG_PRESENTATION);

        IConfigurationElement presentationElement = ModelLoaderUtil
            .validateSingleContribution(presentationElements,
                IntroPartPresentation.ATT_HOME_PAGE_ID);
        return presentationElement;
    }

    private void loadConfigurer() {
    	String cname = getCfgElement().getAttribute(ATT_CONFIGURER);
    	if (cname!=null) {
    		try {
    			Object obj = getCfgElement().createExecutableExtension(ATT_CONFIGURER);
				if (obj instanceof IntroConfigurer) {
    				configurer = (IntroConfigurer)obj;
				}
    		}
    		catch (CoreException e) {
    			Log.error("Error loading intro configurer", e); //$NON-NLS-1$
    		}
    	}
    }

    private void determineHomePage() {
	    	homePage = rootPage;  // Default, may be overridden
	    	if (Platform.getProduct() == null) {
	    		return;
	    	}
	    	String pid = Platform.getProduct().getId();
	    	startPageId = getProcessPreference("INTRO_START_PAGE", pid); //$NON-NLS-1$
	    	String homePagePreference = getProcessPreference("INTRO_HOME_PAGE", pid); //$NON-NLS-1$
	    	if (homePagePreference.length() != 0) {
	    		AbstractIntroPage page = (AbstractIntroPage) findChild(homePagePreference,
	    	            ABSTRACT_PAGE);
	    		if (page != null) {
	    			homePage = page;
	    		    if(startPageId.length() == 0) {
	    			    startPageId = homePagePreference;
	    		    }
	    		}
	    	}
	    	String standbyPagePreference = getProcessPreference("INTRO_STANDBY_PAGE", pid); //$NON-NLS-1$
        modelStandbyPageId = getPresentation().getStandbyPageId();

        if (standbyPagePreference.length() != 0) {
        	standbyPage = (AbstractIntroPage) findChild(standbyPagePreference,
    	            ABSTRACT_PAGE);
        }
        if (standbyPage == null && modelStandbyPageId != null && modelStandbyPageId.length() != 0) {
        	standbyPage = (AbstractIntroPage) findChild(modelStandbyPageId,
    	            ABSTRACT_PAGE);
        }
        if (standbyPage != null) {
            standbyPage.setStandbyPage(true);
        }
    }

    private void loadTheme() {
	    	String pid = Platform.getProduct() != null ? Platform.getProduct().getId() : null;
	    	String themeId = pid != null ? getProcessPreference("INTRO_THEME", pid) : null; //$NON-NLS-1$

	    	IConfigurationElement [] elements = Platform.getExtensionRegistry().getConfigurationElementsFor("org.eclipse.ui.intro.configExtension"); //$NON-NLS-1$
	    	IConfigurationElement themeElement=null;
	    	for (int i=0; i<elements.length; i++) {
	    		if (elements[i].getName().equals("theme")) { //$NON-NLS-1$
	    			String id = elements[i].getAttribute("id"); //$NON-NLS-1$
	    			if (themeId!=null) {
	    				if (id!=null && themeId.equals(id)) {
	    					// use this one
	    					themeElement = elements[i];
	    					break;
	    				}
	    			}
	    			else {
	    				// see if this one is the default
	    				String value = elements[i].getAttribute("default"); //$NON-NLS-1$
	    				if (value!=null && value.equalsIgnoreCase("true")) { //$NON-NLS-1$
	    					themeElement = elements[i];
	    					break;
	    				}
	    			}
	    		}
	    	}
	    	if (themeElement!=null) {
	    		theme = new IntroTheme(themeElement);
	    	}
    }

    /**
     * Loads all pages defined in this config from the xml content file.
     */
    private void loadPages(Document dom, Bundle bundle) {
        String rootPageId = getPresentation().getHomePageId();
        Element[] pages = ModelUtil.getElementsByTagName(dom,
            AbstractIntroPage.TAG_PAGE);
        for (int i = 0; i < pages.length; i++) {
            Element pageElement = pages[i];
            if (pageElement.getAttribute(AbstractIntroIdElement.ATT_ID).equals(
                rootPageId)) {
                // Create the model class for the Root Page.
                rootPage = new IntroHomePage(pageElement, bundle, base);
                rootPage.setParent(this);
                currentPageId = rootPage.getId();
                children.add(rootPage);
            } else {
                // Create the model class for an intro Page.
                IntroPage page = new IntroPage(pageElement, bundle, base);
                page.setParent(this);
                children.add(page);
            }
        }
    }

    /**
     * Loads all shared groups defined in this config, from the DOM.
     */
    private void loadSharedGroups(Document dom, Bundle bundle) {
        Element[] groups = ModelUtil.getElementsByTagName(dom,
            IntroGroup.TAG_GROUP);
        for (int i = 0; i < groups.length; i++) {
            IntroGroup group = new IntroGroup(groups[i], bundle, base);
            group.setParent(this);
            children.add(group);
        }
    }

    /**
     * Handles all the configExtensions to this current model. Resolving
     * configExts means finding target anchor and inserting extension content at
     * target. Also, several passes are used to resolve as many extensions as
     * possible. This allows for resolving nested anchors (ie: anchors to
     * anchors in contributions).
     */
    private void resolveConfigExtensions() {
        for (int i = 0; i < configExtensionElements.length; i++) {
            processConfigExtension(configExtensionElements[i]);
        }

        tryResolvingExtensions();

        // At this stage all pages will be resolved, some contributions may not be

        // now add all unresolved extensions as model children and log fact.
        Iterator keys = unresolvedConfigExt.iterator();
        while (keys.hasNext()) {
            ExtensionContent extension = (ExtensionContent) keys.next();
            Element configExtensionElement = extension.element;
            IConfigurationElement configExtConfigurationElement = extension.configExtElement;
            Bundle bundle = BundleUtil
                .getBundleFromConfigurationElement(configExtConfigurationElement);
            String base = getBase(configExtConfigurationElement);
            children.add(new IntroExtensionContent(configExtensionElement,
                bundle, base, configExtConfigurationElement));

            // INTRO: fix log strings.
            Log
                .warning("Could not resolve the following configExtension: " //$NON-NLS-1$
                        + ModelLoaderUtil.getLogString(bundle,
                            configExtensionElement,
                            IntroExtensionContent.ATT_PATH));
        }
    }

    private void processConfigExtension(IConfigurationElement configExtElement) {
        // This call will extract the parent folder if needed.
        Document dom = loadDOM(configExtElement);
        if (dom == null)
            // we failed to parse the content file. Intro Parser would
            // have logged the fact. Parser would also have checked to
            // see if the content file has the correct root tag.
            return;
        processConfigExtension(dom, configExtElement);
    }


    private void processConfigExtension(Document dom,
            IConfigurationElement configExtElement) {

        // Find the target of this container extension, and add all its
        // children to target. Make sure to pass correct bundle and base to
        // propagate to all children.
        String base = getBase(configExtElement);
        Element[] extensionContentElements = loadExtensionContent(dom,
            configExtElement, base);
        for (int i = 0; i < extensionContentElements.length; i++) {
        	Element extensionContentElement = extensionContentElements[i];
             unresolvedConfigExt.add(new ExtensionContent(extensionContentElement,
                         configExtElement));
        }

        // Now load all pages and shared groups
        // from this config extension. Get the bundle from the extensions since they are
        // defined in other plugins.
        Bundle bundle = BundleUtil
            .getBundleFromConfigurationElement(configExtElement);

        Element[] pages = ModelUtil.getElementsByTagName(dom,
            AbstractIntroPage.TAG_PAGE);
        for (int j = 0; j < pages.length; j++) {
            // Create the model class for an intro Page.
        	if (!UAContentFilter.isFiltered(UAElementFactory.newElement(pages[j]), IntroEvaluationContext.getContext())) {
                IntroPage page = new IntroPage(pages[j], bundle, base);
                page.setParent(this);
                children.add(page);
        	}
        }
    }

    private void tryResolvingExtensions() {
    	int previousSize;
    	do {
    		previousSize = unresolvedConfigExt.size();
			List<ExtensionContent> stillUnresolved = new ArrayList<>();
			for (ExtensionContent content : unresolvedConfigExt) {
                Element extensionContentElement = content.element;
                IConfigurationElement configExtElement = content.configExtElement;
				Bundle bundle = BundleUtil.getBundleFromConfigurationElement(configExtElement);
				String elementBase = getBase(configExtElement);
                processOneExtension(configExtElement, elementBase, bundle, extensionContentElement);
				if (extensionContentElement.hasAttribute("failed")) { //$NON-NLS-1$
					stillUnresolved.add(content);
				}
            }
            unresolvedConfigExt = stillUnresolved;
        } while (unresolvedConfigExt.size() < previousSize);
    }

    /**
     * load the extension content of this configExtension into model classes,
     * and insert them at target. A config extension can have only ONE extension
     * content. This is because if the extension fails, we need to be able to
     * not include the page and group contributions as part of the model. If
     * extension content has XHTML content (ie: content attribute is defined) we
     * load extension DOM into target page dom.
     *
     * note: the extension Element is returned to enable creating a child model
     * element on failure.
     *
     * @param
     * @return
     */
    private Element[] loadExtensionContent(Document dom,
            IConfigurationElement configExtElement, String base) {

        // get the bundle from the extensions since they are defined in
        // other plugins.
		List<Element> elements = new ArrayList<>();
        Element[] extensionContents = ModelUtil.getElementsByTagName(dom,
            IntroExtensionContent.TAG_CONTAINER_EXTENSION);
        Element[] replacementContents =	 ModelUtil.getElementsByTagName(dom,
        			IntroExtensionContent.TAG_CONTAINER_REPLACE);

        addUnfilteredExtensions(elements, extensionContents);
        addUnfilteredExtensions(elements, replacementContents);

        return elements.toArray(new Element[elements.size()]);
    }

	private void addUnfilteredExtensions(List<Element> elements, Element[] extensionContents) {
		for (int i = 0; i < extensionContents.length; i++) {
        	Element extensionContentElement = extensionContents[i];
        	if (!UAContentFilter.isFiltered(UAElementFactory.newElement(extensionContentElement), IntroEvaluationContext.getContext())) {
        	    elements.add(extensionContentElement);
            }
        }
	}

	private void processOneExtension(IConfigurationElement configExtElement, String base, Bundle bundle,
			Element extensionContentElement) {
		// Create the model class for extension content.
		IntroExtensionContent extensionContent = new IntroExtensionContent(
		    extensionContentElement, bundle, base, configExtElement);
		boolean success = false;
		if (extensionContent.isXHTMLContent())
		    success = loadXHTMLExtensionContent(extensionContent);
		else
		    success = load3_0ExtensionContent(extensionContent);

		if (success) {
		    if (extensionContentElement.hasAttribute("failed")) //$NON-NLS-1$
		        extensionContentElement.removeAttribute("failed"); //$NON-NLS-1$
		} else
		    extensionContentElement.setAttribute("failed", "true"); //$NON-NLS-1$ //$NON-NLS-2$
	}


    /**
     * Insert the extension content into the target.
     *
     * @param extensionContent
     * @return
     */
    private boolean loadXHTMLExtensionContent(
            IntroExtensionContent extensionContent) {
        String path = extensionContent.getPath();
        // path must be pageId/anchorID in the case of anchors in XHTML pages.
        String[] pathSegments = path.split("/"); //$NON-NLS-1$
        if (pathSegments.length != 2)
            // path does not have correct format.
            return false;
        AbstractIntroPage targetPage = (AbstractIntroPage) findChild(
            pathSegments[0], ABSTRACT_PAGE);
        if (targetPage == null)
            // target could not be found. Signal failure.
            return false;

        // Insert all children of this extension before the target element. Anchors need
        // to stay in DOM, even after all extensions have been resolved, to enable other
        // plugins to contribute. Find the target node.
        Document pageDom = targetPage.getDocument();
        Element targetElement = targetPage.findDomChild(pathSegments[1], "*"); //$NON-NLS-1$
        if (targetElement == null)
            return false;

        // get extension content
        Element[] elements = extensionContent.getElements();
        // insert all children before anchor in page body.
        for (int i = 0; i < elements.length; i++) {
            Node targetNode = pageDom.importNode(elements[i], true);
            // update the src attribute of this node, if defined by w3
            // specs.

            ModelUtil.updateResourceAttributes((Element) targetNode,
                extensionContent);
            targetElement.getParentNode().insertBefore(targetNode, targetElement);
        }

        if (extensionContent.getExtensionType() == IntroExtensionContent.TYPE_REPLACEMENT) {
            targetElement.getParentNode().removeChild(targetElement);
        }

        // now handle style inheritance.
        // Update the parent page styles. skip style if it is null;
        String[] styles = extensionContent.getStyles();
        if (styles != null) {
            for (int i = 0; i < styles.length; i++)
                ModelUtil.insertStyle(pageDom, styles[i]);
        }

        return true;

    }



    /**
     * Insert the extension content (3.0 format) into the target.
     *
     * @param extensionContent
     * @return
     */
    private boolean load3_0ExtensionContent(IntroExtensionContent extensionContent) {
        String path = extensionContent.getPath();
        int type = extensionContent.getExtensionType();
        AbstractIntroElement target = findTarget(this, path, extensionContent.getId());
        if (target != null && target.isOfType(AbstractIntroElement.ANCHOR) == (type == IntroExtensionContent.TYPE_CONTRIBUTION)) {
        	// insert all children of this extension before the target element/anchor.
	        insertExtensionChildren(target, extensionContent, extensionContent.getBundle(), extensionContent.getBase());
	        // anchors need to stay around to receive other contributions
	        if (type == IntroExtensionContent.TYPE_REPLACEMENT) {
	        	AbstractIntroContainer parent = (AbstractIntroContainer)target.getParent();
	        	parent.removeChild(target);
	        }
	        handleExtensionStyleInheritence(target, extensionContent);
	        return true;
        }
        // appropriate target could not be found. Signal failure.
        return false;
    }

    private void insertExtensionChildren(AbstractIntroElement target,
            IntroExtensionContent extensionContent, Bundle bundle, String base) {
        AbstractIntroContainer parent = (AbstractIntroContainer)target.getParent();
        // insert the elements of the extension before the target
        String mixinStyle = getMixinStyle(extensionContent);
        Element [] children = extensionContent.getChildren();
        parent.insertElementsBefore(children, bundle, base, target, mixinStyle);
    }

    private String getMixinStyle(IntroExtensionContent extensionContent) {
    	String path = extensionContent.getPath();
    	if (!path.endsWith("/@")) //$NON-NLS-1$
    		return null;
    	String pageId = path.substring(0, path.length()-2);
    	IntroModelRoot modelRoot = getModelRoot();
    	if (modelRoot==null)
    		return null;
    	IntroConfigurer configurer = modelRoot.getConfigurer();
    	if (configurer==null)
    		return null;
    	String extensionId = extensionContent.getId();
    	// if this is a replace, take the mixin style as what is being replaced
    	if (extensionContent.getExtensionType() == IntroExtensionContent.TYPE_REPLACEMENT) {
    		IPath ipath = new Path(extensionContent.getPath());
    		String s2 = ipath.segment(1);
    		if (s2 != null && s2.startsWith("@") && s2.length() > 1) { //$NON-NLS-1$
    			extensionId = s2.substring(1);
    		}
    	}
   		return configurer.getMixinStyle(pageId, extensionId);
    }


    /**
     * Updates the inherited styles based on the style attribtes defined in the
     * configExtension. If we are extending a shared group do nothing. For
     * inherited alt-styles, we have to cache the bundle from which we inherited
     * the styles to be able to access resources in that plugin.
     *
     * @param include
     * @param target
     */
    private void handleExtensionStyleInheritence(AbstractIntroElement target,
            IntroExtensionContent extension) {

        AbstractIntroContainer targetContainer = (AbstractIntroContainer)target.getParent();
        if (targetContainer.getType() == AbstractIntroElement.GROUP
                && targetContainer.getParent().getType() == AbstractIntroElement.MODEL_ROOT)
            // if we are extending a shared group, defined under a config, we
            // can not include styles.
            return;

        // Update the parent page styles. skip style if it is null;
        String[] styles = extension.getStyles();
        if (styles != null)
            targetContainer.getParentPage().addStyles(styles);

        // for alt-style cache bundle for loading resources.
		Map<String, Bundle> altStyles = extension.getAltStyles();
        if (altStyles != null)
            targetContainer.getParentPage().addAltStyles(altStyles);
    }

    /**
     * Sets the model state based on all the model classes.
     */
    private void setModelState(boolean loaded, boolean hasValidConfig) {
        this.loaded = loaded;
        this.hasValidConfig = hasValidConfig;
    }

    /**
     * Returns true if there is a valid contribution to
     * org.eclipse.ui.intro.config extension point, with a valid Presentation,
     * and pages.
     *
     * @return Returns the hasValidConfig.
     */
    public boolean hasValidConfig() {
        return hasValidConfig;
    }

    /**
     * @return Returns the introPartPresentation.
     */
    public IntroPartPresentation getPresentation() {
        return introPartPresentation;
    }

    public IntroConfigurer getConfigurer() {
    	return configurer;
    }

    /**
     * @return Returns the home Page.
     */
    public AbstractIntroPage getHomePage() {
        return homePage;
    }

    /**
     * @return Returns the root Page.
     */
    public IntroHomePage getRootPage() {
        return rootPage;
    }

    /**
     * @return Returns the standby Page. May return null if standby page is not
     *         defined.
     */
    public AbstractIntroPage getStandbyPage() {
        return standbyPage;
    }

    /**
     * @return all pages *excluding* the Home Page. If all pages are needed,
     *         call <code>(AbstractIntroPage[])
     *         getChildrenOfType(IntroElement.ABSTRACT_PAGE);</code>
     */
    public IntroPage[] getPages() {
        return (IntroPage[]) getChildrenOfType(AbstractIntroElement.PAGE);
    }

    /**
     * @return Returns the isdynamicIntro.
     */
    public boolean isDynamic() {
        if ("swt".equals(getPresentation().getImplementationKind())) { //$NON-NLS-1$
        	return rootPage != null && rootPage.isDynamic();
        }
        return true;
    }

    /**
     * @return Returns the currentPageId.
     */
    public String getCurrentPageId() {
        return currentPageId;
    }


    /**
     * Sets the current page. If the model does not have a page with the passed
     * id, the message is logged, and the model retains its old current page.
     *
     * @param currentPageId
     *            The currentPageId to set. *
     * @param fireEvent
     *            flag to indicate if event notification is needed.
     * @return true if the model has a page with the passed id, false otherwise.
     *         If the method fails, the current page remains the same as the
     *         last state.
     */
    public boolean setCurrentPageId(String pageId, boolean fireEvent) {
        if (pageId.equals(currentPageId))
            // setting to the same page does nothing. Return true because we did
            // not actually fail. just a no op.
            return true;

        AbstractIntroPage page = (AbstractIntroPage) findChild(pageId,
            ABSTRACT_PAGE);
        if (page == null) {
            // not a page. Test for root page.
            if (!pageId.equals(rootPage.getId())) {
                // not a page nor the home page.
                Log
                    .warning("Could not set current page to Intro page with id: " + pageId); //$NON-NLS-1$
                return false;
            }
        }

        currentPageId = pageId;
        if (fireEvent)
            firePropertyChange(CURRENT_PAGE_PROPERTY_ID);
        return true;
    }

    public boolean setCurrentPageId(String pageId) {
        return setCurrentPageId(pageId, true);
    }

    public void addPropertyListener(IPropertyListener l) {
        propChangeListeners.add(l);
    }

    /**
     * Fires a property changed event. Made public because it can be used to
     * trigger a UI refresh.
     *
     * @param propertyId
     *            the id of the property that changed
     */
    public void firePropertyChange(final int propertyId) {
        Object[] array = propChangeListeners.getListeners();
        for (int i = 0; i < array.length; i++) {
            final IPropertyListener l = (IPropertyListener) array[i];
            SafeRunner.run(new SafeRunnable() {

                @Override
				public void run() {
                    l.propertyChanged(this, propertyId);
                }

                @Override
				public void handleException(Throwable e) {
                    super.handleException(e);
                    // If an unexpected exception happens, remove it
                    // to make sure the workbench keeps running.
                    propChangeListeners.remove(l);
                }
            });
        }
    }

    public void removePropertyListener(IPropertyListener l) {
        propChangeListeners.remove(l);
    }

    /**
     * @return Returns the currentPage. return null if page is not found, or if
     *         we are not in a dynamic intro mode.
     */
    public AbstractIntroPage getCurrentPage() {
        if (!isDynamic())
            return null;
        AbstractIntroPage page = (AbstractIntroPage) findChild(currentPageId,
            ABSTRACT_PAGE);
        if (page != null)
            return page;
        // not a page. Test for root page.
        if (currentPageId.equals(rootPage.getId()))
            return rootPage;
        // return null if page is not found.
        return null;
    }

    @Override
	public int getType() {
        return AbstractIntroElement.MODEL_ROOT;
    }


    /**
     * Assumes that the passed config element has a "content" attribute. Reads
     * it and loads a DOM based on that attribute value. It does not explicitly
     * resolve the resource because this method only loads the introContent and
     * the configExt content files. ie: in plugin.xml. <br>
     * This method also sets the base attribute on the root element in the DOM
     * to enable resolving all resources relative to this DOM.
     *
     * @return
     */
    protected Document loadDOM(IConfigurationElement cfgElement) {
        String content = cfgElement.getAttribute(ATT_CONTENT);

        // To support jarring, extract parent folder of where the intro content
        // file is. It is expected that all intro content is in that one parent
        // folder. This works for both content files and configExtension content
        // files.
        Bundle domBundle = BundleUtil
            .getBundleFromConfigurationElement(cfgElement);
        ModelUtil.ensureFileURLsExist(domBundle, content);

        // Resolve.
        content = BundleUtil.getResourceLocation(content, cfgElement);
        Document document = new IntroContentParser(content).getDocument();

        return document;
    }


    private String getBase(IConfigurationElement configElement) {
        String content = configElement.getAttribute(ATT_CONTENT);
        return ModelUtil.getParentFolderToString(content);
    }

    public String resolveVariables(String text) {
    	if (text==null) return null;
    	if (text.indexOf('$')== -1)
    		return text;
    	// resolve
    	boolean inVariable=false;
    	StringBuilder buf = new StringBuilder();
    	int vindex=0;
    	for (int i=0; i<text.length(); i++) {
    		char c = text.charAt(i);
    		if (c=='$') {
    			if (!inVariable) {
    				inVariable=true;
    				vindex=i+1;
    				continue;
    			}
 				inVariable=false;
   				String variable=text.substring(vindex, i);
   				String value = getVariableValue(variable);
   				if (value==null)
   					value = "$"+variable+"$"; //$NON-NLS-1$ //$NON-NLS-2$
   				buf.append(value);
   				continue;
    		}
    		else if (!inVariable)
    			buf.append(c);
    	}
    	return buf.toString();
    }

    private String getVariableValue(String variable) {
     	if (variable.equals(VAR_THEME)) {
    		if (theme!=null)
    			return theme.getPath();
    	}
     	if (variable.equals(FontSelection.VAR_FONT_STYLE)) {
     		return FontSelection.getFontStyle();
    	}
     	if (variable.equals(VAR_DIRECTION)) {
    		if (ProductPreferences.isRTL()) {
    			return "rtl"; //$NON-NLS-1$
    		} else {
    			return "ltr"; //$NON-NLS-1$
    		}
    	}

    	if (configurer!=null)
    		return configurer.getVariable(variable);
    	return null;
    }

    public String resolvePath(String extensionId, String path) {
    	if (configurer==null) return null;
    	return configurer.resolvePath(extensionId, path);
    }


	public IntroTheme getTheme() {
		return theme;
	}

	public String getStartPageId() {
		return startPageId;
	}

	private String getProcessPreference(String key, String pid) {
		String result = Platform.getPreferencesService().getString
		      (IntroPlugin.PLUGIN_ID,  pid + '_' + key, "", null); //$NON-NLS-1$
    	if (result.length() == 0) {
    		result = Platform.getPreferencesService().getString
		        (IntroPlugin.PLUGIN_ID,  key, "", null); //$NON-NLS-1$
    	}
    	return result;
	}
}