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

import java.util.Properties;

import org.eclipse.core.runtime.IRegistryChangeEvent;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.swt.SWT;
import org.eclipse.swt.browser.Browser;
import org.eclipse.swt.browser.ProgressEvent;
import org.eclipse.swt.browser.ProgressListener;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.ui.IActionBars;
import org.eclipse.ui.IMemento;
import org.eclipse.ui.IPropertyListener;
import org.eclipse.ui.actions.ActionFactory;
import org.eclipse.ui.internal.intro.impl.IIntroConstants;
import org.eclipse.ui.internal.intro.impl.IntroPlugin;
import org.eclipse.ui.internal.intro.impl.Messages;
import org.eclipse.ui.internal.intro.impl.html.HTMLElement;
import org.eclipse.ui.internal.intro.impl.html.IIntroHTMLConstants;
import org.eclipse.ui.internal.intro.impl.html.IntroHTMLGenerator;
import org.eclipse.ui.internal.intro.impl.model.AbstractIntroPage;
import org.eclipse.ui.internal.intro.impl.model.AbstractIntroPartImplementation;
import org.eclipse.ui.internal.intro.impl.model.History;
import org.eclipse.ui.internal.intro.impl.model.IntroContentProvider;
import org.eclipse.ui.internal.intro.impl.model.IntroHomePage;
import org.eclipse.ui.internal.intro.impl.model.IntroModelRoot;
import org.eclipse.ui.internal.intro.impl.model.loader.ContentProviderManager;
import org.eclipse.ui.internal.intro.impl.model.loader.IntroContentParser;
import org.eclipse.ui.internal.intro.impl.model.util.ModelUtil;
import org.eclipse.ui.internal.intro.impl.util.Log;
import org.eclipse.ui.internal.intro.impl.util.Util;
import org.eclipse.ui.intro.config.IIntroContentProvider;
import org.eclipse.ui.intro.config.IIntroContentProviderSite;
import org.eclipse.ui.intro.config.IIntroXHTMLContentProvider;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class BrowserIntroPartImplementation extends
        AbstractIntroPartImplementation implements IPropertyListener,
        IIntroContentProviderSite {


    // the browser widget that will display the intro content
    protected Browser browser = null;

    // the HTML generator used to generate dynamic content
    private IntroHTMLGenerator htmlGenerator = null;

    // cache model instance for reuse.
    // private IntroModelRoot model = getModel();

    private BrowserIntroPartLocationListener urlListener = new BrowserIntroPartLocationListener(
        this);


    protected void updateNavigationActionsState() {
        if (getModel().isDynamic()) {
            forwardAction.setEnabled(history.canNavigateForward());
            backAction.setEnabled(history.canNavigateBackward());
            return;
        }

        // in static html intro, use browser history.
        forwardAction.setEnabled(browser.isForwardEnabled());
        backAction.setEnabled(browser.isBackEnabled());
    }


    /**
     * create the browser and set it's contents
     */
    public void createPartControl(Composite parent) {
        long start = 0;
        if (Log.logPerformance)
            start = System.currentTimeMillis();

        browser = new Browser(parent, SWT.NONE);

        // add a location listener on the browser so we can intercept
        // LocationEvents. Responsible for intercepting URLs and updating UI
        // with history.
        browser.addLocationListener(urlListener);

        // add a location listener that will clear a flag at the end of any
        // navigation to a page. This is used in conjunction with the location
        // listener to filter out redundant navigations due to frames.
        browser.addProgressListener(new ProgressListener() {

            public void changed(ProgressEvent event) {
                // no-op
            }

            public void completed(ProgressEvent event) {
                // ((Browser) event.getSource()).redraw();
                flagEndOfNavigation();
                if (!getModel().isDynamic())
                    updateNavigationActionsState();
            }
        });

        // Enable IE pop-up menu only in debug mode.
        browser.addListener(SWT.MenuDetect, new Listener() {

            public void handleEvent(Event event) {
                if (IntroPlugin.getDefault().isDebugging())
                    event.doit = true;
                else
                    event.doit = false;
            }
        });

        // if we are logging performance, log actual UI creation time for
        // browser.
        if (Log.logPerformance)
            Util.logPerformanceTime("creating a new Browser() took:", start);

        addToolBarActions();

        if (!getModel().hasValidConfig()) {
            browser.setText(Messages.Browser_invalidConfig);
            return;
        }

        // root page is what decides if the model is dynamic or not.
        if (getModel().isDynamic())
            handleDynamicIntro();
        else
            handleStaticIntro();
    }



    private void handleDynamicIntro() {
        IntroHomePage homePage = getModel().getHomePage();
        // check cache state.
        String cachedPage = getCachedCurrentPage();
        if (cachedPage != null) {
            // we have a cached state. handle appropriately
            if (History.isURL(cachedPage)) {
                // set the URL the browser should display
                boolean success = browser.setUrl(cachedPage);
                if (!success) {
                    Log.error("Unable to set the following ULR in browser: " //$NON-NLS-1$
                            + cachedPage, null);
                    return;
                }
                history.updateHistory(cachedPage);
            } else {
                // Generate HTML for the cached page, and set it on the browser.
                getModel().setCurrentPageId(cachedPage, false);
                // generateDynamicContentForPage(getModel().getCurrentPage());
                history.updateHistory(getModel().getCurrentPage());
            }

        } else {
            // No cached page. Generate HTML for the home page, and set it
            // on the browser.
            // generateDynamicContentForPage(homePage);
            history.updateHistory(homePage);
        }
        // INTRO: all setText calls above are commented out because calling
        // setText twice causes problems. revisit when swt bug is fixed.

        // Add this presentation as a listener to model
        // only in dynamic case, for now.
        getModel().addPropertyListener(this);
    }


    /**
     * Generate dynamic HTML for the provided page, and set it in the browser
     * widget. A cache is used for performance and for having a correct dynamic
     * content life cycle. This method also updates the navigation history.
     * 
     * @param page
     *            the page to generate HTML for
     */
    private boolean generateDynamicContentForPage(AbstractIntroPage page) {
        String content = null;

        if (page.isXHTMLPage())
            content = generateXHTMLPage(page, this);
        else {
            HTMLElement html = getHTMLGenerator().generateHTMLforPage(page,
                this);
            if (html != null)
                content = html.toString();
        }


        if (content == null) {
            // there was an error generating the html. log an error
            Log.error("Error generating HTML content for page", null); //$NON-NLS-1$
            return false;
        }

        // set the browser's HTML.
        boolean success = false;
        if (browser != null) {
            long start = 0;
            if (Log.logPerformance)
                start = System.currentTimeMillis();
            success = browser.setText(content);
            if (Log.logPerformance)
                Util
                    .logPerformanceTime("setText() on the browser took:", start);

            if (!success)
                Log.error("Unable to set HTML on the browser", null); //$NON-NLS-1$
        }


        // print the HTML if we are in debug mode and have tracing turned on
        if (IntroPlugin.getDefault().isDebugging()) {
            String printHtml = Platform
                .getDebugOption("org.eclipse.ui.intro/trace/printHTML"); //$NON-NLS-1$
            if (printHtml != null && printHtml.equalsIgnoreCase("true")) { //$NON-NLS-1$
                System.out.println(content);
            }
        }
        return success;
    }

    /**
     * Generate an XHTML page as a string.
     * <ul>
     * <li> Create any dynamic content providers, if there are any. We do this
     * by replacing each content provider with a div with the same id. The div
     * is the parent of the dynamic content.</li>
     * <li>Use xslt to flatten the DOM for the page, and create a string.</li>
     * <li>Re-inject the contentProviders into the DOM to recreate the original
     * state of the DOM. DOM could not have been cloned since cloning a DOM
     * removes the docType.</li>
     * </ul>
     * Note: Resolving dynamic content is done at the UI level, consistant with
     * SWT presentation.
     */
    private String generateXHTMLPage(AbstractIntroPage page,
            IIntroContentProviderSite site) {
        // get/cache all content provider elements in DOM.
        Document dom = page.getResolvedDocument();
        NodeList nodes = dom.getElementsByTagNameNS("*", //$NON-NLS-1$
            IntroContentProvider.TAG_CONTENT_PROVIDER);
        // get the array version of the nodelist to work around DOM api design.
        Node[] contentProviderElements = ModelUtil.getArray(nodes);

        // this modifies the DOM.
        resolveDynamicContent(page, site);
        String content = IntroContentParser.convertToString(dom);

        // this restores the DOM to its original state.
        reinjectDynamicContent(dom, contentProviderElements);
        return content;
    }

    /**
     * Resolve the dynamic content in the page. Resolving dynamic content will
     * alter the original page DOM. Dynamic content tags are removed once
     * resolved to have clean xhtml.
     */
    private Document resolveDynamicContent(AbstractIntroPage page,
            IIntroContentProviderSite site) {
        Document dom = page.getResolvedDocument();

        // get all content provider elements in DOM.
        NodeList contentProviders = dom.getElementsByTagNameNS("*", //$NON-NLS-1$
            IntroContentProvider.TAG_CONTENT_PROVIDER);

        // get the array version of the nodelist to work around DOM api design.
        Node[] nodes = ModelUtil.getArray(contentProviders);
        for (int i = 0; i < nodes.length; i++) {
            Element contentProviderElement = (Element) nodes[i];
            IntroContentProvider provider = new IntroContentProvider(
                contentProviderElement, page.getBundle());
            provider.setParent(page);
            // If we've already loaded the content provider for this element,
            // retrieve it, otherwise load the class.
            IIntroXHTMLContentProvider providerClass = (IIntroXHTMLContentProvider) ContentProviderManager
                .getInst().getContentProvider(provider);
            if (providerClass == null)
                // content provider never created before, create it.
                providerClass = (IIntroXHTMLContentProvider) ContentProviderManager
                    .getInst().createContentProvider(provider, site);

            if (providerClass != null) {
                // create a div with the same id as the contentProvider, pass it
                // as the parent to create the specialized content, and then
                // replace the contentProvider element with this div.
                Properties att = new Properties();
                att.setProperty(IIntroHTMLConstants.ATTRIBUTE_ID, provider
                    .getId());
                Element contentDiv = ModelUtil.createElement(dom,
                    IIntroHTMLConstants.ELEMENT_DIV, att);
                providerClass.createContent(provider.getId(), contentDiv);

                contentProviderElement.getParentNode().replaceChild(contentDiv,
                    contentProviderElement);
            } else {
                // we couldn't load the content provider, so add any alternate
                // text content if there is any.
                // INTRO: do it. 3.0 intro content style uses text element as
                // alt text. We can load XHTML content here.
            }
        }
        return dom;
    }


    private void reinjectDynamicContent(Document dom,
            Node[] contentProviderElements) {
        for (int i = 0; i < contentProviderElements.length; i++) {
            // for each cached contentProvider, find replacement div in DOM and
            // re-subsitute.
            Element contentProviderElement = (Element) contentProviderElements[i];
            Element contentProviderDiv = ModelUtil.getElementById(dom,
                contentProviderElement
                    .getAttribute(IIntroHTMLConstants.ATTRIBUTE_ID),
                IIntroHTMLConstants.ELEMENT_DIV);
            contentProviderDiv.getParentNode().replaceChild(
                contentProviderElement, contentProviderDiv);
        }
    }



    /**
     * Return the cached IntroHTMLGenerator
     * 
     * @return
     */
    private IntroHTMLGenerator getHTMLGenerator() {
        if (htmlGenerator == null)
            htmlGenerator = new IntroHTMLGenerator();

        return htmlGenerator;
    }

    protected void addToolBarActions() {
        // Handle menus:
        IActionBars actionBars = getIntroPart().getIntroSite().getActionBars();
        IToolBarManager toolBarManager = actionBars.getToolBarManager();
        actionBars.setGlobalActionHandler(ActionFactory.FORWARD.getId(),
            forwardAction);
        actionBars.setGlobalActionHandler(ActionFactory.BACK.getId(),
            backAction);
        toolBarManager.add(homeAction);
        toolBarManager.add(backAction);
        toolBarManager.add(forwardAction);
        toolBarManager.update(true);
        actionBars.updateActionBars();
        updateNavigationActionsState();
    }

    public void dynamicStandbyStateChanged(boolean standby,
            boolean isStandbyPartNeeded) {

        if (isStandbyPartNeeded)
            // we have a standby part, nothing more to do in presentation.
            return;

        // presentation is shown here. toggle standby page. No need to update
        // history here.
        IntroHomePage homePage = getModel().getHomePage();
        IntroHomePage standbyPage = getModel().getStandbyPage();
        if (standbyPage == null)
            standbyPage = homePage;

        if (standby) {
            generateDynamicContentForPage(standbyPage);
        } else {
            // REVISIT: If cached page is the standby page and we are not
            // initially in standby mode, it means standby was forced on
            // intro view on close. react.
            if (getModel().getCurrentPage().equals(standbyPage.getId()))
                getModel().setCurrentPageId(getModel().getHomePage().getId());
            generateDynamicContentForPage(getModel().getCurrentPage());
        }
    }



    /**
     * Handle model property changes. Property listeners are only added in the
     * dynamic case.
     * 
     * @see org.eclipse.ui.IPropertyListener#propertyChanged(java.lang.Object,
     *      int)
     */
    public void propertyChanged(Object source, int propId) {
        if (propId == IntroModelRoot.CURRENT_PAGE_PROPERTY_ID) {
            String pageId = getModel().getCurrentPageId();
            if (pageId == null || pageId.equals("")) //$NON-NLS-1$
                // page ID was not set properly. exit.
                return;
            // update the presentation's content based on the model changes
            updateContent();
        }
    }

    public void setFocus() {
        browser.setFocus();
    }

    public void dispose() {
        browser.dispose();
    }

    /**
     * Regenerate the dynamic content for the current page
     */
    protected void updateContent() {
        generateDynamicContentForPage(getModel().getCurrentPage());
    }

    /**
     * 
     * @see org.eclipse.ui.internal.intro.impl.model.AbstractIntroPartImplementation#reflow()
     */
    public void reflow(IIntroContentProvider provider, boolean incremental) {
        updateContent();
    }

    /**
     * Override parent behavior to handle the case when we have a static page.
     * This can happen in both the static intro case, or in the dynamic when the
     * last visited page is the dynamic browser is an http: page, and not an
     * intro page.
     */
    protected void saveCurrentPage(IMemento memento) {
        if (memento == null)
            return;
        // Handle the case where we are on a static page.
        // browser.getURL() returns the empty string if there is no current URL
        // and returns "about:blank" if we are on a dynamic page
        if (browser != null && browser.getUrl() != null
                && browser.getUrl().length() > 0
                && !(browser.getUrl().equals("about:blank"))) { //$NON-NLS-1$
            String currentURL = browser.getUrl();
            if (currentURL != null) {
                memento.putString(IIntroConstants.MEMENTO_CURRENT_PAGE_ATT,
                    currentURL);
            }
        } else {
            super.saveCurrentPage(memento);
        }
    }


    /*
     * (non-Javadoc)
     * 
     * @see org.eclipse.ui.internal.intro.impl.model.AbstractIntroPartImplementation#navigateBackward()
     */
    public boolean navigateBackward() {
        boolean success = false;
        if (getModel().isDynamic()) {
            // dynamic case. Uses navigation history.
            if (history.canNavigateBackward()) {
                history.navigateHistoryBackward();
                if (history.currentLocationIsUrl()) {
                    success = browser.setUrl(history.getCurrentLocationAsUrl());
                } else {
                    // we need to regen HTML. We can not use setting current
                    // page to trigger regen for one case: navigating back from
                    // a url will not trigger regen since current page would be
                    // the same.
                    AbstractIntroPage page = history.getCurrentLocationAsPage();
                    if (page.isIFramePage())
                        // if page is a cloned IFrame page, guard against Iframe
                        // navigations. The returned page has the correct Iframe
                        // set.
                        flagStartOfNavigation();

                    success = generateDynamicContentForPage(page);
                    getModel().setCurrentPageId(page.getId(), false);
                }
            } else
                success = false;
            // update history only in dynamic case.
            updateNavigationActionsState();
        } else
            // static HTML case. use browser real Back.
            success = browser.back();

        return success;
    }


    /*
     * (non-Javadoc)
     * 
     * @see org.eclipse.ui.internal.intro.impl.model.AbstractIntroPartImplementation#navigateForward()
     */
    public boolean navigateForward() {
        boolean success = false;
        if (getModel().isDynamic()) {
            // dynamic case. Uses navigation history.
            if (history.canNavigateForward()) {
                history.navigateHistoryForward();
                if (history.currentLocationIsUrl()) {
                    success = browser.setUrl(history.getCurrentLocationAsUrl());
                } else {
                    AbstractIntroPage page = history.getCurrentLocationAsPage();
                    if (page.isIFramePage())
                        flagStartOfNavigation();
                    success = generateDynamicContentForPage(page);
                    getModel().setCurrentPageId(page.getId(), false);
                }
            } else
                success = false;
            // update history only in dynamic case.
            updateNavigationActionsState();
        } else
            // static HTML case. use browser real Forward.
            success = browser.forward();

        return success;
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.eclipse.ui.internal.intro.impl.model.AbstractIntroPartImplementation#navigateHome()
     */
    public boolean navigateHome() {
        // Home is URL of root page in static case, and root page in
        // dynamic.
        IntroHomePage rootPage = getModel().getHomePage();
        boolean success = false;
        if (getModel().isDynamic()) {
            success = getModel().setCurrentPageId(rootPage.getId());
            updateHistory(rootPage);
        } else {
            String location = rootPage.getUrl();
            success = browser.setUrl(location);
            updateHistory(location);
        }

        return success;
    }



    /*
     * (non-Javadoc)
     * 
     * @see org.eclipse.ui.internal.intro.impl.model.AbstractIntroPartImplementation#handleRegistryChanged(org.eclipse.core.runtime.IRegistryChangeEvent)
     */
    protected void handleRegistryChanged(IRegistryChangeEvent event) {
        if (getModel().isDynamic()) {
            // null generator first.
            htmlGenerator = null;
            // Add this presentation as a listener to model only in dynamic
            // case.
            getModel().addPropertyListener(this);
            getModel().firePropertyChange(
                IntroModelRoot.CURRENT_PAGE_PROPERTY_ID);
        }
    }


    protected void doStandbyStateChanged(boolean standby,
            boolean isStandbyPartNeeded) {
        // if we have a standby part, regardless if standby state, disable
        // actions. Same behavior for static html.
        if (isStandbyPartNeeded | standby) {
            homeAction.setEnabled(false);
            forwardAction.setEnabled(false);
            backAction.setEnabled(false);
        } else {
            homeAction.setEnabled(true);
            updateNavigationActionsState();
        }

        if (getModel().isDynamic())
            dynamicStandbyStateChanged(standby, isStandbyPartNeeded);
        else
            staticStandbyStateChanged(standby);
    }



    // ***************** Static Intro *****************
    private void handleStaticIntro() {
        // We have a static case. Set the url on the browser to be the url
        // defined in the root page. But first check memento if we can
        // restore last visited page.
        String url = getCachedCurrentPage();
        if (!History.isURL(url))
            // no cached state, or invalid state.
            url = getModel().getHomePage().getUrl();

        if (url == null) {
            // We have no content to display. log an error
            Log.error("Url is null; no content to display in browser", null); //$NON-NLS-1$
            return;
        }
        // set the URL the browser should display
        boolean success = browser.setUrl(url);
        if (!success) {
            Log.error("Unable to set the following ULR in browser: " + url, //$NON-NLS-1$
                null);
            return;
        }
    }

    public void staticStandbyStateChanged(boolean standby) {
        IntroHomePage homePage = getModel().getHomePage();
        IntroHomePage standbyPage = getModel().getStandbyPage();
        if (standbyPage == null)
            standbyPage = homePage;

        if (standby)
            browser.setUrl(standbyPage.getUrl());
        else
            browser.setUrl(homePage.getUrl());
    }


    public Browser getBrowser() {
        return browser;
    }

    public void flagStartOfNavigation() {
        if (browser.getData("frameNavigation") == null) //$NON-NLS-1$
            browser.setData("frameNavigation", "true"); //$NON-NLS-1$ //$NON-NLS-2$
    }

    public void flagEndOfNavigation() {
        browser.setData("frameNavigation", null); //$NON-NLS-1$
    }




}