const promiseLimit = require('promise-limit')
const puppeteer = require('puppeteer')

const waitForRender = function (options) {
  options = options || {}

  return new Promise((resolve, reject) => {
    // Render when an event fires on the document.
    if (options.renderAfterDocumentEvent) {
      if (window['__PRERENDER_STATUS'] && window['__PRERENDER_STATUS'].__DOCUMENT_EVENT_RESOLVED) resolve()
      document.addEventListener(options.renderAfterDocumentEvent, () => resolve())

    // Render once a specific element exists.
    } else if (options.renderAfterElementExists) {
      // TODO: Try and get something MutationObserver-based working.
      setInterval(() => {
        if (document.querySelector(options.renderAfterElementExists)) resolve()
      }, 100)

    // Render after a certain number of milliseconds.
    } else if (options.renderAfterTime) {
      setTimeout(() => resolve(), options.renderAfterTime)

    // Default: Render immediately after page content loads.
    } else {
      resolve()
    }
  })
}

class PuppeteerRenderer {
  constructor (rendererOptions) {
    this._puppeteer = null
    this._rendererOptions = rendererOptions || {}

    if (this._rendererOptions.maxConcurrentRoutes == null) this._rendererOptions.maxConcurrentRoutes = 0

    if (this._rendererOptions.inject && !this._rendererOptions.injectProperty) {
      this._rendererOptions.injectProperty = '__PRERENDER_INJECTED'
    }
  }

  async initialize () {
    try {
      // Workaround for Linux SUID Sandbox issues.
      if (process.platform === 'linux') {
        if (!this._rendererOptions.args) this._rendererOptions.args = []

        if (this._rendererOptions.args.indexOf('--no-sandbox') === -1) {
          this._rendererOptions.args.push('--no-sandbox')
          this._rendererOptions.args.push('--disable-setuid-sandbox')
        }
      }

      this._puppeteer = await puppeteer.launch(this._rendererOptions)
    } catch (e) {
      console.error(e)
      console.error('[Prerenderer - PuppeteerRenderer] Unable to start Puppeteer')
      // Re-throw the error so it can be handled further up the chain. Good idea or not?
      throw e
    }

    return this._puppeteer
  }

  async handleRequestInterception (page, baseURL) {
    await page.setRequestInterception(true)

    page.on('request', req => {
      // Skip third party requests if needed.
      if (this._rendererOptions.skipThirdPartyRequests) {
        if (!req.url().startsWith(baseURL)) {
          req.abort()
          return
        }
      }

      req.continue()
    })
  }

  async renderRoutes (routes, Prerenderer) {
    const rootOptions = Prerenderer.getOptions()
    const options = this._rendererOptions

    const limiter = promiseLimit(this._rendererOptions.maxConcurrentRoutes)

    const pagePromises = Promise.all(
      routes.map(
        (route, index) => limiter(
          async () => {
            const page = await this._puppeteer.newPage()

            if (options.consoleHandler) {
              page.on('console', message => options.consoleHandler(route, message))
            }

            if (options.inject) {
              await page.evaluateOnNewDocument(`(function () { window['${options.injectProperty}'] = ${JSON.stringify(options.inject)}; })();`)
            }


            const baseURL = `http://localhost:${rootOptions.server.port}`

            // Allow setting viewport widths and such.
            if (options.viewport) await page.setViewport(options.viewport)

            await this.handleRequestInterception(page, baseURL)

            // Hack just in-case the document event fires before our main listener is added.
            if (options.renderAfterDocumentEvent) {
              page.evaluateOnNewDocument(function (options) {
                window['__PRERENDER_STATUS'] = {}
                document.addEventListener(options.renderAfterDocumentEvent, () => {
                  window['__PRERENDER_STATUS'].__DOCUMENT_EVENT_RESOLVED = true
                })
              }, this._rendererOptions)
            }

            await page.goto(`${baseURL}${route}`, { waituntil: 'networkidle0' })

            // Once this completes, it's safe to capture the page contents.
            await page.evaluate(waitForRender, this._rendererOptions)

            const result = {
              originalRoute: route,
              route: await page.evaluate('window.location.pathname'),
              html: await page.content()
            }

            await page.close()
            return Promise.resolve(result)
          }
        )
      )
    )

    return pagePromises
  }

  destroy () {
    this._puppeteer.close()
  }
}

module.exports = PuppeteerRenderer

@@ -590,186 +590,1149 @@
  // 
-TODO: Try and get something MutationObserver-based working.%0A      setInterval(() =%3E %7B%0A        if (document.querySelector(options.renderAfterElementExists)) resolve()%0A      %7D, 100
+Use MutationObserver to observer a specific element exists.%0A      const targetNode = document.documentElement%0A      // Options for the observer (which mutations to observe)%0A      const config = %7B childList: true, subtree: true %7D%0A      let observed = null%0A      // Callback function to execute when mutations are observed%0A      const callback = function (mutationsList) %7B%0A        for (const mutation of mutationsList) %7B%0A            if (mutation.type == 'childList' && mutation.addedNodes.length) %7B%0A              const hasSpecificElement = %5B...mutation.addedNodes%5D.some(node =%3E node === document.querySelector(options.renderAfterElementExists))%0A              if (hasSpecificElement) %7B%0A                if (observer) %7B%0A                  // Stop observing after find the specific element.%0A                  observer.disconnect()%0A                %7D%0A                resolve()%0A              %7D%0A            %7D%0A        %7D%0A      %7D%0A      // Create an observer instance linked to the callback function%0A      observer = new MutationObserver(callback)%0A%0A      // Start observing the target node for configured mutations%0A      observer.observe(targetNode, config
 )%0A%0A 

Use MutationObserver to observer a specific element exists. (#12)