// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

var MAX_APPS_PER_ROW = [];
MAX_APPS_PER_ROW[LayoutMode.SMALL] = 4;
MAX_APPS_PER_ROW[LayoutMode.NORMAL] = 6;

function getAppsCallback(data) {
  logEvent('received apps');

  // In the case of prefchange-triggered updates, we don't receive this flag.
  // Just leave it set as it was before in that case.
  if ('showPromo' in data)
    apps.showPromo = data.showPromo;

  var appsSection = $('apps');
  var appsSectionContent = $('apps-content');
  var appsMiniview = appsSection.getElementsByClassName('miniview')[0];
  var appsPromo = $('apps-promo');
  var appsPromoLink = $('apps-promo-link');
  var appsPromoPing = APP_LAUNCH_URL.PING_WEBSTORE + '+' + apps.showPromo;
  var webStoreEntry, webStoreMiniEntry;

  // Hide menu options that are not supported on the OS or windowing system.

  // The "Launch as Window" menu option.
  $('apps-launch-type-window-menu-item').hidden = data.disableAppWindowLaunch;

  // The "Create App Shortcut" menu option.
  $('apps-create-shortcut-command-menu-item').hidden =
      $('apps-create-shortcut-command-separator').hidden =
          data.disableCreateAppShortcut;

  // Hide the context menu, if there is any open.
  cr.ui.contextMenuHandler.hideMenu();

  appsMiniview.textContent = '';
  appsSectionContent.textContent = '';

  data.apps.sort(function(a,b) {
    return a.app_launch_index - b.app_launch_index;
  });

  // Determines if the web store link should be detached and place in the
  // top right of the screen.
  apps.detachWebstoreEntry =
      !apps.showPromo && data.apps.length >= MAX_APPS_PER_ROW[layoutMode];

  markNewApps(data.apps);
  apps.data = data.apps;

  clearClosedMenu(apps.menu);

  // We wait for the app icons to load before displaying them, but never wait
  // longer than 200ms.
  apps.loadedImages = 0;
  apps.imageTimer = setTimeout(apps.showImages.bind(apps), 200);

  data.apps.forEach(function(app) {
    appsSectionContent.appendChild(apps.createElement(app));
  });

  if (data.showPromo) {
    // Add the promo content...
    $('apps-promo-heading').textContent = data.promoHeader;
    appsPromoLink.href = data.promoLink;
    appsPromoLink.textContent = data.promoButton;
    appsPromoLink.ping = appsPromoPing;
    $('apps-promo-hide').textContent = data.promoExpire;

    // ... then display the promo.
    document.documentElement.classList.add('apps-promo-visible');
  } else {
    document.documentElement.classList.remove('apps-promo-visible');
  }

  // Only show the web store entry if there are apps installed, since the promo
  // is sufficient otherwise.
  if (data.apps.length > 0) {
    webStoreEntry = apps.createWebStoreElement();
    webStoreEntry.querySelector('a').ping = appsPromoPing;
    appsSectionContent.appendChild(webStoreEntry);
    if (apps.detachWebstoreEntry) {
      webStoreEntry.classList.add('loner');
    } else {
      webStoreEntry.classList.remove('loner');
      apps.data.push('web-store-entry');
    }
  }

  data.apps.slice(0, MAX_MINIVIEW_ITEMS).forEach(function(app) {
    appsMiniview.appendChild(apps.createMiniviewElement(app));
    addClosedMenuEntryWithLink(apps.menu, apps.createClosedMenuElement(app));
  });
  if (data.apps.length < MAX_MINIVIEW_ITEMS) {
    webStoreMiniEntry = apps.createWebStoreMiniElement();
    webStoreMiniEntry.querySelector('a').ping = appsPromoPing;
    appsMiniview.appendChild(webStoreMiniEntry);
    addClosedMenuEntryWithLink(apps.menu,
                               apps.createWebStoreClosedMenuElement());
  }

  if (!data.showLauncher)
    hideSection(Section.APPS);
  else
    appsSection.classList.remove('disabled');

  addClosedMenuFooter(apps.menu, 'apps', MENU_APPS, Section.APPS);

  apps.loaded = true;

  if (appsPromoLink)
    appsPromoLink.ping = appsPromoPing;
  maybeDoneLoading();

  // Disable the animations when the app launcher is being (re)initailized.
  apps.layout({disableAnimations:true});

  if (isDoneLoading()) {
    updateMiniviewClipping(appsMiniview);
    layoutSections();
  }
}

function markNewApps(data) {
  var oldData = apps.data;
  data.forEach(function(app) {
    if (hashParams['app-id'] == app['id']) {
      delete hashParams['app-id'];
      app.isNew = true;
    } else if (oldData &&
        !oldData.some(function(id) { return id == app.id; })) {
      app.isNew = true;
    } else {
      app.isNew = false;
    }
  });
}

function appsPrefChangeCallback(data) {
  // Currently the only pref that is watched is the launch type.
  data.apps.forEach(function(app) {
    var appLink = document.querySelector('.app a[app-id=' + app['id'] + ']');
    if (appLink)
      appLink.setAttribute('launch-type', app['launch_type']);
  });
}

// Launches the specified app using the APP_LAUNCH_NTP_APP_RE_ENABLE histogram.
// This should only be invoked from the AppLauncherHandler.
function launchAppAfterEnable(appId) {
  chrome.send('launchApp', [appId, APP_LAUNCH.NTP_APP_RE_ENABLE]);
}

var apps = (function() {

  function createElement(app) {
    var div = document.createElement('div');
    div.className = 'app';

    var a = div.appendChild(document.createElement('a'));
    a.setAttribute('app-id', app['id']);
    a.setAttribute('launch-type', app['launch_type']);
    a.draggable = false;
    a.xtitle = a.textContent = app['name'];
    a.href = app['launch_url'];

    return div;
  }

  /**
   * Launches an application.
   * @param {string} appId Application to launch.
   * @param {MouseEvent} opt_mouseEvent Mouse event from the click that
   *     triggered the launch, used to detect modifier keys that change
   *     the tab's disposition.
   */
  function launchApp(appId, opt_mouseEvent) {
    var args = [appId, getAppLaunchType()];
    if (opt_mouseEvent) {
      // Launch came from a click - add details of the click
      // Otherwise it came from a 'command' event from elsewhere in the UI.
      args.push(opt_mouseEvent.altKey, opt_mouseEvent.ctrlKey,
                opt_mouseEvent.metaKey, opt_mouseEvent.shiftKey,
                opt_mouseEvent.button);
    }
    chrome.send('launchApp', args);
  }

  function isAppSectionMaximized() {
    return getAppLaunchType() == APP_LAUNCH.NTP_APPS_MAXIMIZED &&
      !$('apps').classList.contains('disabled');
  }

  function isAppsMenu(node) {
    return node.id == 'apps-menu';
  }

  function getAppLaunchType() {
    // We determine if the apps section is maximized, collapsed or in menu mode
    // based on the class of the apps section.
    if ($('apps').classList.contains('menu'))
      return APP_LAUNCH.NTP_APPS_MENU;
    else if ($('apps').classList.contains('collapsed'))
      return APP_LAUNCH.NTP_APPS_COLLAPSED;
    else
      return APP_LAUNCH.NTP_APPS_MAXIMIZED;
  }

  /**
   * @this {!HTMLAnchorElement}
   */
  function handleClick(e) {
    var appId = e.currentTarget.getAttribute('app-id');
    if (!appDragAndDrop.isDragging())
      launchApp(appId, e);
    return false;
  }

  // Keep in sync with LaunchType in extension_prefs.h
  var LaunchType = {
    LAUNCH_PINNED: 0,
    LAUNCH_REGULAR: 1,
    LAUNCH_FULLSCREEN: 2,
    LAUNCH_WINDOW: 3
  };

  // Keep in sync with LaunchContainer in extension_constants.h
  var LaunchContainer = {
    LAUNCH_WINDOW: 0,
    LAUNCH_PANEL: 1,
    LAUNCH_TAB: 2
  };

  var currentApp;
  var promoHasBeenSeen = false;

  function addContextMenu(el, app) {
    el.addEventListener('contextmenu', cr.ui.contextMenuHandler);
    el.addEventListener('keydown', cr.ui.contextMenuHandler);
    el.addEventListener('keyup', cr.ui.contextMenuHandler);

    Object.defineProperty(el, 'contextMenu', {
      get: function() {
        currentApp = app;

        $('apps-launch-command').label = app['name'];
        $('apps-options-command').canExecuteChange();

        var launchTypeEl;
        if (el.getAttribute('app-id') === app['id']) {
          launchTypeEl = el;
        } else {
          appLinkSel = 'a[app-id=' + app['id'] + ']';
          launchTypeEl = el.querySelector(appLinkSel);
        }

        var launchType = launchTypeEl.getAttribute('launch-type');
        var launchContainer = app['launch_container'];
        var isPanel = launchContainer == LaunchContainer.LAUNCH_PANEL;

        // Update the commands related to the launch type.
        var launchTypeIds = ['apps-launch-type-pinned',
                             'apps-launch-type-regular',
                             'apps-launch-type-fullscreen',
                             'apps-launch-type-window'];
        launchTypeIds.forEach(function(id) {
          var command = $(id);
          command.disabled = isPanel;
          command.checked = !isPanel &&
              launchType == command.getAttribute('launch-type');
        });

        return $('app-context-menu');
      }
    });
  }

  document.addEventListener('command', function(e) {
    if (!currentApp)
      return;

    var commandId = e.command.id;
    switch (commandId) {
      case 'apps-options-command':
        window.location = currentApp['options_url'];
        break;
      case 'apps-launch-command':
        launchApp(currentApp['id']);
        break;
      case 'apps-uninstall-command':
        chrome.send('uninstallApp', [currentApp['id']]);
        break;
      case 'apps-create-shortcut-command':
        chrome.send('createAppShortcut', [currentApp['id']]);
        break;
      case 'apps-launch-type-pinned':
      case 'apps-launch-type-regular':
      case 'apps-launch-type-fullscreen':
      case 'apps-launch-type-window':
        chrome.send('setLaunchType',
            [currentApp['id'],
             Number(e.command.getAttribute('launch-type'))]);
        break;
    }
  });

  document.addEventListener('canExecute', function(e) {
    switch (e.command.id) {
      case 'apps-options-command':
        e.canExecute = currentApp && currentApp['options_url'];
        break;
      case 'apps-launch-command':
        e.canExecute = true;
        break;
      case 'apps-uninstall-command':
        e.canExecute = !currentApp['can_uninstall'];
        break;
    }
  });

  // Moves the element at position |from| in array |arr| to position |to|.
  function arrayMove(arr, from, to) {
    var element = arr.splice(from, 1);
    arr.splice(to, 0, element[0]);
  }

  // The autoscroll rate during drag and drop, in px per second.
  var APP_AUTOSCROLL_RATE = 400;

  return {
    loaded: false,

    menu: $('apps-menu'),

    showPromo: false,

    detachWebstoreEntry: false,

    scrollMouseXY_: null,

    scrollListener_: null,

    // The list of app ids, in order, of each app in the launcher.
    data_: null,
    get data() { return this.data_; },
    set data(data) {
      this.data_ = data.map(function(app) {
        return app.id;
      });
      this.invalidate_();
    },

    dirty_: true,
    invalidate_: function() {
      this.dirty_ = true;
    },

    visible_: true,
    get visible() {
      return this.visible_;
    },
    set visible(visible) {
      this.visible_ = visible;
      this.invalidate_();
    },

    maybePingPromoSeen_: function() {
      if (promoHasBeenSeen || !this.showPromo || !isAppSectionMaximized())
        return;

      promoHasBeenSeen = true;
      chrome.send('promoSeen', []);
    },

    // DragAndDropDelegate

    dragContainer: $('apps-content'),
    transitionsDuration: 200,

    get dragItem() { return this.dragItem_; },
    set dragItem(dragItem) {
      if (this.dragItem_ != dragItem) {
        this.dragItem_ = dragItem;
        this.invalidate_();
      }
    },

    // The dimensions of each item in the app launcher.
    dimensions_: null,
    get dimensions() {
      if (this.dimensions_)
        return this.dimensions_;

      var width = 124;
      var height = 136;

      var marginWidth = 6;
      var marginHeight = 10;

      var borderWidth = 0;
      var borderHeight = 0;

      this.dimensions_ = {
        width: width + marginWidth + borderWidth,
        height: height + marginHeight + borderHeight
      };

      return this.dimensions_;
    },

    // Gets the item under the mouse event |e|. Returns null if there is no
    // item or if the item is not draggable.
    getItem: function(e) {
      var item = findAncestorByClass(e.target, 'app');

      // You can't drag the web store launcher.
      if (item && item.classList.contains('web-store-entry'))
        return null;

      return item;
    },

    // Returns true if |coordinates| point to a valid drop location. The
    // coordinates are relative to the drag container and the object should
    // have the 'x' and 'y' properties set.
    canDropOn: function(coordinates) {
      var cols = MAX_APPS_PER_ROW[layoutMode];
      var rows = Math.ceil(this.data.length / cols);

      var bottom = rows * this.dimensions.height;
      var right = cols * this.dimensions.width;

      if (coordinates.x >= right || coordinates.x < 0 ||
          coordinates.y >= bottom || coordinates.y < 0)
        return false;

      var position = this.getIndexAt_(coordinates);
      var appCount = this.data.length;

      if (!this.detachWebstoreEntry)
        appCount--;

      return position >= 0 && position < appCount;
    },

    setDragPlaceholder: function(coordinates) {
      var position = this.getIndexAt_(coordinates);
      var appId = this.dragItem.querySelector('a').getAttribute('app-id');
      var current = this.data.indexOf(appId);

      if (current == position || current < 0)
        return;

      arrayMove(this.data, current, position);
      this.invalidate_();
      this.layout();
    },

    getIndexAt_: function(coordinates) {
      var w = this.dimensions.width;
      var h = this.dimensions.height;

      var appsPerRow = MAX_APPS_PER_ROW[layoutMode];

      var row = Math.floor(coordinates.y / h);
      var col = Math.floor(coordinates.x / w);
      var index = appsPerRow * row + col;

      var appCount = this.data.length;
      var rows = Math.ceil(appCount / appsPerRow);

      // Rather than making the free space on the last row invalid, we
      // map it to the last valid position.
      if (index >= appCount && index < appsPerRow * rows)
        return appCount-1;

      return index;
    },

    scrollPage: function(xy) {
      var rect = this.dragContainer.getBoundingClientRect();

      // Here, we calculate the visible boundaries of the app launcher, which
      // are then used to determine when we should auto-scroll.
      var top = $('apps').getBoundingClientRect().bottom;
      var bottomFudge = 15; // Fudge factor due to a gradient mask.
      var bottom = top + maxiviewVisibleHeight - bottomFudge;
      var left = rect.left + window.scrollX;
      var right = Math.min(window.innerWidth, rect.left + rect.width);

      var dy = Math.min(0, xy.y - top) + Math.max(0, xy.y - bottom);
      var dx = Math.min(0, xy.x - left) + Math.max(0, xy.x - right);

      if (dx == 0 && dy == 0) {
        this.stopScroll_();
        return;
      }

      // If we scroll the page directly from this method, it may be choppy and
      // inconsistent. Instead, we loop using animation frames, and scroll at a
      // speed that's independent of how many times this method is called.
      this.scrollMouseXY_ = {dx: dx, dy: dy};

      if (!this.scrollListener_) {
        this.scrollListener_ = this.scrollImpl_.bind(this);
        this.scrollStep_();
      }
    },

    scrollStep_: function() {
      this.scrollStart_ = Date.now();
      window.webkitRequestAnimationFrame(this.scrollListener_);
    },

    scrollImpl_: function(time) {
      if (!appDragAndDrop.isDragging()) {
        this.stopScroll_();
        return;
      }

      if (!this.scrollMouseXY_)
        return;

      var step = time - this.scrollStart_;

      window.scrollBy(
          this.calcScroll_(this.scrollMouseXY_.dx, step),
          this.calcScroll_(this.scrollMouseXY_.dy, step));

      this.scrollStep_();
    },

    calcScroll_: function(delta, step) {
      if (delta == 0)
        return 0;

      // Increase the multiplier for every 50px the mouse is beyond the edge.
      var sign = delta > 0 ? 1 : -1;
      var scalar = APP_AUTOSCROLL_RATE * step / 1000;
      var multiplier = Math.floor(Math.abs(delta) / 50) + 1;

      return sign * scalar * multiplier;
    },

    stopScroll_: function() {
      this.scrollListener_ = null;
      this.scrollMouseXY_ = null;
    },

    saveDrag: function(draggedItem) {
      this.invalidate_();
      this.layout();

      var draggedAppId = draggedItem.querySelector('a').getAttribute('app-id');
      var appIds = this.data.filter(function(id) {
        return id != 'web-store-entry';
      });

      // Wait until the transitions are complete before notifying the browser.
      // Otherwise, the apps will be re-rendered while still transitioning.
      setTimeout(function() {
        chrome.send('reorderApps', [draggedAppId, appIds]);
      }, this.transitionsDuration + 10);
    },

    layout: function(options) {
      options = options || {};
      if (!this.dirty_ && options.force != true)
        return;

      try {
        var container = this.dragContainer;
        if (options.disableAnimations)
          container.setAttribute('launcher-animations', false);
        var d0 = Date.now();
        this.layoutImpl_();
        this.dirty_ = false;
        logEvent('apps.layout: ' + (Date.now() - d0));

      } finally {
        if (options.disableAnimations) {
          // We need to re-enable animations asynchronously, so that the
          // animations are still disabled for this layout update.
          setTimeout(function() {
            container.setAttribute('launcher-animations', true);
          }, 0);
        }
      }
    },

    layoutImpl_: function() {
      var apps = this.data || [];
      var rects = this.getLayoutRects_(apps.length);
      var appsContent = this.dragContainer;

      // Ping the PROMO_SEEN histogram only when the promo is maximized, and
      // maximum once per NTP load.
      this.maybePingPromoSeen_();

      if (!this.visible)
        return;

      for (var i = 0; i < apps.length; i++) {
        var app = appsContent.querySelector('[app-id='+apps[i]+']').parentNode;

        // If the node is being dragged, don't try to place it in the grid.
        if (app == this.dragItem)
          continue;

        app.style.left = rects[i].left + 'px';
        app.style.top = rects[i].top + 'px';
      }

      // We need to set the container's height manually because the apps use
      // absolute positioning.
      var rows = Math.ceil(apps.length / MAX_APPS_PER_ROW[layoutMode]);
      appsContent.style.height = (rows * this.dimensions.height) + 'px';
    },

    getLayoutRects_: function(appCount) {
      var availableWidth = this.dragContainer.offsetWidth;
      var rtl = isRtl();
      var rects = [];
      var w = this.dimensions.width;
      var h = this.dimensions.height;
      var appsPerRow = MAX_APPS_PER_ROW[layoutMode];

      for (var i = 0; i < appCount; i++) {
        var top = Math.floor(i / appsPerRow) * h;
        var left = (i % appsPerRow) * w;

        // Reflect the X axis if an RTL language is active.
        if (rtl)
          left = availableWidth - left - w;
        rects[i] = {left: left, top: top};
      }
      return rects;
    },

    get loadedImages() {
      return this.loadedImages_;
    },

    set loadedImages(value) {
      this.loadedImages_ = value;
      if (this.loadedImages_ == 0)
        return;

      // Each application icon is loaded asynchronously. Here, we display
      // the icons once they've all been loaded to make it look nicer.
      if (this.loadedImages_ == this.data.length) {
        this.showImages();
        return;
      }

      // We won't actually have the visible height until the sections have
      // been layed out.
      if (!maxiviewVisibleHeight)
        return;

      // If we know the visible height of the maxiview, then we can don't need
      // to wait for all the icons. Instead, we wait until the visible portion
      // have been loaded.
      var appsPerRow = MAX_APPS_PER_ROW[layoutMode];
      var rows = Math.ceil(maxiviewVisibleHeight / this.dimensions.height);
      var count = Math.min(appsPerRow * rows, this.data.length);
      if (this.loadedImages_ == count) {
        this.showImages();
        return;
      }
    },

    showImages: function() {
      $('apps-content').classList.add('visible');
      clearTimeout(this.imageTimer);
    },

    createElement: function(app) {
      var div = createElement(app);
      var a = div.firstChild;

      a.onclick = handleClick;
      a.ping = getAppPingUrl(
          'PING_BY_ID', this.showPromo, 'NTP_APPS_MAXIMIZED');
      a.style.backgroundImage = url(app['icon_big']);
      if (app.isNew) {
        div.setAttribute('new', 'new');
        // Delay changing the attribute a bit to let the page settle down a bit.
        setTimeout(function() {
          // Make sure the new icon is scrolled into view.
          document.body.scrollTop = document.body.scrollHeight;

          // This will trigger the 'bounce' animation defined in apps.css.
          div.setAttribute('new', 'installed');
        }, 500);
        div.addEventListener('webkitAnimationEnd', function(e) {
          div.removeAttribute('new');
        });
      }

      // CSS background images don't fire 'load' events, so we use an Image.
      var img = new Image();
      img.onload = function() { this.loadedImages++; }.bind(this);
      img.src = app['icon_big'];

      var settingsButton = div.appendChild(new cr.ui.ContextMenuButton);
      settingsButton.className = 'app-settings';
      settingsButton.title = localStrings.getString('appsettings');

      addContextMenu(div, app);

      return div;
    },

    createMiniviewElement: function(app) {
      var span = document.createElement('span');
      var a = span.appendChild(document.createElement('a'));

      a.setAttribute('app-id', app['id']);
      a.textContent = app['name'];
      a.href = app['launch_url'];
      a.onclick = handleClick;
      a.ping = getAppPingUrl(
          'PING_BY_ID', this.showPromo, 'NTP_APPS_COLLAPSED');
      a.style.backgroundImage = url(app['icon_small']);
      a.className = 'item';
      span.appendChild(a);

      addContextMenu(span, app);

      return span;
    },

    createClosedMenuElement: function(app) {
      var a = document.createElement('a');
      a.setAttribute('app-id', app['id']);
      a.textContent = app['name'];
      a.href = app['launch_url'];
      a.onclick = handleClick;
      a.ping = getAppPingUrl(
          'PING_BY_ID', this.showPromo, 'NTP_APPS_MENU');
      a.style.backgroundImage = url(app['icon_small']);
      a.className = 'item';

      addContextMenu(a, app);

      return a;
    },

    createWebStoreElement: function() {
      var elm = createElement({
        'id': 'web-store-entry',
        'name': localStrings.getString('web_store_title'),
        'launch_url': localStrings.getString('web_store_url')
      });
      elm.classList.add('web-store-entry');
      return elm;
    },

    createWebStoreMiniElement: function() {
      var span = document.createElement('span');
      span.appendChild(this.createWebStoreClosedMenuElement());
      return span;
    },

    createWebStoreClosedMenuElement: function() {
      var a = document.createElement('a');
      a.textContent = localStrings.getString('web_store_title');
      a.href = localStrings.getString('web_store_url');
      a.style.backgroundImage = url('chrome://theme/IDR_PRODUCT_LOGO_16');
      a.className = 'item';
      return a;
    }
  };
})();

// Enable drag and drop reordering of the app launcher.
var appDragAndDrop = new DragAndDropController(apps);