/*******************************************************************************
 * 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.url;

import java.util.Enumeration;
import java.util.Properties;

import org.eclipse.core.commands.ParameterizedCommand;
import org.eclipse.core.commands.common.CommandException;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.util.Geometry;
import org.eclipse.swt.browser.Browser;
import org.eclipse.swt.custom.BusyIndicator;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.IActionDelegate;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.IWorkbenchWindowActionDelegate;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.commands.ICommandService;
import org.eclipse.ui.handlers.IHandlerService;
import org.eclipse.ui.internal.AnimationEngine;
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.model.AbstractIntroElement;
import org.eclipse.ui.internal.intro.impl.model.AbstractIntroPage;
import org.eclipse.ui.internal.intro.impl.model.IntroLaunchBarElement;
import org.eclipse.ui.internal.intro.impl.model.IntroModelRoot;
import org.eclipse.ui.internal.intro.impl.model.IntroPartPresentation;
import org.eclipse.ui.internal.intro.impl.model.IntroURLAction;
import org.eclipse.ui.internal.intro.impl.model.loader.ExtensionPointManager;
import org.eclipse.ui.internal.intro.impl.model.loader.ModelLoaderUtil;
import org.eclipse.ui.internal.intro.impl.model.util.ModelUtil;
import org.eclipse.ui.internal.intro.impl.parts.StandbyPart;
import org.eclipse.ui.internal.intro.impl.presentations.BrowserIntroPartImplementation;
import org.eclipse.ui.internal.intro.impl.presentations.IntroLaunchBar;
import org.eclipse.ui.internal.intro.impl.util.DialogUtil;
import org.eclipse.ui.internal.intro.impl.util.Log;
import org.eclipse.ui.internal.intro.impl.util.StringUtil;
import org.eclipse.ui.internal.intro.impl.util.Util;
import org.eclipse.ui.intro.IIntroPart;
import org.eclipse.ui.intro.IIntroSite;
import org.eclipse.ui.intro.config.CustomizableIntroPart;
import org.eclipse.ui.intro.config.IIntroAction;
import org.eclipse.ui.intro.config.IIntroURL;
import org.eclipse.ui.intro.config.IntroURLFactory;

/**
 * An intro url. An intro URL is a valid http url, with org.eclipse.ui.intro as
 * a host. This class holds all logic to execute Intro URL commands, ie: an
 * Intro URL knows how to execute itself.
 */
public class IntroURL implements IIntroURL {


    /**
     * Intro URL constants.
     */
    public static final String INTRO_PROTOCOL = "http"; //$NON-NLS-1$
    public static final String INTRO_HOST_ID = "org.eclipse.ui.intro"; //$NON-NLS-1$

    /**
     * Constants that represent Intro URL actions.
     */
    public static final String SET_STANDBY_MODE = "setStandbyMode"; //$NON-NLS-1$
    public static final String SHOW_STANDBY = "showStandby"; //$NON-NLS-1$
    public static final String CLOSE = "close"; //$NON-NLS-1$
    public static final String SHOW_HELP_TOPIC = "showHelpTopic"; //$NON-NLS-1$
    public static final String SHOW_HELP = "showHelp"; //$NON-NLS-1$
    public static final String OPEN_BROWSER = "openBrowser"; //$NON-NLS-1$
    public static final String OPEN_URL = "openURL"; //$NON-NLS-1$
    public static final String RUN_ACTION = "runAction"; //$NON-NLS-1$
    public static final String SHOW_PAGE = "showPage"; //$NON-NLS-1$
    public static final String SHOW_MESSAGE = "showMessage"; //$NON-NLS-1$
    public static final String NAVIGATE = "navigate"; //$NON-NLS-1$
    public static final String SWITCH_TO_LAUNCH_BAR = "switchToLaunchBar"; //$NON-NLS-1$
    public static final String EXECUTE = "execute"; //$NON-NLS-1$

    /**
     * Constants that represent valid action keys.
     */
    public static final String KEY_ID = "id"; //$NON-NLS-1$
    public static final String KEY_PLUGIN_ID = "pluginId"; //$NON-NLS-1$
    public static final String KEY_CLASS = "class"; //$NON-NLS-1$
    public static final String KEY_STANDBY = "standby"; //$NON-NLS-1$
    public static final String KEY_PART_ID = "partId"; //$NON-NLS-1$
    public static final String KEY_INPUT = "input"; //$NON-NLS-1$
    public static final String KEY_MESSAGE = "message"; //$NON-NLS-1$
    public static final String KEY_URL = "url"; //$NON-NLS-1$
    public static final String KEY_DIRECTION = "direction"; //$NON-NLS-1$
    public static final String KEY_EMBED = "embed"; //$NON-NLS-1$
    public static final String KEY_EMBED_TARGET = "embedTarget"; //$NON-NLS-1$
    public static final String KEY_DECODE = "decode"; //$NON-NLS-1$
    public static final String KEY_COMAND = "command"; //$NON-NLS-1$


    public static final String VALUE_BACKWARD = "backward"; //$NON-NLS-1$
    public static final String VALUE_FORWARD = "forward"; //$NON-NLS-1$
    public static final String VALUE_HOME = "home"; //$NON-NLS-1$
    public static final String VALUE_TRUE = "true"; //$NON-NLS-1$
    public static final String VALUE_FALSE = "false"; //$NON-NLS-1$
	public static final String VALUE_CLOSE = "close"; //$NON-NLS-1$
	public static final String VALUE_STANDBY = "standby"; //$NON-NLS-1$
	public static final String VALUE_LAUNCHBAR = "launchbar"; //$NON-NLS-1$



    private String action = null;
    private Properties parameters = null;

    /**
     * Prevent creation. Must be created through an IntroURLParser. This
     * constructor assumed we have a valid intro url.
     *
     * @param url
     */
    IntroURL(String action, Properties parameters) {
        this.action = action;
        this.parameters = parameters;
    }

    /**
     * Executes whatever valid Intro action is embedded in this Intro URL.
     *
     */
    @Override
	public boolean execute() {
        final boolean[] result = new boolean[1];
        Display display = Display.getCurrent();

        BusyIndicator.showWhile(display, () -> result[0] = doExecute());
        return result[0];
    }

    protected boolean doExecute() {
        if (Log.logInfo) {
            String msg = StringUtil.concat("Running Intro URL action ", action, //$NON-NLS-1$
                " with parameters: ", parameters.toString()); //$NON-NLS-1$
            Log.info(msg);
        }

        // check for all supported Intro actions first.
        if (action.equals(CLOSE))
            return closeIntro();

        else if (action.equals(SET_STANDBY_MODE))
            // Sets the state of the intro part. Does not care about passing
            // input to the part.
            return setStandbyState(getParameter(KEY_STANDBY));

        else if (action.equals(SHOW_STANDBY))
            return handleStandbyState(getParameter(KEY_PART_ID),
                getParameter(KEY_INPUT));

        else if (action.equals(SHOW_HELP))
            // display the full Help System.
            return showHelp();

        else if (action.equals(SHOW_HELP_TOPIC))
            // display a Help System Topic. It can be displayed in the Help
            // system window, or embedded as an intro page.
            // return showHelpTopic(getParameter(KEY_ID));
            return showHelpTopic(getParameter(KEY_ID), getParameter(KEY_EMBED),
                getParameter(KEY_EMBED_TARGET));

        else if (action.equals(OPEN_BROWSER))
            // display url in external browser
            return openBrowser(getParameter(KEY_URL),
                getParameter(KEY_PLUGIN_ID));

        if (action.equals(OPEN_URL))
            // display url embedded in intro browser.
            return openURL(getParameter(KEY_URL), getParameter(KEY_PLUGIN_ID));

        else if (action.equals(RUN_ACTION))
            // run an Intro action. Get the pluginId and the class keys. Pass
            // the parameters and the standby state.
            return runAction(getParameter(KEY_PLUGIN_ID),
                getParameter(KEY_CLASS), parameters, getParameter(KEY_STANDBY));

        else if (action.equals(EXECUTE))
			// execute a serialized command
			return executeCommand(getParameter(KEY_COMAND), getParameter(KEY_STANDBY));

        else if (action.equals(SHOW_PAGE))
            // display an Intro Page.
            return showPage(getParameter(KEY_ID), getParameter(KEY_STANDBY));

        else if (action.equals(SHOW_MESSAGE))
            return showMessage(getParameter(KEY_MESSAGE));

        else if (action.equals(NAVIGATE))
            return navigate(getParameter(KEY_DIRECTION));

        else if (action.equals(SWITCH_TO_LAUNCH_BAR))
            return switchToLaunchBar();

        else
            return handleCustomAction();
    }


    private boolean closeIntro() {
        // Relies on Workbench.
        return IntroPlugin.closeIntro();
    }

    /**
     * Sets the into part to standby, and shows the passed standby part, with
     * the given input. Forces the Intro view to open, if not yet created.
     *
     * @param partId
     * @param input
     */
    private boolean handleStandbyState(String partId, String input) {
        // set intro to standby mode. we know we have a customizable part.
        CustomizableIntroPart introPart = (CustomizableIntroPart) IntroPlugin
            .getIntro();
        if (introPart == null)
            introPart = (CustomizableIntroPart) IntroPlugin.showIntro(true);
        // store the flag to indicate that standbypart is needed.
        introPart.getControl().setData(IIntroConstants.SHOW_STANDBY_PART,
            VALUE_TRUE);
        IntroPlugin.setIntroStandby(true);
        StandbyPart standbyPart = introPart
            .getAdapter(StandbyPart.class);

        // We may not have a valid partId or we might fail to instantiate part or
        // create the part content. An empty part will be shown. Signal failure.
        return standbyPart != null && standbyPart.showContentPart(partId, input);
    }

    /**
	 * Set the Workbench Intro Part state. Forces the Intro view to open, if not yet created.
	 *
	 * Historically this value was "true" (show standby) or "false" (show normal). In Neon we add
	 * "close", "standby" and "launchbar".
	 *
	 * @param state
	 * @return true if the intro was shown, or false if the intro could not be shown
	 */
    private boolean setStandbyState(String state) {
        if (state == null)
            return false;
		if (state.equals(VALUE_CLOSE)) {
			return IntroPlugin.closeIntro();
		} else if (state.equals(VALUE_LAUNCHBAR)) {
			return switchToLaunchBar();
		}
		boolean standby = state.equals(VALUE_TRUE) || state.equals(VALUE_STANDBY);

        IIntroPart introPart = IntroPlugin.showIntro(standby);
        if (introPart == null)
            return false;
        return true;
    }


    /**
     * Run an action
     */
    private boolean runAction(String pluginId, String className,
            Properties parameters, String standbyState) {

        Object actionObject = ModelLoaderUtil.createClassInstance(pluginId,
            className);
        try {
            if (actionObject instanceof IIntroAction) {
                IIntroAction introAction = (IIntroAction) actionObject;
                IIntroSite site = IntroPlugin.getDefault().getIntroModelRoot()
                    .getPresentation().getIntroPart().getIntroSite();
                introAction.run(site, parameters);
            } else if (actionObject instanceof IAction) {
                IAction action = (IAction) actionObject;
                action.run();

            } else if (actionObject instanceof IActionDelegate) {
                final IActionDelegate delegate = (IActionDelegate) actionObject;
                if (delegate instanceof IWorkbenchWindowActionDelegate)
                    ((IWorkbenchWindowActionDelegate) delegate).init(PlatformUI
                        .getWorkbench().getActiveWorkbenchWindow());
                Action proxy = new Action(this.action) {

                    @Override
					public void run() {
                        delegate.run(this);
                    }
                };
                proxy.run();
            } else
                // we could not create the class.
                return false;
            // ran action successfully. Now set intro intro standby if needed.
            if (standbyState == null)
                return true;
            return setStandbyState(standbyState);
        } catch (Exception e) {
            Log.error("Could not run action: " + className, e); //$NON-NLS-1$
            return false;
        }
    }

    /**
	 * Executes a serialized <code>ParameterizedCommand</code>. Uses
	 * {@link ICommandService#deserialize(String)} to convert the <code>command</code> argument
	 * into the parameterized command.
	 */
	private boolean executeCommand(String command, String standbyState) {
		ICommandService commandService = getCommandService();
		IHandlerService handlerService = getHandlerService();
		if (commandService == null || handlerService == null) {
			Log.error("Could not get ICommandService or IHandlerService while trying to execute: " + command, null); //$NON-NLS-1$
			return false;
		}

		try {
			ParameterizedCommand pCommand = commandService.deserialize(command);
			handlerService.executeCommand(pCommand, null);

			// Executed command successfully. Now set intro standby if needed.
			if (standbyState == null)
				return true;
			return setStandbyState(standbyState);
		} catch (CommandException ex) {
			Log.error("Could not execute command: " + command, ex); //$NON-NLS-1$
			return false;
		}
	}

    private ICommandService getCommandService() {
		IWorkbench wb =	PlatformUI.getWorkbench();
		if (wb != null) {
			Object serviceObject = wb.getAdapter(ICommandService.class);
		    if (serviceObject != null) {
			    ICommandService service = (ICommandService)serviceObject;
			    return service;
		    }
		}
		return null;
	}

	private IHandlerService getHandlerService() {
		IWorkbench wb =	PlatformUI.getWorkbench();
		if (wb != null) {
			Object serviceObject = wb.getAdapter(IHandlerService.class);
		    if (serviceObject != null) {
			    IHandlerService service = (IHandlerService)serviceObject;
			    return service;
		    }
		}
		return null;
	}

    /**
     * Open a help topic. If embed="true", open the help href as an intro page.
     * If false, open the href in the Help system window. If embedTarget is set,
     * then the Help System topic is embedded instead of the content of the
     * specified div.<br>
     * In the case of SWT presentation, embedd flag is ignored and the topic is
     * opened in the Help system window.
     */
    private boolean showHelpTopic(String href, String embed, String embedTarget) {
        return new ShowHelpURLHandler(this).showHelpTopic(href, embed,
            embedTarget);
    }



    /**
     * Open the help system.
     */
    private boolean showHelp() {
        PlatformUI.getWorkbench().getHelpSystem().displayHelp();
        return true;
    }

    /**
     * Launch external browser.
     */
    private boolean openBrowser(String url, String pluginId) {
        // no need to decode url because we will create another url from this
        // url anyway. Resolve the url just in case we are trying to load a
        // plugin relative file.
        url = ModelUtil.resolveURL(url, pluginId);
        return Util.openBrowser(url);
    }


    /**
     * Show a URL in an intro page. This is the embedded version of the intro
     * action openBrowser(). It is useful when trying to show an html file
     * relative to another plugin. When the presentation is UI forms
     * presentation, this call behaves exactly as the openBrowser intro action.
     */

    private boolean openURL(String url, String pluginId) {
        IntroModelRoot model = IntroPlugin.getDefault().getIntroModelRoot();
        String presentationStyle = model.getPresentation()
            .getImplementationKind();

        if (presentationStyle.equals(IntroPartPresentation.BROWSER_IMPL_KIND)) {
            // HTML presentation
            url = ModelUtil.resolveURL(url, pluginId);
            BrowserIntroPartImplementation impl = (BrowserIntroPartImplementation) IntroPlugin
                .getDefault().getIntroModelRoot().getPresentation()
                .getIntroPartImplementation();
            Browser browser = impl.getBrowser();
            return browser.setUrl(url);
        }
        {
            // SWT presentation.
            return openBrowser(url, pluginId);
        }
    }


    private boolean showMessage(String message) {
        if (message == null)
            return false;
        DialogUtil.displayInfoMessage(null, message);
        return true;
    }

    /**
     * Display an Intro Page.
     * <p>
     * INTRO: revisit picking first page.
     */
    boolean showPage(String pageId, String standbyState) {
        // set the current page id in the model. This will trigger appropriate
        // listener event to the UI. If setting the page in the model fails (ie:
        // the page was not found in the current model, look for it in loaded
        // models. return false if failed.
        // avoid flicker.
        CustomizableIntroPart currentIntroPart = (CustomizableIntroPart) IntroPlugin
            .getIntro();
        currentIntroPart.getControl().setRedraw(false);

        IntroModelRoot modelRoot = IntroPlugin.getDefault().getIntroModelRoot();
        boolean success = modelRoot.setCurrentPageId(pageId);
        if (!success)
            success = includePageToShow(modelRoot, pageId);

        // we turned drawing off. Turn it on again.
        currentIntroPart.getControl().setRedraw(true);

        if (success) {
            // found page. Set the history
            modelRoot.getPresentation().updateHistory(
                modelRoot.getCurrentPage());
            // ran action successfully. Now set intro intro standby if needed.
            if (standbyState == null)
                return true;
            return setStandbyState(standbyState);
        }
        // could not find referenced page.
        return false;
    }

    /**
     * Finds the target page and includes it in passed model.
     *
     * @param pageId
     * @return
     */
    private boolean includePageToShow(IntroModelRoot model, String pageId) {
        AbstractIntroPage page = findPageToShow(pageId);
        if (page == null) {
            Log.warning("Could not find an Intro page with id=" + pageId); //$NON-NLS-1$
            return false;
        }
        // now clone the target page because original model should be kept
        // intact. Resolve target page first to resolve its includes
        // properly. Insert presentation shared style at the top of the shared
        // styles list because once reparented, the shared style is lost.
        // Finally, add clone page to current model.
        page.getChildren();
        // current kind.
        String currentPresentationKind = model.getPresentation()
            .getImplementationKind();
        // load shared style corresponding to same presentation kind from target
        // model.
        IntroPartPresentation targetPresentation = ((IntroModelRoot) page
            .getParent()).getPresentation();
        String targetSharedStyle = targetPresentation
            .getSharedStyle(currentPresentationKind);
        // clone.
        AbstractIntroPage clonedPage = null;
        try {
            clonedPage = (AbstractIntroPage) page.clone();
        } catch (CloneNotSupportedException ex) {
            // should never be here.
            Log.error("Failed to clone Intro model node.", ex); //$NON-NLS-1$
            return false;
        }
        // reparent cloned target to current model.
        clonedPage.setParent(model);
        // REVISIT: SWT presentation does not support multiple shared
        // styles.
        if (targetSharedStyle != null)
            // add target model shared style.
            clonedPage.insertStyle(targetSharedStyle, 0);
        model.addChild(clonedPage);
        return model.setCurrentPageId(clonedPage.getId());
    }


    /**
     * Searches all loaded models for the first page with the given id.
     *
     * @param pageId
     * @return
     */
    private AbstractIntroPage findPageToShow(String pageId) {
		// check all cached models.
		for (IntroModelRoot model : ExtensionPointManager.getInst().getIntroModels().values()) {
            AbstractIntroPage page = (AbstractIntroPage) model.findChild(
                pageId, AbstractIntroElement.ABSTRACT_PAGE);
            if (page != null)
                return page;
        }
        // could not find page in any model.
        return null;
    }

    /**
     * Navigate foward in the presentation, whichever one it is.
     *
     * @return
     */
    private boolean navigate(String direction) {
        // set intro to standby mode. we know we have a customizable part.
        CustomizableIntroPart introPart = (CustomizableIntroPart) IntroPlugin
            .getIntro();
        if (introPart == null)
            // intro is closed. Do nothing.
            return false;

        IntroPartPresentation presentation = introPart
            .getAdapter(IntroPartPresentation.class);

        if (direction.equalsIgnoreCase(VALUE_BACKWARD))
            return presentation.navigateBackward();
        else if (direction.equalsIgnoreCase(VALUE_FORWARD))
            return presentation.navigateForward();
        else if (direction.equalsIgnoreCase(VALUE_HOME))
            return presentation.navigateHome();
        return false;
    }


    /**
     * @return Returns the action imbedded in this URL.
     */
    @Override
	public String getAction() {
        return action;
    }

    /**
     * Return a parameter defined in the Intro URL. Returns null if the
     * parameter is not defined. If this intro url has a decode=true parameter,
     * then all parameters are returned decoded using UTF-8.
     *
     * @param parameterId
     * @return
     */
    @Override
	public String getParameter(String parameterId) {
        // make sure to decode only on return, since we may need to recreate the
        // url when handling custom urls.
        String value = parameters.getProperty(parameterId);
        String decode = parameters.getProperty(KEY_DECODE);

        if (value != null)
            try {
                if (decode!=null && decode.equalsIgnoreCase(VALUE_TRUE))
                    // we are told to decode the parameters of the url through
                    // the decode parameter. Assume that parameters are
                    // UTF-8 encoded.
                    return StringUtil.decode(value, "UTF-8"); //$NON-NLS-1$
                return value;
            } catch (Exception e) {
                // should never be here.
                Log.error("Failed to decode URL: " + parameterId, e); //$NON-NLS-1$
            }
        return value;
    }

    private boolean handleCustomAction() {
        IntroURLAction command = ExtensionPointManager.getInst()
            .getSharedConfigExtensionsManager().getCommand(action);
        if (command == null) {
            String message = Messages.IntroURL_badCommand;
            DialogUtil.displayInfoMessage(null, message,
                new Object[] { action });
            return false;
        }

        // custom command. execute it.
        StringBuilder url = new StringBuilder();
        url.append("http://org.eclipse.ui.intro/"); //$NON-NLS-1$
        url.append(command.getReplaceValue().trim());
        if (command.getReplaceValue().indexOf("?") == -1) //$NON-NLS-1$
            // command does not have parameters.
            url.append("?"); //$NON-NLS-1$
        else
            // command already has parameters.
            url.append("&"); //$NON-NLS-1$
        url.append(retrieveInitialQuery());
        IIntroURL introURL = IntroURLFactory.createIntroURL(url.toString());
        if (introURL != null)
            return introURL.execute();
        return false;
    }


    /**
     * Recreate the initial query passed to this URL.
     *
     * @return
     */
    private String retrieveInitialQuery() {
        StringBuilder query = new StringBuilder();
        Enumeration keys = parameters.keys();
        while (keys.hasMoreElements()) {
            String key = (String) keys.nextElement();
            query.append(key);
            query.append("="); //$NON-NLS-1$
            query.append(parameters.get(key));
            if (keys.hasMoreElements())
                query.append("&"); //$NON-NLS-1$
        }
        return query.toString();
    }


    private boolean switchToLaunchBar() {
        IIntroPart intro = PlatformUI.getWorkbench().getIntroManager()
            .getIntro();
        if (intro == null)
            return false;

        CustomizableIntroPart cpart = (CustomizableIntroPart) intro;
        IntroModelRoot modelRoot = IntroPlugin.getDefault().getIntroModelRoot();
        Rectangle bounds = cpart.getControl().getBounds();
        Rectangle startBounds = Geometry.toDisplay(cpart.getControl()
            .getParent(), bounds);

		IntroLaunchBarElement launchBarElement = modelRoot.getPresentation().getLaunchBarElement();
		if (launchBarElement == null)
			return true;
		IWorkbenchWindow window = intro.getIntroSite().getWorkbenchWindow();
		IntroLaunchBar launchBar = IntroLaunchBar.create(window, modelRoot, launchBarElement);

        PlatformUI.getWorkbench().getIntroManager().setIntroStandby(intro, true);

        closeIntro();
        Rectangle endBounds = Geometry.toDisplay(launchBar.getControl()
            .getParent(), launchBar.getControl().getBounds());

		AnimationEngine.createTweakedAnimation(window.getShell(), 400, startBounds, endBounds);
        return true;
    }
}
