package com.ited.gwt.history.client;

import static com.ited.lang.misc.UtilsMisc.isNotNull;
import static com.ited.lang.misc.UtilsMisc.isNull;

import com.google.gwt.core.shared.GWT;
import com.google.gwt.event.logical.shared.HasValueChangeHandlers;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.GwtEvent;
import com.google.gwt.event.shared.HandlerManager;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.History;
import com.ited.lang.asserts.setter.Setter;
import com.ited.lang.misc.UtilsString;

/**
 * A wrapper around {@link History} to gain flexibility.
 * @author Ed Bras
 */
public final class BrowserHistory {

	private static final String ERR_ID = "BrHst_";

	private final HistoryEventSource historyEventSource = new HistoryEventSource();

	private SimpleBrowserHistory target;

	private boolean isEventFireAllowedDuringCreation;

	private BrowserHistory() {
	}

	public static BrowserHistory getInstance() {
		return InstanceHolder.INSTANCE;
	}

	public BrowserHistory setEventFireAllowedDuringCreation(final boolean yes) {
		if (yes != isEventFireAllowedDuringCreation()) {
			Setter.notTrue(isNotNull(this.target), getClass(), ERR_ID + "EvFr", yes);
			this.isEventFireAllowedDuringCreation = yes;
		}
		return this;
	}

	public boolean isEventFireAllowedDuringCreation() {
		return this.isEventFireAllowedDuringCreation;
	}

	/**
	 * Adds a {@link com.google.gwt.event.logical.shared.ValueChangeEvent} handler to be informed of changes to the browser's history stack.
	 *
	 * @param handler the handler
	 * @return the registration used to remove this value change handler
	 */
	public HandlerRegistration addValueChangeHandler(final ValueChangeHandler<String> handler) {
		return this.historyEventSource.addValueChangeHandler(handler);
	}

	/**
	 * Encode a history token for use as part of a URI.
	 *
	 * @param historyToken the token to encode
	 * @return the encoded token, suitable for use as part of a URI
	 */
	public String encodeHistoryToken(final String historyToken) {
		return getEnsureTarget().encodeHistoryToken(historyToken);
	}

	/**
	* Fire {@link ValueChangeHandler#onValueChange(com.google.gwt.event.logical.shared.ValueChangeEvent)}
	* events with the current history state. This is most often called at the end of an application's
	* {@link com.google.gwt.core.client.EntryPoint#onModuleLoad()} to inform history handlers of the initial application state.
	*/
	public void fireCurrentHistoryState() {
		getHistoryEventSource().fireValueChangedEvent(getToken());
	}

	/**
	 * Gets the current history token. The handler will not receive a
	 * {@link ValueChangeHandler#onValueChange(com.google.gwt.event.logical.shared.ValueChangeEvent)}
	 * event for the initial token; requiring that an application request the token explicitly on startup gives it an opportunity to run different
	 * initialization code in the presence or absence of an initial token.
	 *
	 * @return the initial token, or the empty string if none is present.
	 */
	public String getToken() {
		return getEnsureTarget().getToken();
	}

	public boolean hasToken() {
		return UtilsString.hasContentTrim(getToken());
	}

	/**
	 * Adds a new browser history entry. Calling this method will cause
	 * {@link ValueChangeHandler#onValueChange(com.google.gwt.event.logical.shared.ValueChangeEvent)} to be called as well.
	 *
	 * @param historyToken the token to associate with the new history item
	 */
	public void newItem(final String historyToken) {
		newItem(historyToken, true);
	}

	/**
	 * Adds a new browser history entry. Calling this method will cause
	 * {@link ValueChangeHandler#onValueChange(com.google.gwt.event.logical.shared.ValueChangeEvent)}
	 * to be called as well if and only if issueEvent is true.
	 *
	 * @param token the token to associate with the new history item
	 * @param issueEvent true if a {@link ValueChangeHandler#onValueChange(com.google.gwt.event.logical.shared.ValueChangeEvent)} event should be issued
	 */
	public void newItem(final String token, final boolean issueEvent) {
		final String historyToken = isNull(token) ? "" : token;
		if (!historyToken.equals(getToken())) {
			setToken(historyToken);
			getEnsureTarget().newToken(encodeHistoryToken(historyToken));
			if (issueEvent) {
				getHistoryEventSource().fireValueChangedEvent(historyToken);
			}
		}
	}

	public void setUrlHistoryTokenPrefix(final String historyTokenPrefix) {
		getEnsureTarget().setUrlHistoryTokenPrefix(historyTokenPrefix);
	}

	public String getUrlHistoryTokenPrefix() {
		return getEnsureTarget().getUrlHistoryTokenPrefix();
	}

	public boolean hasUrlHistoryTokenPrefix() {
		return getEnsureTarget().hasUrlHistoryTokenPrefix();
	}

	/**
	* Programmatic equivalent to the user pressing the browser's 'back' button.
	*/
	public native void back() /*-{ $wnd.history.back(); }-*/;

	/**
	* Programmatic equivalent to the user pressing the browser's 'forward' button.
	*/
	public native void forward() /*-{ $wnd.history.forward();}-*/;

	/**
	 * Replace the current history token on top of the browsers history stack.
	 *
	 * <p>Note: This method has problems. The URL is updated with window.location.replace,
	 * this unfortunately has side effects when using the deprecated iframe linker (ie. "std" linker). Make sure you are using the cross site
	 * iframe linker when using this method in your code.
	 *
	 * <p>Calling this method will cause {@link ValueChangeHandler#onValueChange(com.google.gwt.event.logical.shared.ValueChangeEvent)} to be called as well.
	 *
	 * @param historyToken history token to replace current top entry
	 */
	public void replaceItem(final String historyToken) {
		replaceItem(historyToken, true);
	}

	/**
	 * Replace the current history token on top of the browsers history stack.
	 *
	 * <p>Note: This method has problems. The URL is updated with window.location.replace, this unfortunately has side effects when using the
	 * deprecated iframe linker (ie. "std" linker). Make sure you are using the cross site iframe linker when using this method in your code.
	 *
	 * <p>Calling this method will cause
	 * {@link ValueChangeHandler#onValueChange(com.google.gwt.event.logical.shared.ValueChangeEvent)}
	 * to be called as well if and only if issueEvent is true.
	 *
	 * @param historyToken history token to replace current top entry
	 * @param issueEvent issueEvent true if a {@link ValueChangeHandler#onValueChange(com.google.gwt.event.logical.shared.ValueChangeEvent)}
	 *          event should be issued
	 */
	public void replaceItem(final String historyToken, final boolean issueEvent) {
		setToken(historyToken);
		getEnsureTarget().replaceToken(encodeHistoryToken(historyToken));
		if (issueEvent) {
			fireCurrentHistoryState();
		}
	}

	//
	//
	protected SimpleBrowserHistory getEnsureTarget() {
		if (isNull(this.target)) {
			this.target = GWT.create(SimpleBrowserHistory.class);
			this.target.setEventFireAllowedDuringCreation(isEventFireAllowedDuringCreation());
			this.target.init();
		}
		return this.target;
	}

	protected HistoryEventSource getHistoryEventSource() {
		return this.historyEventSource;
	}

	protected void setToken(final String token) {
		getEnsureTarget().setToken(token);
	}

	/**
	 */
	private static final class HistoryEventSource implements HasValueChangeHandlers<String> {

		private final HandlerManager handlers = new HandlerManager(null);

		private HistoryEventSource() {
		}

		@Override
		public void fireEvent(final GwtEvent<?> event) {
			getHandlers().fireEvent(event);
		}

		@Override
		public HandlerRegistration addValueChangeHandler(final ValueChangeHandler<String> handler) {
			return getHandlers().addHandler(ValueChangeEvent.getType(), handler);
		}

		public void fireValueChangedEvent(final String newToken) {
			ValueChangeEvent.fire(this, newToken);
		}

		public HandlerManager getHandlers() {
			return this.handlers;
		}
	}

	/**
	 */
	private interface InstanceHolder {
		BrowserHistory INSTANCE = GWT.create(BrowserHistory.class);
	}

}
