/*
 * Copyright 2009 Google Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package com.google.gwt.core.client.impl;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.impl.AsyncFragmentLoader.LoadTerminatedHandler;
import com.google.gwt.core.client.impl.AsyncFragmentLoader.LoadingStrategy;

/**
 * Load runAsync code using a script tag. The
 * {@link com.google.gwt.core.linker.XSLinker} sets
 * <code>__gwtModuleFunction</code> to point at the function that wraps the
 * initially downloaded code. On that function is a property
 * <code>installCode</code> that can be invoked to eval more code in a scope
 * nested somewhere within that function. The loaded script for fragment 123 is
 * expected to invoke __gwtModuleFunction.runAsyncCallback123 with the code to
 * be installed.
 */
public class CrossSiteLoadingStrategy implements LoadingStrategy {
  /**
   * A trivial JavaScript map from ints to ints.
   */
  private static final class IntToIntMap extends JavaScriptObject {
    public static IntToIntMap create() {
      return (IntToIntMap) JavaScriptObject.createArray();
    }

    protected IntToIntMap() {
    }

    /**
     * Get an entry. If there is no such entry, return 0.
     */
    public native int get(int x) /*-{
      return this[x] || 0;
    }-*/;
    
    public native void put(int x, int y) /*-{
      this[x] = y;
    }-*/;
  }

  private static RuntimeException LoadTerminated = new RuntimeException(
      "Code download terminated");

  /**
   * Clear callbacks on script objects.
   */
  private static native void clearCallbacks(JavaScriptObject script) /*-{
    script.onerror = script.onload = script.onreadystatechange = null;
  }-*/;

  /**
   * Clear the success callback for fragment <code>fragment</code>.
   */
  private static native void clearOnSuccess(int fragment) /*-{
    delete __gwtModuleFunction['runAsyncCallback'+fragment];
  }-*/;

  private static native JavaScriptObject createScriptTag(String url) /*-{
    var script = document.createElement('script');
    script.src = url;
    return script;
  }-*/;

  private static native void installScriptTag(JavaScriptObject script) /*-{
    document.head.appendChild(script);
  }-*/;

  private static native JavaScriptObject removeTagAndCallErrorHandler(
      int fragment, JavaScriptObject tag,
      LoadTerminatedHandler loadFinishedHandler) /*-{
     return function(exception) {
       if (tag.parentNode == null) {
         // onSuccess or onFailure must have already been called.
         return;
       }
       @com.google.gwt.core.client.impl.CrossSiteLoadingStrategy::clearOnSuccess(*)(fragment);
       @com.google.gwt.core.client.impl.CrossSiteLoadingStrategy::clearCallbacks(*)(tag);
       document.head.removeChild(tag);
       function callLoadTerminated() {
         loadFinishedHandler.@com.google.gwt.core.client.impl.AsyncFragmentLoader.LoadTerminatedHandler::loadTerminated(*)(exception);
       }
       $entry(callLoadTerminated)();
     }
   }-*/;

  private static native JavaScriptObject removeTagAndEvalCode(int fragment,
      JavaScriptObject tag) /*-{
     return function(code) {
       @com.google.gwt.core.client.impl.CrossSiteLoadingStrategy::clearOnSuccess(*)(fragment);
       @com.google.gwt.core.client.impl.CrossSiteLoadingStrategy::clearCallbacks(*)(tag);
       document.head.removeChild(tag);
       __gwtModuleFunction.installCode(code);
     }
   }-*/;

  private static native void setOnFailure(JavaScriptObject script,
      JavaScriptObject callback) /*-{
    var exception = @com.google.gwt.core.client.impl.CrossSiteLoadingStrategy::LoadTerminated;
    script.onerror = function() {
      callback(exception);
    };
    script.onload = function() {
      callback(exception);
    };
    script.onreadystatechange = function () {
      if (/loaded|complete/.test(script.readyState)) {
        callback(exception);
      }
    };
  }-*/;

  /**
   * Set the success callback for fragment <code>fragment</code>
   * to the supplied JavaScript function.
   */
  private static native void setOnSuccess(int fragment, JavaScriptObject callback) /*-{
    __gwtModuleFunction['runAsyncCallback'+fragment] = callback;
  }-*/;

  private final IntToIntMap serialNumbers = IntToIntMap.create();

  public void startLoadingFragment(int fragment,
      LoadTerminatedHandler loadFinishedHandler) {
    JavaScriptObject tag = createScriptTag(getUrl(fragment));
    setOnSuccess(fragment, removeTagAndEvalCode(fragment, tag));
    setOnFailure(tag, removeTagAndCallErrorHandler(fragment, tag,
        loadFinishedHandler));
    installScriptTag(tag);
  }

  protected String getDeferredJavaScriptDirectory() {
    return "deferredjs/";
  }

  private int getSerial(int fragment) {
    int ser = serialNumbers.get(fragment);
    serialNumbers.put(fragment, ser + 1);
    return ser;
  }

  /**
   * The URL to retrieve a fragment of code from. NOTE: this function is not
   * stable. It tweaks the URL with each call so that browsers are not tempted
   * to cache a download failure.
   */
  private String getUrl(int fragment) {
    return GWT.getModuleBaseURL() + getDeferredJavaScriptDirectory()
        + GWT.getPermutationStrongName() + "/" + fragment + ".cache.js?serial="
        + getSerial(fragment);
  }
}
