/*
  Copyright 2008 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.
*/

/* 
  JavaScript that makes it easy for publishers that have static content
  to work with Gears without delving into JavaScript. In particular, this
  file makes it possible to:
  * specify a manifest file by adding a new attribute to the HTML tag,
  'manifest', where you can point to a Gears manifest file either relative
  or absolute:
    <html gears-manifest="foobar.js">
  * specify a desktop shortcut icon to turn your site into a simple
  Site Specific Browser. You must indicate that you want a shortcut
  for your web application on the HTML tag:
    <html gears-manifest="foobar.js" shortcut="true"> 
  A shortcut icon consists of a name to give it
  on the desktop; a description; the URL to navigate to when clicked;
  and a collection of images in various sizes (16x16, 32x32, 48x48,
  128x128). Specify these using META and LINK tags:
    <meta name="shortcut.name" content="Test Application"></meta>
    <meta name="shortcut.description" 
      content="An application at http://www.test.com/index.html"></meta>
    <meta name="shortcut.url" content="http://www.test.com/index.html"></meta>
    
    <link rel="shortcut.icon" title="16x16" 
          href="http://www.test.com/icon16x16.png"></link>
    <link rel="shortcut.icon" title="32x32" 
                href="http://www.test.com/icon32x32.png"></link>
                
    Most of these have sensible defaults and can be left out:
    The 'shortcut' attribute on the HTML tag defaults to 'false' if not
    present.
    'shortcut.name' defaults to the HTML TITLE element if not present.
    'shortcut.url' defaults to the page's URL if not present.
    'shortcut.description' is optional and defaults to the string
    "Offline Web Application"
    You must have at least one 'shortcut.icon'.
  * embed a light-weight offline UI widget into your page to make it easy
  for users to take your page/site offline by creating an element
  with the ID "gears-pubtools-offline-widget". This will be filled in with
  an offline widget:
    <div id="gears-pubtools-offline-widget"></div>
    
  If you want to see debug messages on browsers and don't have FireBug
  installed, set 'debug' to true; it is off by default:
  
  <html debug="true">
  
  @author Brad Neuberg, http://codinginparadise.org
*/  
  
// pu is the PubTools utilities object used internally, defined in 
// pubtools-util.js  
  
pu.declare("PubTools", null, {
  constructor: function(){
    console.debug("pubtools constructor");
    pu.connect(pu, "loaded", this, this.ready_);
  },
  
  /*
    Goes offline, downloading offline resources and creating
    a desktop icon if it has not been created before.
  */
  downloadOffline: function() {
    if (!this.isGearsInstalled()) {
      // for apps that override default UI
      alert('Google Gears is not installed; you must install it before '
            + 'you can use this feature.');
      window.open('http://gears.google.com');
      return;
    }
    
    this.busy_('Downloading');
    this.doLocalServer_(
      pu.hitch(this, function(updateStatus, err) {
        if (updateStatus == 0) { // OK'
          this.stopBusy_();
          // on IE, Gears interacts with window.clearInterval() in a strange
          // way when createShortcut() is called; what happens is Gears
          // blocks the browser from continuing to execute when the
          // modal createShortcut() dialog displays, which causes
          // our clearInterval() call in stopBusy_() above to not execute
          // since there is a slight delay, which means our busy
          // messages collide and flash back and forth incorrectly.
          // As a workaround, we call the rest of the flow on a slight
          // timeout to allow clearInterval() in stopBusy_() to finish
          window.setTimeout(pu.hitch(this, function() {
            this.busy_('Creating desktop shortcut');
            this.doDesktopIcon_();
            this.stopBusy_();
          
            // toggle to Delete Offline action
            this.initOfflineWidget_(false);
          }), 1);
        } else if (updateStatus == 3) { // error
          this.stopBusy_();
          alert('Unable to download site offline:\n\n' + err);
          this.initOfflineWidget_(true);
        }
    }));
  },
  
  /** Deletes any offline resources. */
  deleteOffline: function() {
    if (!this.isGearsInstalled()) {
      // for apps that override default UI
      alert('Google Gears is not installed; you must install it before '
            + 'you can use this feature.');
      window.open('http://gears.google.com');
      return;
    }
    
    // get the resource store name
    var storeName = this.getResourceStoreName();
    
    var localServer;
    try {
      localServer = google.gears.factory.create('beta.localserver', '1.0');
      var store = localServer.openManagedStore(storeName);
      if (!store || store.currentVersion == '') {
        return; //  never created
      }
      
      this.busy_('Deleting offline pages');
      localServer.removeManagedStore(storeName);
      this.stopBusy_();
      
      // toggle to show 'Download Offline'
      this.initOfflineWidget_(true);
    } catch (exp) {
      this.stopBusy_();
      alert('Unable to delete offline: ' + exp);
    }
  },
  
  /** 
    Do we have any offline resources already? Returns true or false.
  */
  hasOfflineResources: function() {
    try {
      // get the resource store name
      var storeName = this.getResourceStoreName();
      var localServer = google.gears.factory.create('beta.localserver', '1.0');
      var store = localServer.openManagedStore(storeName);
      if (!store || store.currentVersion == '') {
        return false; //  never created
      }
      
      return true;
    } catch (exp) {
      console.debug('Exception detecting ' + storeName + ': ' + exp);
      return false;
    }
  },
  
  /* Do we even have Google Gears installed? */
  isGearsInstalled: function() {
    return !(typeof google == 'undefined');
  },
  
  /*
    Takes the title of the page's URL and turns into a
    ManagedResourceName.
  */
  getResourceStoreName: function() {
    var url = window.location.href;
    
    // FIXME: Remove anchor?
    
    // turn the following characters into underscores:
    // / \ : * ? " < > | ; , 
    url = url.replace(/\/|\\|:|\*|\?|\"|\<|\>|\||\;|\,/g, '_');
    
    // Gears has a 64 character limit
    url = url.substring(0, 63);

    return url;
  },
    
  /*
    Note that we do a trick. We _don't_ include this JavaScript file
    in a page's offline manifest. This means that when we are online,
    this JavaScript file is fetched and the "Download Offline" link
    is displayed. If we are offline, the page can't fetch this JavaScript,
    which causes nothing to be displayed -- the correct behavior since we
    don't want to display a "Download Offline" link when offline. This
    trick makes it possible for us to avoid a complicated system
    to detect on/offline state.
  */
  ready_: function() {
    console.debug("pubtools ready");
    var html = document.getElementsByTagName('html');
    if (!html || !html.length || html.length == 0) {
      return;
    }
    html = html[0];
    
    var manifest = html.getAttribute('gears-manifest');
    if (!manifest) {
      console.debug('Please give the URL to the manifest file on '
                    + 'the HTML tag with the "gears-manifest" attribute');
      return;
    }
    
    // print debug messages?
    var debug = (html.getAttribute('debug') == 'true') ? true : false;
    if (!debug && !console.firebug) { // leave Firebug alone
      console.debug = console.log = function(msg) {};
    }
    
    this.initOfflineWidget_();
  },
  
  /*
    Gets any shortcut info that this page might define.
  */
  getShortcutInfo_: function() {
    var enabled = false;
    var htmlElem = document.getElementsByTagName("html")[0];
    enabled = htmlElem.getAttribute("shortcut");
    if (typeof enabled == "undefined") {
      enabled = false;
    } else if (enabled == "true") {
      enabled = true;
    } else if (enabled == "false") {
      enabled = false;
    }
    
    var title = document.getElementsByTagName('title');
    if (title && title.length > 0 && title[0].childNodes 
        && title[0].childNodes.length) {
      title = title[0].childNodes[0].nodeValue;
    } else {
      title = 'foobar';
    }
    
    var info = {
      enabled: enabled,
      shortcutName: title,
      url: window.location.href, // FIXME: Remove anchor?
      description: 'Offline Web Application',
      icons: {},
      toString: function() { // for debugging
        var r = '{shortcutName: ' + this.shortcutName + ', '
                  + 'url: ' + this.url + ', '
                  + 'description: ' + this.description + ', '
                  + 'icons: {';
        for (var i in this.icons) {
          r += i + ': ' + this.icons[i] + ', ';
        } 
        
        r += '}}';
        return r;
      }
    };
    
    var metas = document.getElementsByTagName('meta');
    for (var i = 0; i < metas.length; i++) {
      var content = metas[i].getAttribute("content");
      switch (metas[i].getAttribute('name')) {
        case 'shortcut.name': 
          info.shortcutName = content;
          break;
        case 'shortcut.url':
          info.url = content;
          break;
        case 'shortcut.description':
          info.description = content;
          break;
      }
    }
    
    var links = document.getElementsByTagName('link');
    var iconGiven = false;
    for (var i = 0; i < links.length; i++) {
      if (links[i].getAttribute('rel') == 'shortcut.icon') {
        var iconSize = links[i].getAttribute('title');
        if (!iconSize) {
          console.debug('You must provide a "title" attribute for your '
                + 'shortcut icon.\n'
                + 'Example: <link rel="shortcut.icon" title="16x16" '
                + 'href="icon16x16.png"></link>');
          return;   
        }
        
        info.icons[iconSize] = links[i].getAttribute('href');
        iconGiven = true;
      }
    }
    
    // do we have everything we need?
    if (!info.enabled) {
      return info;
    } else {
      if (!info.shortcutName || !info.description
          || !info.url || !iconGiven) {
        console.debug('Invalid values for shortcut info');
        info.enabled = false;
        return info;
      }
    }
    
    // transform the shortcut name into a valid name
    // remove the following: "\/:*?<>|
    info.shortcutName = 
            info.shortcutName.replace(/\"|\/|\:|\*|\?|\<|\>|\|/g, '');
    
    return info;
  },
  
  /*
    Executes the local server and pulls down remote resources.
    
    @param callback : Function - Called with an updateStatus
    value from 0 to 4 (see Gears docs for LocalServer) and an
    error message if an error occurred.
  */
  doLocalServer_: function(callback) {
    // get the resource store name
    var storeName = this.getResourceStoreName();
    
    var localServer;
    try { 
      localServer = google.gears.factory.create('beta.localserver', '1.0');
    } catch (exp) {
      alert('You must allow this page to use Google Gears to use this '
            + 'feature');
      return;
    }
    var store = localServer.createManagedStore(storeName);
    
    // keep checking on status
    // FIXME: Gears really needs to be able to have explicit event handlers
    this.lastUpdateStatus_ = undefined;
    var interval = window.setInterval(
        pu.hitch(this, function() {
          if (store.updateStatus == 0 || store.updateStatus == 3) {
            window.clearInterval(interval); // finished
          }
          
          if (typeof store.updateStatus != "undefined"
              && store.updateStatus != this.lastUpdateStatus_) {
            this.lastUpdateStatus_ = store.updateStatus;
            var err = (store.updateStatus == 3) ? 
                        store.lastErrorMessage : null;
            callback(store.updateStatus, err);
          }
        }), 300);
        
    // FIXME: Remove anchor?
    var htmlElem = document.getElementsByTagName('html')[0];
    var url = htmlElem.getAttribute('gears-manifest');
    if (!url) {
      var msg = 'You must provide a "gears-manifest" attribute for your '
            + 'HTML tag to use this library.\n'
            + 'Example: <html gears-manifest="manifest.js">';
      callback(3, msg);
      return;
    }
    store.manifestUrl = url;
    
    store.checkForUpdate();
  },
  
  /*
    Adds a desktop shortcut icon for this web app.
  */
  doDesktopIcon_: function() {
    // get any desktop shortcut info that might be available
    var i = this.getShortcutInfo_();
    
    if (!i.enabled) {
      return;
    }
    
    var desktop;
    try {
      desktop = google.gears.factory.create('beta.desktop');
    } catch (exp) {
      console.debug('This version of Gears can not create desktop shortcuts');
      return;
    }
    
    desktop.createShortcut(i.shortcutName, i.url, i.icons, i.description);
  },
  
  /*
    Causes "..." busy characters to appear and disappear.
    
    TODO: Get rid of this method and just do a pu.byId().innerHTML
    call wherever folks want to change the message.
    
    @param msg : The message to display with busy dots afterwards.
  */
  busy_: function(msg) {
    if (!document.getElementById('gears-pubtools-offline-widget')) {
      return;
    }
    
    if (this.intervalID_ != null && this.intervalID_ != undefined) {
      this.stopBusy_();
    }
    
    this.interval_ = 0;
    var f = pu.hitch(this, function() {
      var widget = document.getElementById('gears-pubtools-offline-widget');
      var s = msg;
      for (var i = 0; i <= this.interval_; i++) {
        s += '.';
      }
      widget.innerHTML = s;
      
      // cycle through the dots
      this.interval_ = (++this.interval_ % 3);
    });
    this.intervalID_ = window.setInterval(f, 300);
  },
  
  /*
    Clears the interval handling the busy "..." characters.
    
    @param msg Either a String or a DOMElement to display
  */
  stopBusy_: function(msg) {
    var widget = document.getElementById('gears-pubtools-offline-widget');
    if (!this.intervalID_ || !widget) {
      return;
    }
    
    if (!msg) {
      msg = '';
    }
    
    window.clearInterval(this.intervalID_);
    this.intervalID_ = null;
    
    if (typeof msg == 'string') {
      widget.innerHTML = msg;
    } else {
      widget.innerHTML = '';
      widget.appendChild(msg);
    }
  },
  
  /*
    Creates the offline widget.
    
    @param isDownloadOffline : Boolean - controls what toggled state
    we show in the widget. If true, then we show 'Download Offline'
    which allows user to download content. If false, we show 'Delete Offline'
    which allows user to delete offlined resources. If undefined, then we
    figure out the correct state, such as when the page first loads.
  */
  initOfflineWidget_: function(isDownloadOffline) {
    var widget = document.getElementById('gears-pubtools-offline-widget');
    if (!widget) {
      return;
    }
    
    var a = document.createElement('a');
    a.href = '#';
    
    // do we even have Gears?
    if (!this.isGearsInstalled()) {
      a.innerHTML = 'Install Google Gears';
      a.onclick = pu.hitch(this, function() {
        window.open('http://gears.google.com');
        return false; // cancel default link action
      });
      
      widget.innerHTML = '';
      widget.appendChild(a);
      
      return;
    }
    
    if (isDownloadOffline == undefined) {
      // do we have offline resources?
      isDownloadOffline = !this.hasOfflineResources();
    }
    
    if (isDownloadOffline) {
      a.innerHTML = 'Download Offline';
      a.onclick = pu.hitch(this, function() {
        this.downloadOffline();
        return false; // cancel default link action
      });
    } else {
        a.onclick = pu.hitch(this, function() {
          this.deleteOffline();
          return false; // cancel default link action
        });
        a.innerHTML = 'Delete Offline';
    }
    
    widget.innerHTML = '';
    widget.appendChild(a);
  }
}); // end declare(PubTools)

var pubTools = new PubTools(); 
