// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2013 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0

package com.google.appinventor.client.wizards;

import static com.google.appinventor.client.Ode.MESSAGES;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

import com.google.appinventor.client.Ode;
import com.google.appinventor.client.OdeAsyncCallback;
import com.google.appinventor.client.explorer.project.Project;
import com.google.appinventor.client.settings.user.UserSettings;
import com.google.appinventor.client.tracking.Tracking;
import com.google.appinventor.client.wizards.NewProjectWizard.NewProjectCommand;
import com.google.appinventor.client.youngandroid.TextValidators;
import com.google.appinventor.shared.properties.json.JSONUtil;
import com.google.appinventor.shared.rpc.project.UserProject;
import com.google.appinventor.shared.rpc.project.youngandroid.NewYoungAndroidProjectParameters;
import com.google.appinventor.shared.rpc.project.youngandroid.YoungAndroidProjectNode;
import com.google.appinventor.shared.settings.SettingsConstants;
import com.google.gwt.cell.client.AbstractCell;
import com.google.gwt.dom.client.Style;
import com.google.gwt.event.dom.client.ChangeEvent;
import com.google.gwt.event.dom.client.ChangeHandler;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.http.client.Request;
import com.google.gwt.http.client.RequestBuilder;
import com.google.gwt.http.client.RequestCallback;
import com.google.gwt.http.client.RequestException;
import com.google.gwt.http.client.Response;
import com.google.gwt.json.client.JSONArray;
import com.google.gwt.json.client.JSONObject;
import com.google.gwt.json.client.JSONParser;
import com.google.gwt.json.client.JSONString;
import com.google.gwt.json.client.JSONValue;
import com.google.gwt.resources.client.ImageResource;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.user.cellview.client.CellList;
import com.google.gwt.user.cellview.client.HasKeyboardSelectionPolicy.KeyboardSelectionPolicy;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.ListBox;
import com.google.gwt.user.client.ui.SimplePanel;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.view.client.ProvidesKey;
import com.google.gwt.view.client.SelectionChangeEvent;
import com.google.gwt.view.client.SingleSelectionModel;

/**
 * Wizard for importing AI2 project from a server.  A 'template' is
 * a partially built project that is designed to provide 'scaffolding' for  a
 * lesson or tutorial.  It could take various forms -- e.g., just the media that
 * makes up an app, or just the components but no blocks, or just a "library" of
 * certain blocks, etc.
 *
 * The repositories can be either 'built-in' (stored on the appengine server) or
 * 'external', stored on any other Web server -- provided it is stored in the
 *  format described below.
 *
 * Built-in template repositories are hosted in appengine/war/templates/
 * That entire directory will be copied during build to: appengine/build/war/templates/
 *
 * External templates must be placed at a reachable URL on a web server.
 *
 * If templates are stored on a static website. Your webserver must
 * implement "Cross-origin Resource Sharing (CORS). With Apache you
 * can do this by using mod_headers placing a .htaccess file in the
 * templates directory that contains the one line:
 *
 * 'Header set Access-Control-Allow-Origin "*"'.
 *
 * External repositories can be added/removed by the user at runtime.
 *
 * To add a new repository, fill in the URL to the directory that
 * contains the templates direcory, for example
 * http://appinventor.cs.trincoll.edu/csp/week1/ where templates is
 * contained in week1.
 *
 * The Wizard assumes that the templates/ repository is organized as follows and uses
 * the naming conventions shown here, where 'HelloPurr' is a typical project.  If a
 * project is stored in /templates/Project it's .json and .asc and .aia files are
 * expected to be named Project.json, Project.aia, Project.asc:
 *
 *  /templates/HelloPurr/
 *  /templates/HelloPurr/HelloPurr.json  -- JSON description of the project
 *  /templates/HelloPurr/HelloPurr.aia -- the zip archive
 *  /templates/HelloPurr/HelloPurr.asc -- a base64 encoded version of the aia file
 *  /templates/HelloPurr/screenshot.png -- optional screenshot, specified in JSON file
 *  /templates/HelloPurr/thumbnail.png -- optional thumbnail, specified in JSON file
 *  /templates/SomeOtherProject/
 *  ...
 *
 * The .json file is used to construct the description of the project in the Wizard's
 * dialog:
 *
 *  {"name": "HelloPurr", "subtitle": "A purring kitty app", "description":
      "<p>This is App Inventor's version of the HelloWorld app. ...", "screenshot":
      "screenshot.png",  "thumbnail":"thumbnail.png" }
 *
 * The images are used in the templates summary that is displayed in  dialog when
 * the use selects a repository.
 *
 * The base64 encoded file is the one that the Wizard imports.
 */

public class TemplateUploadWizard extends Wizard implements NewUrlDialogCallback {
  // Project archive extension
  private static final String PROJECT_ARCHIVE_EXTENSION = ".zip";
  private static final String PROJECT_ARCHIVE_ENCODED_EXTENSION = ".asc";
  public static final String TEMPLATES_ROOT_DIRECTORY =  "templates/";
  public static final String URL_HOST = "";  // Default uses server as host, i.e., relative addr
  public static final String EXTERNAL_JSON_FILE = "templates.json";
  public static final String MIT_TEMPLATES = "Built-in Templates";
  public static final int MIT_TEMPLATES_INDEX = 1;
  public static final int TIMEOUT = 3000;  // 3 seconds

  /**
   * Reference to the instantiated Wizard. Reset to null when dialog
   * 'Ok' or 'Cancel' buttons are clicked.
   */
  private static TemplateUploadWizard instance;

  /**
   * Needed to retrieve existing templates from user settings
   *
   */
  private static UserSettings userSettings;

  /**
   * The current template host Url.
   */
  private String templateHostUrl = "";

  public void setTemplateUrlHost(String host) {
    templateHostUrl = host;
  }

  public String getTemplateUrlHost() {
    return templateHostUrl;
  }

  /**
   * Map of dynamic (i.e., not built-in) templates.
   */
  private static Map<String, ArrayList<TemplateInfo>> templatesMap =
    new HashMap<String, ArrayList<TemplateInfo>>();

  /**
   * Json representation of a template repository consisting of
   * one or more App Inventor projects.
   * Set from ProjectService.retrieveTemplateData
   */
  private static String templateDataString;

  /**
   * Keeps track of user-created (not built-in) repositories.
   */
  private static ArrayList<String> dynamicTemplateUrls = new ArrayList<String>();

  /**
   * Sets the dynamic template Urls from a jsonStr.  This method is
   * called during start up where jsonStr is retrieved from User
   * settings.
   *
   * @param jsonStr
   */
  public static void setStoredTemplateUrls(String jsonStr) {
    if (jsonStr == null || jsonStr.length() == 0)
      return;
    JSONValue jsonVal = JSONParser.parseLenient(jsonStr);
    JSONArray jsonArr = jsonVal.isArray();
    for (int i = 0; i < jsonArr.size(); i++) {
      JSONValue value = jsonArr.get(i);
      JSONString str = value.isString();
      dynamicTemplateUrls.add(str.stringValue());
    }
  }

  /**
   * Retrieves a Json string representing the Urls of dynamic
   * template repositories.  Called when saving UserSettings
   * during shutdown or otherwise.
   *
   * @return a Json string of repository Urls
   */
  public static String getStoredTemplateUrls() {
    String[] arr = new String[dynamicTemplateUrls.size()];
    for (int k = 0; k < arr.length; k++) {
      arr[k] = dynamicTemplateUrls.get(k);
    }
    return JSONUtil.toJson(arr);
  }

  /**
   * Returns true if hostUrl is already part of the template library.
   *
   */
  public static boolean hasUrl(String hostUrl) {
   return templatesMap.get(hostUrl) != null;
  }

  /**
   * A list of built-in templates -- typically the MIT repository.
   */
  private static ArrayList<TemplateInfo> builtInTemplates;

  /**
   * Initializes the built-in template repositories.
   *
   * Called by ProjectService.retrieveTemplateData, which passes
   *  a Json string describing the template. This variable is read
   *  when the user selects 'Upload Template' from the Projects Toolbar
   * @param json takes the form of a string:
   *
   * {"name": "HelloPurr",   "subtitle": "A purring kitty app", "description":
       "<p>This is App Inventor's version of the HelloWorld app. For more information
        see the <a href='http://appinventor.mit.edu/explore/content/hellopurr.html'
        target='_blank'>HelloPurr tutorial</a>.", "screenshot": "screenshot.png",
       "thumbnail":"thumbnail.png" }
  */
  public static void initializeBuiltInTemplates(String json) {
    templateDataString = json;
    builtInTemplates = getTemplates();
    templatesMap.put(MIT_TEMPLATES, builtInTemplates);
  }

  /**
   * UI Panel holding the template list.
   */
  private HorizontalPanel templatePanel;

  /**
   * UI Listbox of template Urls.
   */
  private ListBox templatesMenu;

  /**
   * UI Button for removing a template Url.
   */
  private Button removeButton;

  /**
   * Remembers the last template library selected.
   */
  private int lastSelectedIndex = MIT_TEMPLATES_INDEX;

  /**
   * Set to true when the user has selected an external template.
   */
  private boolean usingExternalTemplate = false;

  /**
   * Set to true when the user is inputting a new Url.
   */
  private boolean newUrlTestIsPending = false;

  /**
   * Stores the Url the user has input, which is not
   * added to the list of repositories until it is validated.
   */
  private String pendingUrl = "";

  /**
   * Stores the name of the template project selected by user.
   */
  private String selectedTemplateNAME = null;

  /**
   * Returns a list of Template objects containing data needed
   *  to load a template from a zip file. Each non-null template object
   *  is created from its Json string
   *
   * @return ArrayList of TemplateInfo objects
   */
  protected static ArrayList<TemplateInfo> getTemplates() {
    JSONValue jsonVal = JSONParser.parseLenient(templateDataString);
    JSONArray jsonArr = jsonVal.isArray();
    ArrayList<TemplateInfo> templates = new ArrayList<TemplateInfo>();
    for (int i = 0; i < jsonArr.size(); i++) {
      JSONValue value = jsonArr.get(i);
      JSONObject obj = value.isObject();
      if (obj != null)
        templates.add(new TemplateInfo(obj)); // Create TemplateInfo from Json
    }
    return templates;
  }

  /**
   * Creates a new project upload wizard. This is invoked either from ProjectToolbar,
   *  when the user chooses 'Import from repo' from the toolbar menu or from the
   *  retrieveExternalTemplateData's callback method, if an external template library
   *  is selected from the drop-down menu.
   *
   */
  public TemplateUploadWizard() {
    super(MESSAGES.templateUploadWizardCaption(), true, false); // modal, adaptive sizing
    instance = this;

    // Initialize the UI
    this.setStylePrimaryName("ode-DialogBox");
    setUpUiAndFinishCommand();
  }

  /**
   * Callback from the InputTemplateUrlWizard.  Part of NewUrlDialogCallback.
   * When newUrl is received an attempt is made to retrieve template data
   * from that address.
   *
   * @param newUrl the possibly invalid Url entered by the user.
   */
  @Override
  public void updateTemplateOptions(String newUrl) {
    if (newUrl.length() != 0) {
      newUrlTestIsPending = true;
      this.pendingUrl = newUrl;
      retrieveSelectedTemplates(newUrl);
    }
  }

  /**
   * Sets up the UI and calls the initFinish() method passing it the
   *  Command that's done when 'Ok' is clicked on the Wizard dialog.
   */
  private void setUpUiAndFinishCommand() {
    this.addPage(createUI(builtInTemplates));
    populateTemplateDialog(builtInTemplates);

    initCancelCommand(new Command() {
        @Override
        public void execute() {
          instance = null;
        }
      });

    // Create finish command (upload a project archive)
    initFinishCommand(new Command() {
        @Override
        public void execute() {
          String filename = selectedTemplateNAME + PROJECT_ARCHIVE_EXTENSION;
          // Make sure the project name is legal and unique.
          if (!TextValidators.checkNewProjectName(selectedTemplateNAME)) {
            center();
            return;
          }
          NewProjectCommand callbackCommand = new NewProjectCommand() {
              @Override
              public void execute(Project project) {
                Ode.getInstance().openYoungAndroidProjectInDesigner(project);
              }
            };

          createProjectFromExistingZip(selectedTemplateNAME, callbackCommand);

          Tracking.trackEvent(Tracking.PROJECT_EVENT, Tracking.PROJECT_ACTION_NEW_YA, filename);
          instance = null;
        }
      });
  }

  /**
   * The UI consists of a vertical panel that holds a drop-down list box,
   *   a Horizontal panel that holds the templates list (cell list) plus
   *   the selected template. This is inserted in the Wizard dialog.
   *
   * @param templates should never be null
   * @return the main panel for Wizard dialog.
   */
  VerticalPanel createUI(final ArrayList<TemplateInfo> templates) {
    VerticalPanel panel = new VerticalPanel();
    panel.setStylePrimaryName("gwt-SimplePanel");
    panel.setVerticalAlignment(VerticalPanel.ALIGN_MIDDLE);
    panel.setHorizontalAlignment(VerticalPanel.ALIGN_CENTER);

    templatePanel = new HorizontalPanel();
    templatePanel.add(makeTemplateSelector(templates));
    if (templates.size() > 0)
      templatePanel.add(new TemplateWidget(templates.get(0), templateHostUrl));

    templatesMenu = makeTemplatesMenu();

    HorizontalPanel hPanel = new HorizontalPanel();
    hPanel.add(templatesMenu);
    removeButton = new Button("Remove this repository", new ClickHandler() {
        @Override
        public void onClick(ClickEvent arg0) {
          removeCurrentlySelectedRepository();
        }
      });
    removeButton.setVisible(false);
    hPanel.add(removeButton);
    panel.add(hPanel);
    panel.add(templatePanel);
    return panel;
  }

  /**
   * Adds a new templates host to the list of available repositories.
   *
   * @param hostUrl
   * @param newTemplates
   */
  private static void addNewTemplateHost(String hostUrl, ArrayList<TemplateInfo> newTemplates) {
    templatesMap.put(hostUrl, newTemplates);

    // Display the templates dialog
    if (instance == null) {
      if (dynamicTemplateUrls.contains(hostUrl)) {
// Don't alert because we may be invoked via repo=... approach which
// can happen multiple times.
//        Window.alert("We already have that host " + hostUrl) ;
        instance = new TemplateUploadWizard();
        instance.setTemplateUrlHost(hostUrl);
        instance.populateTemplateDialog(newTemplates);
        instance.center();
        return;
      }
      instance = new TemplateUploadWizard();
      instance.setTemplateUrlHost(hostUrl);
      instance.updateTemplateOptions(hostUrl);
      instance.center();
    } else {
      instance.populateTemplateDialog(newTemplates);
    }

    // Update the user settings
    UserSettings settings = Ode.getUserSettings();
    settings.getSettings(SettingsConstants.USER_GENERAL_SETTINGS).
      changePropertyValue(SettingsConstants.USER_TEMPLATE_URLS,
        TemplateUploadWizard.getStoredTemplateUrls());
    settings.saveSettings(null);
  }

  /**
   * Removes a template repository.
   */
  private void removeCurrentlySelectedRepository() {
    boolean ok = Window.confirm("Are you sure you want to remove this repository? " +
      "Click cancel to abort.");
    if (ok) {
      dynamicTemplateUrls.remove(templateHostUrl);
      templatesMap.remove(templateHostUrl);
      templatesMenu.removeItem(lastSelectedIndex);
      templatesMenu.setSelectedIndex(1);
      templatesMenu.setItemSelected(1, true);
      removeButton.setVisible(false);
      retrieveSelectedTemplates(templatesMenu.getValue(1));

      // Update the user settings
      UserSettings settings = Ode.getUserSettings();
      settings.getSettings(SettingsConstants.USER_GENERAL_SETTINGS).
        changePropertyValue(SettingsConstants.USER_TEMPLATE_URLS,
          TemplateUploadWizard.getStoredTemplateUrls());
      settings.saveSettings(null);
    }
  }

  /**
   * Creates a drop down menu for selecting Template repositories.
   * @return the drop down menu of repository Urls.
   */
  private ListBox makeTemplatesMenu() {
    final ListBox templatesMenu = new ListBox();
    templatesMenu.addItem(MESSAGES.templateUploadNewUrlCaption());
    templatesMenu.addItem(MIT_TEMPLATES);

    for (int k = 0; k < dynamicTemplateUrls.size(); k++) { // Dynamically added Urls
      templatesMenu.addItem(dynamicTemplateUrls.get(k));
    }
    templatesMenu.setSelectedIndex(MIT_TEMPLATES_INDEX);
    templatesMenu.addChangeHandler(new ChangeHandler() {
        public void onChange(ChangeEvent event) {
          int selectedIndex = templatesMenu.getSelectedIndex();

          if (selectedIndex == 0) {
            templatesMenu.setSelectedIndex(lastSelectedIndex);
            usingExternalTemplate = true; // MIT templates at index 1
            removeButton.setVisible(false);
            new InputTemplateUrlWizard(instance).center();     // This will do a callback
          } else if (selectedIndex == 1) {
            removeButton.setVisible(false);          lastSelectedIndex = selectedIndex;
            usingExternalTemplate = false; // MIT templates at index 1
            templateHostUrl = "";
            retrieveSelectedTemplates(templatesMenu.getValue(selectedIndex));  // may do callback
          } else {
            removeButton.setVisible(true);         lastSelectedIndex = selectedIndex;
            usingExternalTemplate = true; // MIT templates at index 1
            templateHostUrl = templatesMenu.getValue(selectedIndex);
            retrieveSelectedTemplates(templatesMenu.getValue(selectedIndex));  // may do callback
          }
        }
      });
    templatesMenu.setVisibleItemCount(1);  // Turns menu into a drop-down list).
    return templatesMenu;
  }

  /**
   * Retrieves the templates associated with a particular template repository.
   * Called when the user selects a template library from the drop-down box.
   * If the templates are already stored in the templates map, we just
   * refresh the Wizard's dialog. Otherwise we retrieve the templates
   * from the external hostUrl.
   *
   * @param hostUrl url of an external templates host -- e.g., 'http://localhost:85/'
   */
  void retrieveSelectedTemplates(String hostUrl) {
    ArrayList<TemplateInfo> templates = templatesMap.get(hostUrl);
    if (templates == null) {
      TemplateUploadWizard.retrieveExternalTemplateData(hostUrl);
    } else {
      populateTemplateDialog(templates);
    }
  }

  /**
   * Loads templates into the templatePanel, which consists of a
   *  clickable list widget of templates and a widget that displays
   *  a summary of the list's current selection.
   *
   * @param templates
   */
  void populateTemplateDialog(ArrayList<TemplateInfo> templates) {
    String hostUrl = "";

    // Validity check for user-entered Url.
    if (this.newUrlTestIsPending) {
      newUrlTestIsPending = false;
      hostUrl = pendingUrl;
      pendingUrl = "";
      if (templates != null) {
        dynamicTemplateUrls.add(hostUrl);
        templatesMenu.addItem(hostUrl);
        templatesMenu.setSelectedIndex(templatesMenu.getItemCount()-1);  // Last item
        lastSelectedIndex = templatesMenu.getSelectedIndex();
        usingExternalTemplate = true;
        templateHostUrl = templatesMenu.getValue(lastSelectedIndex);
      } else {
        return;
      }
    }

    if (templates == null)
      return;

    // Display the templates for the the selected Url.
    for (int k = 0; k < templatePanel.getWidgetCount(); k++) {
      templatePanel.getWidget(k).removeFromParent();
    }
    VerticalPanel parent = (VerticalPanel) templatePanel.getParent();
    templatePanel.removeFromParent();
    templatePanel = new HorizontalPanel();
    // Add the new templates
    templatePanel.add(makeTemplateSelector(templates));
    if (templates.size() > 0)
      templatePanel.add(new TemplateWidget(templates.get(0), templateHostUrl));
    parent.add(templatePanel);
  }

  /**
   * Creates a new project from a Zip file and lists it in the ProjectView.
   *
   * @param projectName project name
   * @param onSuccessCommand command to be executed after process creation
   *   succeeds (can be {@code null})
   */
  public void createProjectFromExistingZip(final String projectName,
    final NewProjectCommand onSuccessCommand) {

    // Callback for updating the project explorer after the project is created on the back-end
    final Ode ode = Ode.getInstance();
    final OdeAsyncCallback<UserProject> callback = new OdeAsyncCallback<UserProject>(
      // failure message
      MESSAGES.createProjectError()) {
      @Override
      public void onSuccess(UserProject projectInfo) {
        // Update project explorer -- i.e., display in project view
        if (projectInfo == null) {

          Window.alert("This template has no aia file. Creating a new project with name = " + projectName);
          ode.getProjectService().newProject(
            YoungAndroidProjectNode.YOUNG_ANDROID_PROJECT_TYPE,
            projectName,
            new NewYoungAndroidProjectParameters(projectName),
            this);
          return;
        }
        Project project = ode.getProjectManager().addProject(projectInfo);
        if (onSuccessCommand != null) {
          onSuccessCommand.execute(project);
        }
      }
    };

    // Use project RPC service to create the project on back end using
    String pathToZip = "";
    if (usingExternalTemplate) {
      String zipUrl = templateHostUrl + TEMPLATES_ROOT_DIRECTORY + projectName + "/" +
        projectName + PROJECT_ARCHIVE_ENCODED_EXTENSION;
      RequestBuilder builder = new RequestBuilder(RequestBuilder.GET, zipUrl);
     try {
        Request response = builder.sendRequest(null, new RequestCallback() {
            @Override
            public void onError(Request request, Throwable exception) {
              Window.alert("Unable to load Project Template Data");
            }
            @Override
            public void onResponseReceived(Request request, Response response) {
              ode.getProjectService().newProjectFromExternalTemplate(projectName,response.getText(),callback);
            }

          });
      } catch (RequestException e) {
        Window.alert("Error fetching project zip file template.");
      }
    } else {
      pathToZip = TEMPLATES_ROOT_DIRECTORY + projectName + "/" + projectName +
        PROJECT_ARCHIVE_EXTENSION;
      ode.getProjectService().newProjectFromTemplate(projectName, pathToZip, callback);
    }
  }

  /**
   * Called from Ode when a template Url is passed as GET parameter.
   *  The Url could take two forms:
   *  1. appinventor.cs.trincoll.edu/templates/Project/Project.asc
   *     This is a Base64 encoded AI project. In this case the project should be opened.
   *  2. appinventor.cs.trincoll.edu/templates/  or .../templates
   *     This is a repository with 0 or more templates. They should be
   *     loaded into the client and displaye in the Templates Dialog.
   * @param url the template's Url
   * @param onSuccessCommand command to open the project
   */
  public static void openProjectFromTemplate(String url, final NewProjectCommand onSuccessCommand) {
    if(!url.startsWith("http")) {
      url = "http://" + url;
    }
    if (url.endsWith(".asc")) {
      openTemplateProject(url, onSuccessCommand);
    } else  {
      retrieveExternalTemplateData(url);
    }
  }

  /**
   * Helper method for opening a project given its Url
   * @param url A string of the form "http://... .asc
   * @param onSuccessCommand
   */
  private static void openTemplateProject(final String url, final NewProjectCommand onSuccessCommand) {
    final Ode ode = Ode.getInstance();

    // This Async callback is called after the project is input and created
    final OdeAsyncCallback<UserProject> callback = new OdeAsyncCallback<UserProject>(
        // failure message
        MESSAGES.createProjectError()) {
      @Override
      public void onSuccess(UserProject projectInfo) {
        // This just adds the new project to the project manager, not to AppEngine
        Project project = ode.getProjectManager().addProject(projectInfo);
        // And this opens the project
        if (onSuccessCommand != null) {
          onSuccessCommand.execute(project);
        }
      }
    };

    final String projectName;
    if (url.endsWith(".asc")) {
      projectName = url.substring(1 + url.lastIndexOf("/"), url.lastIndexOf("."));
    } else {
      return;
    }

    // If project of the same name already exists, just open it
    if (!TextValidators.checkNewProjectName(projectName)) {
      Project project = ode.getProjectManager().getProject(projectName);
      if (onSuccessCommand != null) {
        onSuccessCommand.execute(project);
      }
     return;   // Don't retrieve the template if the project is a duplicate
    }

    // Here's where we retrieve the template data
    // Do a GET to retrieve data at url
    RequestBuilder builder = new RequestBuilder(RequestBuilder.GET, url);
    try {
      Request response = builder.sendRequest(null, new RequestCallback() {
        @Override
        public void onError(Request request, Throwable exception) {
          Window.alert("Unable to load Project Template Data");
        }

        // Response received from the GET
        @Override
        public void onResponseReceived(Request request, Response response) {
          // The response.getText is the zip data used to create a new project.
          // The callback opens the project
          if (response != null && response.getStatusCode() == 200 && response.getText() != null && !response.getText().isEmpty()) {
            ode.getProjectService().newProjectFromExternalTemplate(projectName, response.getText(), callback);
          } else if (url.startsWith("http:")) {
            openTemplateProject(url.replaceFirst("http:", "https:"), onSuccessCommand);
          }
        }
      });
    } catch (RequestException e) {
      Window.alert("Error fetching template file.");
    }
  }

  /**
   * A class to stores template details.
   *
   */
  public static class TemplateInfo { // implements Comparable<TemplateInfo> {
    public String name;
    public String subtitle;
    public String description;
    public String thumbStr;        // thumbStr and/or screenshotStr can be ""
    public String screenshotStr;
    public ImageResource thumbnail;
    public ImageResource screenshot;

    /**
     * The key provider that provides the unique ID of a template, its name.
     */
    public static final ProvidesKey<TemplateInfo> KEY_PROVIDER = new ProvidesKey<TemplateInfo>() {
      @Override
      public Object getKey(TemplateInfo item) {
        return item == null ? null : item.name;
      }
    };

    /**
     * Default constructor
     */
    public TemplateInfo() {
    }

    public TemplateInfo(String name, String subtitle, String description, String screenshot, String thumbnail) {
      this.name = name;
      this.subtitle = subtitle;
      this.description = description;
      this.screenshotStr = screenshot;
      this.thumbStr = thumbnail;
    }

    /**
     * Builds the TemplateInfo object from JSON
     * @param value
     */
    public TemplateInfo(JSONObject value) {
      this.name = value.get("name").toString();
      this.name = this.name.substring(1, this.name.length() -1);
      this.subtitle = value.get("subtitle").toString();
      this.subtitle = this.subtitle.substring(1, this.subtitle.length() -1);
      this.description = value.get("description").toString();
      this.description = this.description.substring(1, this.description.length() -1);
      this.thumbStr = value.get("thumbnail").toString();
      this.thumbStr = this.thumbStr.substring(1, this.thumbStr.length() -1);
      this.screenshotStr = value.get("screenshot").toString();
      this.screenshotStr = this.screenshotStr.substring(1, this.screenshotStr.length() -1);
    };
  }

  /**
   * A composite widget for displaying a template.
   */
  public static class TemplateWidget extends Composite {
    private static Label title = new Label();
    private static Label subtitle = new Label();
    private static Image image = new Image();
    private static HTML descriptionHtml = new HTML();
    private VerticalPanel panel;

    public TemplateWidget(TemplateInfo info, String hostUrl) {
      setTemplate(info, hostUrl);

      panel = new VerticalPanel();
      panel.add(title);
      title.getElement().getStyle().setFontWeight(Style.FontWeight.BOLD);
      panel.add(subtitle);
      descriptionHtml.setHTML(info.description);
      panel.add(descriptionHtml);
      panel.add(image);
      SimplePanel wrapper = new SimplePanel();
      wrapper.getElement().getStyle().setOverflowY(Style.Overflow.SCROLL);
      wrapper.add(panel);
      initWidget(wrapper);
      setStylePrimaryName("ode-ContextMenu");
      setHeight("355px");
    }

    public static void setTemplate(TemplateInfo info, String hostUrl) {
      title.setText(info.name);
      subtitle.setText(info.subtitle);
      descriptionHtml.setHTML(info.description);

      if (! info.screenshotStr.equals("")) {
        String url = hostUrl + TEMPLATES_ROOT_DIRECTORY + info.name + "/" + info.screenshotStr;
        image.setUrl(url);
      } else {
        TemplateWidget.image.setResource(Ode.getImageBundle().appInventorLogo());
      }
      image.setWidth("240px");
      image.setHeight("400px");

      // Display the screenshot if available
      if (! info.screenshotStr.equals("")) {
        String url = hostUrl + TEMPLATES_ROOT_DIRECTORY + info.name + "/" + info.screenshotStr;
        image.setUrl(url);
      }
    }
  }

  /**
   * A Cell widget for displaying a summary of a template.
   *
   */
  public static class TemplateCell extends AbstractCell<TemplateInfo> {

    public TemplateInfo info;
    private String hostUrl;

    public TemplateCell(TemplateInfo info, String hostUrl) {
      this.info = info;
      this.hostUrl = hostUrl;
    }

    @Override
      public void render(Context context, TemplateInfo template, SafeHtmlBuilder sb) {
      if (template == null)
        return;
      sb.appendHtmlConstant("<table style='margin: 4pt 0;'>");

      // Add the thumbnail image, if available, or a default image.
      sb.appendHtmlConstant("<tr><td rowspan='3' width=\"32px\">");
      if ( !template.thumbStr.equals("") )   {
        String src = hostUrl + TEMPLATES_ROOT_DIRECTORY +   template.name + "/" + template.thumbStr;
        sb.appendHtmlConstant("<img style='width:32px' src='" + src + "'>");
      } else {
        ImageResource imgResource = Ode.getImageBundle().appInventorLogo();
        Image img = new Image(imgResource);
        String url = img.getUrl();
        sb.appendHtmlConstant("<img style='width:32px' src='" + url + "'>");
      }
      sb.appendHtmlConstant("</td>");

      // Add the name and description.
      sb.appendHtmlConstant("<td style='font-size:95%;'><b>");
      sb.appendEscaped(template.name);
      sb.appendHtmlConstant("</b></td></tr><tr><td>");
      sb.appendEscaped(template.subtitle);
      sb.appendHtmlConstant("</td></tr></table>");
    }
  }

  /**
   * Creates the scrollable list of cells each of which serves as a link to a template.
   *
   * @param list an ArrayList of TemplateInfo
   * @return A CellList widget
   */
  public CellList<TemplateInfo> makeTemplateSelector(ArrayList<TemplateInfo> list) {
    TemplateCell templateCell = new TemplateCell(list.get(0), templateHostUrl);

    CellList<TemplateInfo> templateCellList = new CellList<TemplateInfo>(templateCell,TemplateInfo.KEY_PROVIDER);
    templateCellList.setPageSize(list.size() + 10);
    templateCellList.setKeyboardSelectionPolicy(KeyboardSelectionPolicy.ENABLED);
    templateCellList.setWidth("250px");
    templateCellList.setHeight("355px");
    templateCellList.setVisible(true);
    templateCellList.getElement().getStyle().setOverflowY(Style.Overflow.SCROLL);

    // Add a selection model to handle user selection.
    final SingleSelectionModel<TemplateInfo> selectionModel =
      new SingleSelectionModel<TemplateInfo>(TemplateInfo.KEY_PROVIDER);
    templateCellList.setSelectionModel(selectionModel);
    selectionModel.setSelected(list.get(0), true);
    final TemplateUploadWizard wizard = this;
    selectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() {
        public void onSelectionChange(SelectionChangeEvent event) {
          TemplateInfo selected = selectionModel.getSelectedObject();
          if (selected != null) {
            selectedTemplateNAME = selected.name;
            TemplateWidget.setTemplate(selected, wizard.getTemplateUrlHost());
          }
        }
      });

    // Set the total row count. This isn't strictly necessary, but it affects
    // paging calculations, so its good habit to keep the row count up to date.
    templateCellList.setRowCount(list.size(), true);

    // Push the data into the widget.
    templateCellList.setRowData(0, list);
    return templateCellList;
  }

  /**
   * Display the UploadTemplate dialog.
   */
  @Override
  public void show() {
    super.show();
    // Wizard size (having it resize between page changes is quite annoying)
    int width = 640;
    int height = 458;
    this.center();

    setPixelSize(width, height);
    super.setPagePanelHeight(400);
  }

  /**
   * Called from ProjectToolbar when user selects a set of external templates. It uses
   *  JsonP to retrieve a json file from an external server.
   *
   * @param hostUrl, Url of the host -- e.g., http://localhost:85/
   */
  public static void retrieveExternalTemplateData(final String hostUrl) {
    String url = hostUrl +  TEMPLATES_ROOT_DIRECTORY + EXTERNAL_JSON_FILE;

    RequestBuilder builder = new RequestBuilder(RequestBuilder.GET, url);
    try {
      Request response = builder.sendRequest(null, new RequestCallback() {
          @Override
          public void onError(Request request, Throwable exception) {
            Window.alert("Unable to load Project Template Data.");
            if (instance != null) {
              instance.populateTemplateDialog(null);
            }
          }
          @Override
          public void onResponseReceived(Request request, Response response) {
            if (response.getStatusCode() != Response.SC_OK) {
              Window.alert("Unable to load Project Template Data.");
              return;
            }

            ArrayList<TemplateInfo> externalTemplates = new ArrayList<TemplateInfo>();

            JSONValue jsonVal = JSONParser.parseLenient(response.getText());
            JSONArray jsonArr = jsonVal.isArray();

            for(int i = 0; i < jsonArr.size(); i++) {
              JSONValue entry1 = jsonArr.get(i);
              JSONObject entry = entry1.isObject();
              externalTemplates.add(
                new TemplateInfo(entry.get("name").isString().stringValue(),
                  entry.get("subtitle").isString().stringValue(),
                  entry.get("description").isString().stringValue(),
                  entry.get("screenshot").isString().stringValue(),
                  entry.get("thumbnail").isString().stringValue()));
            }
            if (externalTemplates.size() == 0) {
              Window.alert("Unable to retrieve templates for host = " + hostUrl + ".");
              return;
            }
            addNewTemplateHost(hostUrl, externalTemplates);
          }
        });
    } catch (RequestException e) {
      Window.alert("Error fetching external template.");
    }
  }

}