package com.ited.gwt.history.client;

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

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.user.client.Window;
import com.ited.gwt.logger.client.FactoryLogger;
import com.ited.gwt.logger.client.Logger;
import com.ited.gwt.misc.client.UtilsDom;
import com.ited.gwt.misc.client.UtilsGwt;
import com.ited.gwt.misc.client.UtilsJsni;
import com.ited.lang.asserts.arg.ArgsClass;
import com.ited.lang.html.UtilsHtml;
import com.ited.lang.misc.UtilsMisc;
import com.ited.lang.misc.UtilsString;

/**
 * Extends GWT's {@link SimpleBrowserHistory} and adds HTML5 pushState support.
 *
 * <p>The complete path is treated as history token.
 * The leading '/' is hidden from GWTs History API, so that the path '/' is returned as an empty
 * history token ('').</p>
 *
 * @author  Ed Bras
 */
public final class PushStateHistory extends SimpleBrowserHistory {

	private static final String ERR_ID = "PshStHs_";

	private final Logger log = FactoryLogger.getLogger(this.getClass().getName());

	PushStateHistory() {
	}

	@Override
	public void newToken(final String historyToken) {
		ArgsClass.notNullObjMsg(getClass(), ERR_ID + "NwT", historyToken);
		final String token = removeEndingPathSeparator(removeStartingPathSeparator(historyToken)); // clean the token
		newTokenIntern(token, createUrl(token));
	}

	//
	//
	@Override
	protected void init() {
		initHistoryToken();
		//		updateHistoryToken(Window.Location.getPath() + Window.Location.getQueryString()); // initialize with the current path
		//		newToken(getToken()); // initialize the empty state with the current history token
		initPopStateHandler(); // initialize the popState handler
	}

	private String createUrl(final String historyToken) {
		if (UtilsString.hasContentTrim(historyToken)) {
			final StringBuilder url = new StringBuilder();
			final String base = createBaseUrl();
			if (UtilsString.hasContentTrim(base)) {
				url.append(base);
			}
			if (hasUrlHistoryTokenPrefix()) {
				url.append(UtilsMisc.ensureEndPathSeparatorAdded(getUrlHistoryTokenPrefix()));
			}
			return url.append(historyToken).append('/').append(Window.Location.getQueryString()).toString();
		}
		return null;
	}

	private void initHistoryToken() {
		// Required to ensure that this instance contains the current history token present in the url, as it's to start the app correctly.
		if (UtilsString.isEmptyTrim(getToken())) {
			// We add the current history token if present such that we can browse it through the browser back/forward. That isn't possible if we
			// don't add it to the browser history stack.
			//
			// Details: in case the marker is detected during startup, we not mark it as a browser history event such that we allow the view to add a
			// history marker, else we can't browse back/forward to this point.
			// Example: suppose we start the app with "/declare/followUp" such that the followUp page is shown. If not marker is added,
			// we can't go back to the followUp page when navigating away from it.
			final String historyToken = createCurrentUrlHistoryToken();
			if (UtilsString.hasContent(historyToken)) {
				getBrowserHistory().newItem(historyToken, isEventFireAllowedDuringCreation());
			}
		}
	}

	private String createCurrentUrlHistoryToken() {
		final String base = createBaseUrl();
		final int sizeBase = UtilsString.size(base);
		// Ensure the closing "/" is present, else the last part something like "example.com/bla" will not be seen as history token.
		final String currentUrl = UtilsMisc.ensureEndPathSeparatorAdded(Window.Location.getPath());
		final int sizeCurrentUrl = UtilsString.size(currentUrl);
		if (sizeCurrentUrl > sizeBase) { // a history token is present.
			final int index = currentUrl.lastIndexOf('/');
			if (index > sizeBase) { // overcomes that we set html files as tokens.
				return removeEndingPathSeparator(currentUrl.substring(sizeBase + 1, index));
			}
		}
		return null;
	}

	private void newTokenIntern(final String historyToken, final String tokenUrl) {
		final String url = UtilsString.valueOf(tokenUrl);
		if (!url.equals(getToken())) {
			getLog().logDebug("New token '" + url + "'");
			String result = UtilsGwt.appendGwtCodeServer(encodeHistoryToken(url));
			result = addStartingPathSeparator(result);
			pushState(historyToken, result);
			getLog().logDebug("Pushed '" + result + "' (" + url + ")");
		}
	}

	/**
	 * Called from native JavaScript when an old history state was popped.
	 */
	private void onPopState(final JavaScriptObject state) {
		if (isNotNull(state)) {
			final String historyToken = UtilsJsni.getPropertyAsString(state, getHistoryTokenKey());
			getLog().logDebug("Popped '" + historyToken + "'");
			setToken(historyToken);
			//			updateHistoryToken(historyToken);
			getBrowserHistory().fireCurrentHistoryState();
		}
	}

	/**
	 * @return the url without the domain, including any fixed url prefix.
	 */
	private String createBaseUrl() {
		final String url = UtilsHtml.createBaseUrlOnly(UtilsHtml.removeFirstPath(GWT.getModuleBaseURL()));
		return UtilsMisc.ensureEndPathSeparatorAdded(removeStartingPathSeparator(url));
	}

	private String removeStartingPathSeparator(final String url) {
		return UtilsMisc.ensureStartingPathSeparatorRemoved(url);
	}

	private String addStartingPathSeparator(final String url) {
		return UtilsMisc.ensureStartingPathSeparatorAdded(url);
	}

	private String removeEndingPathSeparator(final String url) {
		return UtilsMisc.ensureEndPathSeparatorRemoved(url);
	}

	private BrowserHistory getBrowserHistory() {
		return BrowserHistory.getInstance();
	}

	// @formatter:off

	/**
	 * Initialize an event handler that gets executed when the token changes.
	 */
	private native void initPopStateHandler() /*-{
	    var that = this;
	    var oldHandler = $wnd.onpopstate;
	    $wnd.onpopstate = $entry(function(event) {
        that.@com.ited.gwt.history.client.PushStateHistory::onPopState(Lcom/google/gwt/core/client/JavaScriptObject;)(event.state);
	      if (oldHandler) {
	        oldHandler(event);
	      }
	    });
	}-*/;


	private static void pushState(final String historyToken, final String url) {
		JavaScriptObject data = null;
		if (UtilsString.hasContentTrim(historyToken) && UtilsString.hasContentTrim(url)) {
			data = UtilsJsni.createPropertyObject(getHistoryTokenKey(), historyToken);
		}
		UtilsDom.pushState(data, UtilsDom.getDocumentTitle(), url);
	}

	private static String getHistoryTokenKey() {
		return  "historyToken";
	}

	// @formatter:on

	private Logger getLog() {
		return this.log;
	}
}
