// Copyright 2014 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.

// Event management for WebViewInternal.

var EventBindings = require('event_bindings');
var MessagingNatives = requireNative('messaging_natives');
var WebView = require('webViewInternal').WebView;

var CreateEvent = function(name) {
  var eventOpts = {supportsListeners: true, supportsFilters: true};
  return new EventBindings.Event(name, undefined, eventOpts);
};

var FrameNameChangedEvent = CreateEvent('webViewInternal.onFrameNameChanged');
var PluginDestroyedEvent = CreateEvent('webViewInternal.onPluginDestroyed');

// WEB_VIEW_EVENTS is a map of stable <webview> DOM event names to their
//     associated extension event descriptor objects.
// An event listener will be attached to the extension event |evt| specified in
//     the descriptor.
// |fields| specifies the public-facing fields in the DOM event that are
//     accessible to <webview> developers.
// |customHandler| allows a handler function to be called each time an extension
//     event is caught by its event listener. The DOM event should be dispatched
//     within this handler function. With no handler function, the DOM event
//     will be dispatched by default each time the extension event is caught.
// |cancelable| (default: false) specifies whether the event's default
//     behavior can be canceled. If the default action associated with the event
//     is prevented, then its dispatch function will return false in its event
//     handler. The event must have a custom handler for this to be meaningful.
var WEB_VIEW_EVENTS = {
  'close': {
    evt: CreateEvent('webViewInternal.onClose'),
    fields: []
  },
  'consolemessage': {
    evt: CreateEvent('webViewInternal.onConsoleMessage'),
    fields: ['level', 'message', 'line', 'sourceId']
  },
  'contentload': {
    evt: CreateEvent('webViewInternal.onContentLoad'),
    fields: []
  },
  'dialog': {
    cancelable: true,
    customHandler: function(handler, event, webViewEvent) {
      handler.handleDialogEvent(event, webViewEvent);
    },
    evt: CreateEvent('webViewInternal.onDialog'),
    fields: ['defaultPromptText', 'messageText', 'messageType', 'url']
  },
  'exit': {
     evt: CreateEvent('webViewInternal.onExit'),
     fields: ['processId', 'reason']
  },
  'findupdate': {
    evt: CreateEvent('webViewInternal.onFindReply'),
    fields: [
      'searchText',
      'numberOfMatches',
      'activeMatchOrdinal',
      'selectionRect',
      'canceled',
      'finalUpdate'
    ]
  },
  'loadabort': {
    cancelable: true,
    customHandler: function(handler, event, webViewEvent) {
      handler.handleLoadAbortEvent(event, webViewEvent);
    },
    evt: CreateEvent('webViewInternal.onLoadAbort'),
    fields: ['url', 'isTopLevel', 'reason']
  },
  'loadcommit': {
    customHandler: function(handler, event, webViewEvent) {
      handler.handleLoadCommitEvent(event, webViewEvent);
    },
    evt: CreateEvent('webViewInternal.onLoadCommit'),
    fields: ['url', 'isTopLevel']
  },
  'loadprogress': {
    evt: CreateEvent('webViewInternal.onLoadProgress'),
    fields: ['url', 'progress']
  },
  'loadredirect': {
    evt: CreateEvent('webViewInternal.onLoadRedirect'),
    fields: ['isTopLevel', 'oldUrl', 'newUrl']
  },
  'loadstart': {
    evt: CreateEvent('webViewInternal.onLoadStart'),
    fields: ['url', 'isTopLevel']
  },
  'loadstop': {
    evt: CreateEvent('webViewInternal.onLoadStop'),
    fields: []
  },
  'newwindow': {
    cancelable: true,
    customHandler: function(handler, event, webViewEvent) {
      handler.handleNewWindowEvent(event, webViewEvent);
    },
    evt: CreateEvent('webViewInternal.onNewWindow'),
    fields: [
      'initialHeight',
      'initialWidth',
      'targetUrl',
      'windowOpenDisposition',
      'name'
    ]
  },
  'permissionrequest': {
    cancelable: true,
    customHandler: function(handler, event, webViewEvent) {
      handler.handlePermissionEvent(event, webViewEvent);
    },
    evt: CreateEvent('webViewInternal.onPermissionRequest'),
    fields: [
      'identifier',
      'lastUnlockedBySelf',
      'name',
      'permission',
      'requestMethod',
      'url',
      'userGesture'
    ]
  },
  'responsive': {
    evt: CreateEvent('webViewInternal.onResponsive'),
    fields: ['processId']
  },
  'sizechanged': {
    evt: CreateEvent('webViewInternal.onSizeChanged'),
    customHandler: function(handler, event, webViewEvent) {
      handler.handleSizeChangedEvent(event, webViewEvent);
    },
    fields: ['oldHeight', 'oldWidth', 'newHeight', 'newWidth']
  },
  'unresponsive': {
    evt: CreateEvent('webViewInternal.onUnresponsive'),
    fields: ['processId']
  },
  'zoomchange': {
    evt: CreateEvent('webViewInternal.onZoomChange'),
    fields: ['oldZoomFactor', 'newZoomFactor']
  }
};

// Constructor.
function WebViewEvents(webViewInternal, viewInstanceId) {
  this.webViewInternal = webViewInternal;
  this.viewInstanceId = viewInstanceId;
  this.setup();
}

// Sets up events.
WebViewEvents.prototype.setup = function() {
  this.setupFrameNameChangedEvent();
  this.setupPluginDestroyedEvent();
  this.webViewInternal.maybeSetupChromeWebViewEvents();
  this.webViewInternal.setupExperimentalContextMenus();

  var events = this.getEvents();
  for (var eventName in events) {
    this.setupEvent(eventName, events[eventName]);
  }
};

WebViewEvents.prototype.setupFrameNameChangedEvent = function() {
  FrameNameChangedEvent.addListener(function(e) {
    this.webViewInternal.onFrameNameChanged(e.name);
  }.bind(this), {instanceId: this.viewInstanceId});
};

WebViewEvents.prototype.setupPluginDestroyedEvent = function() {
  PluginDestroyedEvent.addListener(function(e) {
    this.webViewInternal.onPluginDestroyed();
  }.bind(this), {instanceId: this.viewInstanceId});
};

WebViewEvents.prototype.getEvents = function() {
  var experimentalEvents = this.webViewInternal.maybeGetExperimentalEvents();
  for (var eventName in experimentalEvents) {
    WEB_VIEW_EVENTS[eventName] = experimentalEvents[eventName];
  }
  var chromeEvents = this.webViewInternal.maybeGetChromeWebViewEvents();
  for (var eventName in chromeEvents) {
    WEB_VIEW_EVENTS[eventName] = chromeEvents[eventName];
  }
  return WEB_VIEW_EVENTS;
};

WebViewEvents.prototype.setupEvent = function(name, info) {
  info.evt.addListener(function(e) {
    var details = {bubbles:true};
    if (info.cancelable) {
      details.cancelable = true;
    }
    var webViewEvent = new Event(name, details);
    $Array.forEach(info.fields, function(field) {
      if (e[field] !== undefined) {
        webViewEvent[field] = e[field];
      }
    }.bind(this));
    if (info.customHandler) {
      info.customHandler(this, e, webViewEvent);
      return;
    }
    this.webViewInternal.dispatchEvent(webViewEvent);
  }.bind(this), {instanceId: this.viewInstanceId});

  this.webViewInternal.setupEventProperty(name);
};


WebViewEvents.prototype.handleDialogEvent = function(event, webViewEvent) {
  var showWarningMessage = function(dialogType) {
    var VOWELS = ['a', 'e', 'i', 'o', 'u'];
    var WARNING_MSG_DIALOG_BLOCKED = '<webview>: %1 %2 dialog was blocked.';
    var article = (VOWELS.indexOf(dialogType.charAt(0)) >= 0) ? 'An' : 'A';
    var output = WARNING_MSG_DIALOG_BLOCKED.replace('%1', article);
    output = output.replace('%2', dialogType);
    window.console.warn(output);
  };

  var requestId = event.requestId;
  var actionTaken = false;

  var validateCall = function() {
    var ERROR_MSG_DIALOG_ACTION_ALREADY_TAKEN = '<webview>: ' +
        'An action has already been taken for this "dialog" event.';

    if (actionTaken) {
      throw new Error(ERROR_MSG_DIALOG_ACTION_ALREADY_TAKEN);
    }
    actionTaken = true;
  };

  var getGuestInstanceId = function() {
    return this.webViewInternal.getGuestInstanceId();
  }.bind(this);

  var dialog = {
    ok: function(user_input) {
      validateCall();
      user_input = user_input || '';
      WebView.setPermission(getGuestInstanceId(), requestId, 'allow',
                            user_input);
    },
    cancel: function() {
      validateCall();
      WebView.setPermission(getGuestInstanceId(), requestId, 'deny');
    }
  };
  webViewEvent.dialog = dialog;

  var defaultPrevented = !this.webViewInternal.dispatchEvent(webViewEvent);
  if (actionTaken) {
    return;
  }

  if (defaultPrevented) {
    // Tell the JavaScript garbage collector to track lifetime of |dialog| and
    // call back when the dialog object has been collected.
    MessagingNatives.BindToGC(dialog, function() {
      // Avoid showing a warning message if the decision has already been made.
      if (actionTaken) {
        return;
      }
      WebView.setPermission(
          getGuestInstanceId(), requestId, 'default', '', function(allowed) {
        if (allowed) {
          return;
        }
        showWarningMessage(event.messageType);
      });
    });
  } else {
    actionTaken = true;
    // The default action is equivalent to canceling the dialog.
    WebView.setPermission(
        getGuestInstanceId(), requestId, 'default', '', function(allowed) {
      if (allowed) {
        return;
      }
      showWarningMessage(event.messageType);
    });
  }
};

WebViewEvents.prototype.handleLoadAbortEvent = function(event, webViewEvent) {
  var showWarningMessage = function(reason) {
    var WARNING_MSG_LOAD_ABORTED = '<webview>: ' +
        'The load has aborted with reason "%1".';
    window.console.warn(WARNING_MSG_LOAD_ABORTED.replace('%1', reason));
  };
  if (this.webViewInternal.dispatchEvent(webViewEvent)) {
    showWarningMessage(event.reason);
  }
};

WebViewEvents.prototype.handleLoadCommitEvent = function(event, webViewEvent) {
  this.webViewInternal.onLoadCommit(event.baseUrlForDataUrl,
                                    event.currentEntryIndex, event.entryCount,
                                    event.processId, event.url,
                                    event.isTopLevel);
  this.webViewInternal.dispatchEvent(webViewEvent);
};

WebViewEvents.prototype.handleNewWindowEvent = function(event, webViewEvent) {
  var ERROR_MSG_NEWWINDOW_ACTION_ALREADY_TAKEN = '<webview>: ' +
      'An action has already been taken for this "newwindow" event.';

  var ERROR_MSG_NEWWINDOW_UNABLE_TO_ATTACH = '<webview>: ' +
      'Unable to attach the new window to the provided webViewInternal.';

  var ERROR_MSG_WEBVIEW_EXPECTED = '<webview> element expected.';

  var showWarningMessage = function() {
    var WARNING_MSG_NEWWINDOW_BLOCKED = '<webview>: A new window was blocked.';
    window.console.warn(WARNING_MSG_NEWWINDOW_BLOCKED);
  };

  var requestId = event.requestId;
  var actionTaken = false;
  var getGuestInstanceId = function() {
    return this.webViewInternal.getGuestInstanceId();
  }.bind(this);

  var validateCall = function () {
    if (actionTaken) {
      throw new Error(ERROR_MSG_NEWWINDOW_ACTION_ALREADY_TAKEN);
    }
    actionTaken = true;
  };

  var windowObj = {
    attach: function(webview) {
      validateCall();
      if (!webview || !webview.tagName || webview.tagName != 'WEBVIEW')
        throw new Error(ERROR_MSG_WEBVIEW_EXPECTED);
      // Attach happens asynchronously to give the tagWatcher an opportunity
      // to pick up the new webview before attach operates on it, if it hasn't
      // been attached to the DOM already.
      // Note: Any subsequent errors cannot be exceptions because they happen
      // asynchronously.
      setTimeout(function() {
        var webViewInternal = privates(webview).internal;
        // Update the partition.
        if (event.storagePartitionId) {
          webViewInternal.onAttach(event.storagePartitionId);
        }

        var attached = webViewInternal.attachWindow(event.windowId, true);

        if (!attached) {
          window.console.error(ERROR_MSG_NEWWINDOW_UNABLE_TO_ATTACH);
        }

        var guestInstanceId = getGuestInstanceId();
        if (!guestInstanceId) {
          // If the opener is already gone, then we won't have its
          // guestInstanceId.
          return;
        }

        // If the object being passed into attach is not a valid <webview>
        // then we will fail and it will be treated as if the new window
        // was rejected. The permission API plumbing is used here to clean
        // up the state created for the new window if attaching fails.
        WebView.setPermission(
            guestInstanceId, requestId, attached ? 'allow' : 'deny');
      }, 0);
    },
    discard: function() {
      validateCall();
      var guestInstanceId = getGuestInstanceId();
      if (!guestInstanceId) {
        // If the opener is already gone, then we won't have its
        // guestInstanceId.
        return;
      }
      WebView.setPermission(guestInstanceId, requestId, 'deny');
    }
  };
  webViewEvent.window = windowObj;

  var defaultPrevented = !this.webViewInternal.dispatchEvent(webViewEvent);
  if (actionTaken) {
    return;
  }

  if (defaultPrevented) {
    // Make browser plugin track lifetime of |windowObj|.
    MessagingNatives.BindToGC(windowObj, function() {
      // Avoid showing a warning message if the decision has already been made.
      if (actionTaken) {
        return;
      }

      var guestInstanceId = getGuestInstanceId();
      if (!guestInstanceId) {
        // If the opener is already gone, then we won't have its
        // guestInstanceId.
        return;
      }

      WebView.setPermission(
          guestInstanceId, requestId, 'default', '', function(allowed) {
            if (allowed) {
              return;
            }
            showWarningMessage();
          });
    });
  } else {
    actionTaken = true;
    // The default action is to discard the window.
    WebView.setPermission(
        getGuestInstanceId(), requestId, 'default', '', function(allowed) {
      if (allowed) {
        return;
      }
      showWarningMessage();
    });
  }
};

WebViewEvents.prototype.getPermissionTypes = function() {
  var permissions =
      ['media',
      'geolocation',
      'pointerLock',
      'download',
      'loadplugin',
      'filesystem'];
  return permissions.concat(
      this.webViewInternal.maybeGetExperimentalPermissions());
};

WebViewEvents.prototype.handlePermissionEvent =
    function(event, webViewEvent) {
  var ERROR_MSG_PERMISSION_ALREADY_DECIDED = '<webview>: ' +
      'Permission has already been decided for this "permissionrequest" event.';

  var showWarningMessage = function(permission) {
    var WARNING_MSG_PERMISSION_DENIED = '<webview>: ' +
        'The permission request for "%1" has been denied.';
    window.console.warn(
        WARNING_MSG_PERMISSION_DENIED.replace('%1', permission));
  };

  var requestId = event.requestId;
  var getGuestInstanceId = function() {
    return this.webViewInternal.getGuestInstanceId();
  }.bind(this);

  if (this.getPermissionTypes().indexOf(event.permission) < 0) {
    // The permission type is not allowed. Trigger the default response.
    WebView.setPermission(
        getGuestInstanceId(), requestId, 'default', '', function(allowed) {
      if (allowed) {
        return;
      }
      showWarningMessage(event.permission);
    });
    return;
  }

  var decisionMade = false;
  var validateCall = function() {
    if (decisionMade) {
      throw new Error(ERROR_MSG_PERMISSION_ALREADY_DECIDED);
    }
    decisionMade = true;
  };

  // Construct the event.request object.
  var request = {
    allow: function() {
      validateCall();
      WebView.setPermission(getGuestInstanceId(), requestId, 'allow');
    },
    deny: function() {
      validateCall();
      WebView.setPermission(getGuestInstanceId(), requestId, 'deny');
    }
  };
  webViewEvent.request = request;

  var defaultPrevented = !this.webViewInternal.dispatchEvent(webViewEvent);
  if (decisionMade) {
    return;
  }

  if (defaultPrevented) {
    // Make browser plugin track lifetime of |request|.
    MessagingNatives.BindToGC(request, function() {
      // Avoid showing a warning message if the decision has already been made.
      if (decisionMade) {
        return;
      }
      WebView.setPermission(
          getGuestInstanceId(), requestId, 'default', '', function(allowed) {
        if (allowed) {
          return;
        }
        showWarningMessage(event.permission);
      });
    });
  } else {
    decisionMade = true;
    WebView.setPermission(
        getGuestInstanceId(), requestId, 'default', '',
        function(allowed) {
          if (allowed) {
            return;
          }
          showWarningMessage(event.permission);
        });
  }
};

WebViewEvents.prototype.handleSizeChangedEvent = function(
    event, webViewEvent) {
  this.webViewInternal.onSizeChanged(webViewEvent);
};

exports.WebViewEvents = WebViewEvents;
exports.CreateEvent = CreateEvent;