@app =
  _$: $
  _$$: $$
  _page: page
  collections: {}
  models:      {}
  templates:   {}
  views:       {}

  init: ->
    try @initErrorTracking() catch
    return unless @browserCheck()

    @el = $('._app')
    @localStorage = new LocalStorageStore
    @serviceWorker = new app.ServiceWorker if app.ServiceWorker.isEnabled()
    @settings = new app.Settings
    @db = new app.DB()

    @settings.initLayout()

    @docs = new app.collections.Docs
    @disabledDocs = new app.collections.Docs
    @entries = new app.collections.Entries

    @router = new app.Router
    @shortcuts = new app.Shortcuts
    @document = new app.views.Document
    @mobile = new app.views.Mobile if @isMobile()

    if document.body.hasAttribute('data-doc')
      @DOC = JSON.parse(document.body.getAttribute('data-doc'))
      @bootOne()
    else if @DOCS
      @bootAll()
    else
      @onBootError()
    return

  browserCheck: ->
    return true if @isSupportedBrowser()
    document.body.innerHTML = app.templates.unsupportedBrowser
    @hideLoadingScreen()
    false

  initErrorTracking: ->
    # Show a warning message and don't track errors when the app is loaded
    # from a domain other than our own, because things are likely to break.
    # (e.g. cross-domain requests)
    if @isInvalidLocation()
      new app.views.Notif 'InvalidLocation'
    else
      if @config.sentry_dsn
        Raven.config @config.sentry_dsn,
          release: @config.release
          whitelistUrls: [/devdocs/]
          includePaths: [/devdocs/]
          ignoreErrors: [/NPObject/, /NS_ERROR/, /^null$/, /EvalError/]
          tags:
            mode: if @isSingleDoc() then 'single' else 'full'
            iframe: (window.top isnt window).toString()
            electron: (!!window.process?.versions?.electron).toString()
          shouldSendCallback: =>
            try
              if @isInjectionError()
                @onInjectionError()
                return false
              if @isAndroidWebview()
                return false
            true
          dataCallback: (data) ->
            try
              $.extend(data.user ||= {}, app.settings.dump())
              data.user.docs = data.user.docs.split('/') if data.user.docs
              data.user.lastIDBTransaction = app.lastIDBTransaction if app.lastIDBTransaction
              data.tags.scriptCount = document.scripts.length
            data
        .install()
      @previousErrorHandler = onerror
      window.onerror = @onWindowError.bind(@)
      CookiesStore.onBlocked = @onCookieBlocked
    return

  bootOne: ->
    @doc = new app.models.Doc @DOC
    @docs.reset [@doc]
    @doc.load @start.bind(@), @onBootError.bind(@), readCache: true
    new app.views.Notice 'singleDoc', @doc
    delete @DOC
    return

  bootAll: ->
    docs = @settings.getDocs()
    for doc in @DOCS
      (if docs.indexOf(doc.slug) >= 0 then @docs else @disabledDocs).add(doc)
    @migrateDocs()
    @docs.load @start.bind(@), @onBootError.bind(@), readCache: true, writeCache: true
    delete @DOCS
    return

  start: ->
    @entries.add doc.toEntry() for doc in @docs.all()
    @entries.add doc.toEntry() for doc in @disabledDocs.all()
    @initDoc(doc) for doc in @docs.all()
    @trigger 'ready'
    @router.start()
    @hideLoadingScreen()
    setTimeout =>
      @welcomeBack() unless @doc
      @removeEvent 'ready bootError'
    , 50
    return

  initDoc: (doc) ->
    doc.entries.add type.toEntry() for type in doc.types.all()
    @entries.add doc.entries.all()
    return

  migrateDocs: ->
    for slug in @settings.getDocs() when not @docs.findBy('slug', slug)
      needsSaving = true
      doc = @disabledDocs.findBy('slug', 'webpack') if slug == 'webpack~2'
      doc = @disabledDocs.findBy('slug', 'angular') if slug == 'angular~4_typescript'
      doc = @disabledDocs.findBy('slug', 'angular~2') if slug == 'angular~2_typescript'
      doc ||= @disabledDocs.findBy('slug_without_version', slug)
      if doc
        @disabledDocs.remove(doc)
        @docs.add(doc)

    @saveDocs() if needsSaving
    return

  enableDoc: (doc, _onSuccess, onError) ->
    return if @docs.contains(doc)

    onSuccess = =>
      return if @docs.contains(doc)
      @disabledDocs.remove(doc)
      @docs.add(doc)
      @docs.sort()
      @initDoc(doc)
      @saveDocs()
      if app.settings.get('autoInstall')
        doc.install(_onSuccess, onError)
      else
        _onSuccess()
      return

    doc.load onSuccess, onError, writeCache: true
    return

  saveDocs: ->
    @settings.setDocs(doc.slug for doc in @docs.all())
    @db.migrate()
    @serviceWorker?.updateInBackground()

  welcomeBack: ->
    visitCount = @settings.get('count')
    @settings.set 'count', ++visitCount
    new app.views.Notif 'Share', autoHide: null if visitCount is 5
    new app.views.News()
    new app.views.Updates()
    @updateChecker = new app.UpdateChecker()

  reboot: ->
    if location.pathname isnt '/' and location.pathname isnt '/settings'
      window.location = "/##{location.pathname}"
    else
      window.location = '/'
    return

  reload: ->
    @docs.clearCache()
    @disabledDocs.clearCache()
    if @serviceWorker then @serviceWorker.reload() else @reboot()
    return

  reset: ->
    @localStorage.reset()
    @settings.reset()
    @db?.reset()
    @serviceWorker?.update()
    window.location = '/'
    return

  showTip: (tip) ->
    return if @isSingleDoc()
    tips = @settings.getTips()
    if tips.indexOf(tip) is -1
      tips.push(tip)
      @settings.setTips(tips)
      new app.views.Tip(tip)
    return

  hideLoadingScreen: ->
    document.body.classList.add '_overlay-scrollbars' if $.overlayScrollbarsEnabled()
    document.documentElement.classList.remove '_booting'
    return

  indexHost: ->
    # Can't load the index files from the host/CDN when service worker is
    # enabled because it doesn't support caching URLs that use CORS.
    @config[if @serviceWorker and @settings.hasDocs() then 'index_path' else 'docs_origin']

  onBootError: (args...) ->
    @trigger 'bootError'
    @hideLoadingScreen()
    return

  onQuotaExceeded: ->
    return if @quotaExceeded
    @quotaExceeded = true
    new app.views.Notif 'QuotaExceeded', autoHide: null
    return

  onCookieBlocked: (key, value, actual) ->
    return if @cookieBlocked
    @cookieBlocked = true
    new app.views.Notif 'CookieBlocked', autoHide: null
    Raven.captureMessage "CookieBlocked/#{key}", level: 'warning', extra: {value, actual}
    return

  onWindowError: (args...) ->
    return if @cookieBlocked
    if @isInjectionError args...
      @onInjectionError()
    else if @isAppError args...
      @previousErrorHandler? args...
      @hideLoadingScreen()
      @errorNotif or= new app.views.Notif 'Error'
      @errorNotif.show()
    return

  onInjectionError: ->
    unless @injectionError
      @injectionError = true
      alert """
        JavaScript code has been injected in the page which prevents DevDocs from running correctly.
        Please check your browser extensions/addons. """
      Raven.captureMessage 'injection error', level: 'info'
    return

  isInjectionError: ->
    # Some browser extensions expect the entire web to use jQuery.
    # I gave up trying to fight back.
    window.$ isnt app._$ or window.$$ isnt app._$$ or window.page isnt app._page or typeof $.empty isnt 'function' or typeof page.show isnt 'function'

  isAppError: (error, file) ->
    # Ignore errors from external scripts.
    file and file.indexOf('devdocs') isnt -1 and file.indexOf('.js') is file.length - 3

  isSupportedBrowser: ->
    try
      features =
        bind:               !!Function::bind
        pushState:          !!history.pushState
        matchMedia:         !!window.matchMedia
        insertAdjacentHTML: !!document.body.insertAdjacentHTML
        defaultPrevented:     document.createEvent('CustomEvent').defaultPrevented is false
        cssVariables:       !!CSS?.supports?('(--t: 0)')

      for key, value of features when not value
        Raven.captureMessage "unsupported/#{key}", level: 'info'
        return false

      true
    catch error
      Raven.captureMessage 'unsupported/exception', level: 'info', extra: { error: error }
      false

  isSingleDoc: ->
    document.body.hasAttribute('data-doc')

  isMobile: ->
    @_isMobile ?= app.views.Mobile.detect()

  isAndroidWebview: ->
    @_isAndroidWebview ?= app.views.Mobile.detectAndroidWebview()

  isInvalidLocation: ->
    @config.env is 'production' and location.host.indexOf(app.config.production_host) isnt 0

$.extend app, Events
