package com.vaadin.navigator;

/*
 @VaadinApache2LicenseForJavaFiles@
 */

import java.io.Serializable;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import com.vaadin.navigator.ViewChangeListener.ViewChangeEvent;
import com.vaadin.terminal.Page;
import com.vaadin.terminal.Page.FragmentChangedEvent;
import com.vaadin.terminal.Page.FragmentChangedListener;
import com.vaadin.ui.Component;
import com.vaadin.ui.ComponentContainer;
import com.vaadin.ui.CssLayout;
import com.vaadin.ui.CustomComponent;

/**
 * Navigator utility that allows switching of views in a part of an application.
 * 
 * The view switching can be based e.g. on URI fragments containing the view
 * name and parameters to the view. There are two types of parameters for views:
 * an optional parameter string that is included in the fragment (may be
 * bookmarkable).
 * 
 * Views can be explicitly registered or dynamically generated and listening to
 * view changes is possible.
 * 
 * Note that {@link Navigator} is not a component itself but comes with
 * {@link SimpleViewDisplay} which is a component that displays the selected
 * view as its contents.
 * 
 * @author Vaadin Ltd
 * @since 7.0
 */
public class Navigator implements Serializable {

    // TODO divert navigation e.g. if no permissions? Or just show another view
    // but keep URL? how best to intercept
    // TODO investigate relationship with TouchKit navigation support

    /**
     * Empty view component.
     */
    public static class EmptyView extends CssLayout implements View {
        /**
         * Create minimally sized empty view.
         */
        public EmptyView() {
            setWidth("0px");
            setHeight("0px");
        }

        public void navigateTo(String fragmentParameters) {
            // nothing to do
        }
    }

    /**
     * Fragment manager using URI fragments of a Page to track views and enable
     * listening to view changes.
     * 
     * This class is mostly for internal use by Navigator, and is only public
     * and static to enable testing.
     */
    public static class UriFragmentManager implements FragmentManager,
            FragmentChangedListener {
        private final Page page;
        private final Navigator navigator;

        /**
         * Create a new URIFragmentManager and attach it to listen to URI
         * fragment changes of a {@link Page}.
         * 
         * @param page
         *            page whose URI fragment to get and modify
         * @param navigator
         *            {@link Navigator} to notify of fragment changes (using
         *            {@link Navigator#navigateTo(String)}
         */
        public UriFragmentManager(Page page, Navigator navigator) {
            this.page = page;
            this.navigator = navigator;

            page.addListener(this);
        }

        public String getFragment() {
            return page.getFragment();
        }

        public void setFragment(String fragment) {
            page.setFragment(fragment, false);
        }

        public void fragmentChanged(FragmentChangedEvent event) {
            UriFragmentManager.this.navigator.navigateTo(getFragment());
        }
    }

    /**
     * View display that is a component itself and replaces its contents with
     * the view.
     * 
     * This display only supports views that are {@link Component}s themselves.
     * Attempting to display a view that is not a component causes an exception
     * to be thrown.
     * 
     * By default, the view display has full size.
     */
    public static class SimpleViewDisplay extends CustomComponent implements
            ViewDisplay {

        /**
         * Create new {@link ViewDisplay} that is itself a component displaying
         * the view.
         */
        public SimpleViewDisplay() {
            setSizeFull();
        }

        public void showView(View view) {
            if (view instanceof Component) {
                setCompositionRoot((Component) view);
            } else {
                throw new IllegalArgumentException("View is not a component: "
                        + view);
            }
        }
    }

    /**
     * View display that replaces the contents of a {@link ComponentContainer}
     * with the active {@link View}.
     * 
     * All components of the container are removed before adding the new view to
     * it.
     * 
     * This display only supports views that are {@link Component}s themselves.
     * Attempting to display a view that is not a component causes an exception
     * to be thrown.
     */
    public static class ComponentContainerViewDisplay implements ViewDisplay {

        private final ComponentContainer container;

        /**
         * Create new {@link ViewDisplay} that updates a
         * {@link ComponentContainer} to show the view.
         */
        public ComponentContainerViewDisplay(ComponentContainer container) {
            this.container = container;
        }

        public void showView(View view) {
            if (view instanceof Component) {
                container.removeAllComponents();
                container.addComponent((Component) view);
            } else {
                throw new IllegalArgumentException("View is not a component: "
                        + view);
            }
        }
    }

    /**
     * View provider which supports mapping a single view name to a single
     * pre-initialized view instance.
     * 
     * For most cases, ClassBasedViewProvider should be used instead of this.
     */
    public static class StaticViewProvider implements ViewProvider {
        private final String viewName;
        private final View view;

        /**
         * Create a new view provider which returns a pre-created view instance.
         * 
         * @param viewName
         *            name of the view (not null)
         * @param view
         *            view instance to return (not null), reused on every
         *            request
         */
        public StaticViewProvider(String viewName, View view) {
            this.viewName = viewName;
            this.view = view;
        }

        public String getViewName(String viewAndParameters) {
            if (null == viewAndParameters) {
                return null;
            }
            if (viewAndParameters.startsWith(viewName)) {
                return viewName;
            }
            return null;
        }

        public View getView(String viewName) {
            if (this.viewName.equals(viewName)) {
                return view;
            }
            return null;
        }

        /**
         * Get the view name for this provider.
         * 
         * @return view name for this provider
         */
        public String getViewName() {
            return viewName;
        }
    }

    /**
     * View provider which maps a single view name to a class to instantiate for
     * the view.
     * 
     * Note that the view class must be accessible by the class loader used by
     * the provider. This may require its visibility to be public.
     * 
     * This class is primarily for internal use by {@link Navigator}.
     */
    public static class ClassBasedViewProvider implements ViewProvider {

        private final String viewName;
        private final Class<? extends View> viewClass;

        /**
         * Create a new view provider which creates new view instances based on
         * a view class.
         * 
         * @param viewName
         *            name of the views to create (not null)
         * @param viewClass
         *            class to instantiate when a view is requested (not null)
         */
        public ClassBasedViewProvider(String viewName,
                Class<? extends View> viewClass) {
            if (null == viewName || null == viewClass) {
                throw new IllegalArgumentException(
                        "View name and class should not be null");
            }
            this.viewName = viewName;
            this.viewClass = viewClass;
        }

        public String getViewName(String viewAndParameters) {
            if (null == viewAndParameters) {
                return null;
            }
            if (viewAndParameters.equals(viewName)
                    || viewAndParameters.startsWith(viewName + "/")) {
                return viewName;
            }
            return null;
        }

        public View getView(String viewName) {
            if (this.viewName.equals(viewName)) {
                try {
                    View view = viewClass.newInstance();
                    return view;
                } catch (InstantiationException e) {
                    // TODO error handling
                    throw new RuntimeException(e);
                } catch (IllegalAccessException e) {
                    // TODO error handling
                    throw new RuntimeException(e);
                }
            }
            return null;
        }

        /**
         * Get the view name for this provider.
         * 
         * @return view name for this provider
         */
        public String getViewName() {
            return viewName;
        }

        /**
         * Get the view class for this provider.
         * 
         * @return {@link View} class
         */
        public Class<? extends View> getViewClass() {
            return viewClass;
        }
    }

    private final FragmentManager fragmentManager;
    private final ViewDisplay display;
    private View currentView = null;
    private List<ViewChangeListener> listeners = new LinkedList<ViewChangeListener>();
    private List<ViewProvider> providers = new LinkedList<ViewProvider>();

    /**
     * Create a navigator that is tracking the active view using URI fragments
     * of the current {@link Page} and replacing the contents of a
     * {@link ComponentContainer} with the active view.
     * 
     * In case the container is not on the current page, use another
     * {@link Navigator#Navigator(Page, ViewDisplay)} with an explicitly created
     * {@link ComponentContainerViewDisplay}.
     * 
     * All components of the container are removed each time before adding the
     * active {@link View}. Views must implement {@link Component} when using
     * this constructor.
     * 
     * <p>
     * After all {@link View}s and {@link ViewProvider}s have been registered,
     * the application should trigger navigation to the current fragment using
     * e.g.
     * 
     * <pre>
     * navigator.navigateTo(Page.getCurrent().getFragment());
     * </pre>
     * 
     * @param container
     *            ComponentContainer whose contents should be replaced with the
     *            active view on view change
     */
    public Navigator(ComponentContainer container) {
        display = new ComponentContainerViewDisplay(container);
        fragmentManager = new UriFragmentManager(Page.getCurrent(), this);
    }

    /**
     * Create a navigator that is tracking the active view using URI fragments.
     * 
     * <p>
     * After all {@link View}s and {@link ViewProvider}s have been registered,
     * the application should trigger navigation to the current fragment using
     * e.g.
     * 
     * <pre>
     * navigator.navigateTo(Page.getCurrent().getFragment());
     * </pre>
     * 
     * @param page
     *            whose URI fragments are used
     * @param display
     *            where to display the views
     */
    public Navigator(Page page, ViewDisplay display) {
        this.display = display;
        fragmentManager = new UriFragmentManager(page, this);
    }

    /**
     * Create a navigator.
     * 
     * When a custom fragment manager is not needed, use the constructor
     * {@link #Navigator(Page, ViewDisplay)} which uses a URI fragment based
     * fragment manager.
     * 
     * Note that navigation to the initial view must be performed explicitly by
     * the application after creating a Navigator using this constructor.
     * 
     * @param fragmentManager
     *            fragment manager keeping track of the active view and enabling
     *            bookmarking and direct navigation
     * @param display
     *            where to display the views
     */
    public Navigator(FragmentManager fragmentManager, ViewDisplay display) {
        this.display = display;
        this.fragmentManager = fragmentManager;
    }

    /**
     * Navigate to a view and initialize the view with given parameters.
     * 
     * The view string consists of a view name optionally followed by a slash
     * and (fragment) parameters. ViewProviders are used to find and create the
     * correct type of view.
     * 
     * If multiple providers return a matching view, the view with the longest
     * name is selected. This way, e.g. hierarchies of subviews can be
     * registered like "admin/", "admin/users", "admin/settings" and the longest
     * match is used.
     * 
     * If the view being deactivated indicates it wants a confirmation for the
     * navigation operation, the user is asked for the confirmation.
     * 
     * Registered {@link ViewChangeListener}s are called upon successful view
     * change.
     * 
     * @param viewAndParameters
     *            view name and parameters
     */
    public void navigateTo(String viewAndParameters) {
        String longestViewName = null;
        View viewWithLongestName = null;
        for (ViewProvider provider : providers) {
            String viewName = provider.getViewName(viewAndParameters);
            if (null != viewName
                    && (longestViewName == null || viewName.length() > longestViewName
                            .length())) {
                View view = provider.getView(viewName);
                if (null != view) {
                    longestViewName = viewName;
                    viewWithLongestName = view;
                }
            }
        }
        if (viewWithLongestName != null) {
            String parameters = null;
            if (viewAndParameters.length() > longestViewName.length() + 1) {
                parameters = viewAndParameters.substring(longestViewName
                        .length() + 1);
            }
            navigateTo(viewWithLongestName, longestViewName, parameters);
        }
        // TODO if no view is found, what to do?
    }

    /**
     * Internal method activating a view, setting its parameters and calling
     * listeners.
     * 
     * This method also verifies that the user is allowed to perform the
     * navigation operation.
     * 
     * @param view
     *            view to activate
     * @param viewName
     *            (optional) name of the view or null not to set the fragment
     * @param fragmentParameters
     *            parameters passed in the fragment for the view
     */
    protected void navigateTo(View view, String viewName,
            String fragmentParameters) {
        ViewChangeEvent event = new ViewChangeEvent(this, currentView, view,
                viewName, fragmentParameters);
        if (!isViewChangeAllowed(event)) {
            return;
        }

        if (null != viewName && getFragmentManager() != null) {
            String currentFragment = viewName;
            if (fragmentParameters != null) {
                currentFragment += "/" + fragmentParameters;
            }
            if (!currentFragment.equals(getFragmentManager().getFragment())) {
                getFragmentManager().setFragment(currentFragment);
            }
        }

        view.navigateTo(fragmentParameters);
        currentView = view;

        if (display != null) {
            display.showView(view);
        }

        fireViewChange(event);
    }

    /**
     * Check whether view change is allowed.
     * 
     * All related listeners are called. The view change is blocked if any of
     * them wants to block the navigation operation.
     * 
     * The view change listeners may also e.g. open a warning or question dialog
     * and save the parameters to re-initiate the navigation operation upon user
     * action.
     * 
     * @param event
     *            view change event (not null, view change not yet performed)
     * @return true if the view change should be allowed, false to silently
     *         block the navigation operation
     */
    protected boolean isViewChangeAllowed(ViewChangeEvent event) {
        for (ViewChangeListener l : listeners) {
            if (!l.isViewChangeAllowed(event)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Return the fragment manager that is used to get, listen to and manipulate
     * the URI fragment or other source of navigation information.
     * 
     * @return fragment manager in use
     */
    protected FragmentManager getFragmentManager() {
        return fragmentManager;
    }

    /**
     * Returns the ViewDisplay used by the navigator. Unless another display is
     * specified, a {@link SimpleViewDisplay} (which is a {@link Component}) is
     * used by default.
     * 
     * @return current ViewDisplay
     */
    public ViewDisplay getDisplay() {
        return display;
    }

    /**
     * Fire an event when the current view has changed.
     * 
     * @param event
     *            view change event (not null)
     */
    protected void fireViewChange(ViewChangeEvent event) {
        for (ViewChangeListener l : listeners) {
            l.navigatorViewChanged(event);
        }
    }

    /**
     * Register a static, pre-initialized view instance for a view name.
     * 
     * Registering another view with a name that is already registered
     * overwrites the old registration of the same type.
     * 
     * @param viewName
     *            String that identifies a view (not null nor empty string)
     * @param view
     *            {@link View} instance (not null)
     */
    public void addView(String viewName, View view) {

        // Check parameters
        if (viewName == null || view == null) {
            throw new IllegalArgumentException(
                    "view and viewName must be non-null");
        }

        removeView(viewName);
        registerProvider(new StaticViewProvider(viewName, view));
    }

    /**
     * Register for a view name a view class.
     * 
     * Registering another view with a name that is already registered
     * overwrites the old registration of the same type.
     * 
     * A new view instance is created every time a view is requested.
     * 
     * @param viewName
     *            String that identifies a view (not null nor empty string)
     * @param viewClass
     *            {@link View} class to instantiate when a view is requested
     *            (not null)
     */
    public void addView(String viewName, Class<? extends View> viewClass) {

        // Check parameters
        if (viewName == null || viewClass == null) {
            throw new IllegalArgumentException(
                    "view and viewClass must be non-null");
        }

        removeView(viewName);
        registerProvider(new ClassBasedViewProvider(viewName, viewClass));
    }

    /**
     * Remove view from navigator.
     * 
     * This method only applies to views registered using
     * {@link #addView(String, View)} or {@link #addView(String, Class)}.
     * 
     * @param viewName
     *            name of the view to remove
     */
    public void removeView(String viewName) {
        Iterator<ViewProvider> it = providers.iterator();
        while (it.hasNext()) {
            ViewProvider provider = it.next();
            if (provider instanceof StaticViewProvider) {
                StaticViewProvider staticProvider = (StaticViewProvider) provider;
                if (staticProvider.getViewName().equals(viewName)) {
                    it.remove();
                }
            } else if (provider instanceof ClassBasedViewProvider) {
                ClassBasedViewProvider classBasedProvider = (ClassBasedViewProvider) provider;
                if (classBasedProvider.getViewName().equals(viewName)) {
                    it.remove();
                }
            }
        }
    }

    /**
     * Register a view provider (factory).
     * 
     * Providers are called in order of registration until one that can handle
     * the requested view name is found.
     * 
     * @param provider
     *            provider to register
     */
    public void registerProvider(ViewProvider provider) {
        providers.add(provider);
    }

    /**
     * Unregister a view provider (factory).
     * 
     * @param provider
     *            provider to unregister
     */
    public void unregisterProvider(ViewProvider provider) {
        providers.remove(provider);
    }

    /**
     * Listen to changes of the active view.
     * 
     * The listener will get notified after the view has changed.
     * 
     * @param listener
     *            Listener to invoke after view changes.
     */
    public void addListener(ViewChangeListener listener) {
        listeners.add(listener);
    }

    /**
     * Remove a view change listener.
     * 
     * @param listener
     *            Listener to remove.
     */
    public void removeListener(ViewChangeListener listener) {
        listeners.remove(listener);
    }

}
