/* imports common modules */

var electron = require('electron')
var ipc = electron.ipcRenderer

var propertiesToClone = ['deltaX', 'deltaY', 'metaKey', 'ctrlKey', 'defaultPrevented', 'clientX', 'clientY']

function cloneEvent (e) {
  var obj = {}

  for (var i = 0; i < propertiesToClone.length; i++) {
    obj[propertiesToClone[i]] = e[propertiesToClone[i]]
  }
  return JSON.stringify(obj)
}

// workaround for Electron bug
setTimeout(function () {
  document.addEventListener('wheel', function (e) {
    ipc.send('wheel-event', cloneEvent(e))
  }, {passive: true})
}, 0)
;
/* send bookmarks data.  */

function isVisible (el) {
  // https://github.com/jquery/jquery/blob/305f193aa57014dc7d8fa0739a3fefd47166cd44/src/css/hiddenVisibleSelectors.js
  return el.offsetWidth || el.offsetHeight || (el.getClientRects && el.getClientRects().length)
}

function extractPageText (doc, win) {
  var maybeNodes = [].slice.call(doc.body.childNodes)
  var textNodes = []

  var ignore = 'link, style, script, noscript, .hidden, [class*="-hidden"], .visually-hidden, .visuallyhidden, [role=presentation], [hidden], [style*="display:none"], [style*="display: none"], .ad, .dialog, .modal, select, svg, details:not([open])'

  while(maybeNodes.length) {
    var node = maybeNodes[0]

    // remove the node from the list of nodes to check
    maybeNodes.shift()

    // if the node should be ignored, skip it and all of it's child nodes
    if (node.matches && node.matches(ignore)) {
      continue
    }

    // if the node is a text node, add it to the list of text nodes

    if (node.nodeType === 3) {
      textNodes.push(node)
      continue
    }

    if (!isVisible(node)) {
      continue
    }

    // otherwise, add the node's text nodes to the list of text, and the other child nodes to the list of nodes to check
    var childNodes = node.childNodes
    var cnl = childNodes.length

    for (var i = 0; i < cnl; i++) {
      var childNode = childNodes[i]
      // text node
      if (childNode.nodeType === 3) {
        textNodes.push(childNode)
      } else {
        maybeNodes.unshift(childNode)
      }
    }
  }

  var text = ''

  var tnl = textNodes.length

  // combine the text of all of the accepted text nodes together
  for (var i = 0; i < tnl; i++) {
    text += textNodes[i].textContent + ' '
  }

  // special meta tags

  var mt = doc.head.querySelector('meta[name=description]')

  if (mt) {
    text += ' ' + mt.content
  }

  text = text.trim()

  text = text.replace(/[\n\t]/g, ' ') // remove useless newlines/tabs that increase filesize

  text = text.replace(/\s{2,}/g, ' ') // collapse multiple spaces into one
  return text
}

function getPageData (cb) {
  requestAnimationFrame(function () {
    /* also parse special metadata: price, rating, location, cooking time */

    var price, rating, location, cookTime

    // pricing

    var priceEl = document.querySelector('[itemprop=price], .price, .offer-price, #priceblock_ourprice, .discounted-price')
    var currencyEl = document.querySelector('[itemprop=priceCurrency], [property=priceCurrency]')

    if (priceEl) {
      price = priceEl.getAttribute('content') || priceEl.textContent
    }

    if (currencyEl) {
      var currency = currencyEl.getAttribute('content') || currencyEl.textContent
    }

    if (!/\d/g.test(price)) { // if the price doesn't contain a number, it probably isn't accurate
      price = undefined
    }

    var currencySymbolMap = {
      'USD': '$',
      'EUR': '€',
    // TODO add support for more currencies
    }

    if (price && /^[\d\.]+$/g.test(price) && currencySymbolMap[currency]) { // try to add a currency if we don't have one
      price = currencySymbolMap[currency] + price
    }

    if (price) {
      price = price.trim()
    }

    // ratings

    var ratingEl = document.querySelector('.star-img, .rating, [itemprop="ratingValue"], [property="ratingValue"]')

    if (!ratingEl) { // if we didn't find an element, try again with things that might be a rating element, but are less likely
      ratingEl = document.querySelector('[class^="rating"], [class^="review"]')
    }

    if (ratingEl) {
      rating = ratingEl.title || ratingEl.alt || ratingEl.content || ratingEl.getAttribute('content') || ratingEl.textContent
      rating = rating.replace('rating', '').replace('stars', '').replace('star', '').trim()

      // if the rating is just a number, round it first, because some websites (such as walmart.com) don't round it automatically
      if (/^[\d\.]+$/g.test(rating)) {
        try {
          rating = Math.round(parseFloat(rating) * 100) / 100
        } catch (e) {}
      }

      if (rating && /\d+$/g.test(rating)) { // if the rating ends in a number, we assume it means "stars", and append the prefix
        rating = rating + ' stars'
      }
    }

    if (rating && rating.length > 20) {
      // very long strings are unlikely to actually be ratings
      rating = undefined
    }

    // location

    var locationEl = document.querySelector('[itemprop="location"], [itemprop="address"]')

    if (!locationEl) {
      var locationEl = document.querySelector('.adr, .addr, .address')
    }

    if (locationEl) {
      location = locationEl.textContent.trim()
    }

    // remove US postcodes, since these usually aren't important and they take up space
    // TODO make this work for other countries
    if (location && /,?\d{5}$/g.test(location)) {
      location = location.replace(/,?\d{5}$/g, '')
    }

    if (location && location.length > 60) {
      location = undefined
    }

    // cooking time

    var cookingTimeEl = document.querySelector('[itemprop="totalTime"], [itemprop="cookTime"]')

    if (cookingTimeEl) {
      cookTime = cookingTimeEl.textContent
      cookTime = cookTime.replace(/\sm$/g, ' minutes').replace(/\sh$/g, ' hours')
      cookTime = cookTime.replace('1 hours', '1 hour')
    }

    if (cookTime && cookTime.length > 20) {
      cookTime = undefined
    }

    var text = extractPageText(document, window)

    // try to also extract text for same-origin iframes (such as the reader mode frame)

    var frames = document.querySelectorAll('iframe')

    for (var x = 0; x < frames.length; frames++) {
      try {
        text += '. ' + extractPageText(frames[x].contentDocument, frames[x].contentWindow)
      } catch (e) {}
    }

    // limit the amount of text that is collected

    text = text.substring(0, 300000)

    cb({
      extractedText: text,
      metadata: {
        price: price,
        rating: rating,
        location: location,
        cookTime: cookTime
      }
    })
  })
}

// send the data when the page loads

window.addEventListener('load', function (e) {
  setTimeout(function () {
    getPageData(function (data) {
      ipc.send('pageData', data)
    })
  }, 500)
})
;
/* detects if a page is readerable, and tells the main process if it is */

function pageIsReaderable () {
  var paragraphMap = new Map()

  var paragraphs = document.querySelectorAll('p')
  var totalLength = 0

  if (!paragraphs) {
    return false
  }

  for (var i = 0; i < paragraphs.length; i++) {
    var pLength = Math.max(paragraphs[i].textContent.replace(/\s+/g, ' ').length - 100, -30)
    totalLength += pLength

    var prev = paragraphMap.get(paragraphs[i].parentNode) || 0
    paragraphMap.set(paragraphs[i].parentNode, prev + pLength)
  }

  var largestValue = 0

  paragraphMap.forEach(function (value, key) {
    if (value > largestValue) {
      largestValue = value
    }
  })

  if ((largestValue > 600 && largestValue / totalLength > 0.33) || (largestValue > 400 && document.querySelector('article, meta[property="og:type"][content="article"]'))) {
    return true
  } else {
    return false
  }
}

function checkReaderStatus () {
  if (pageIsReaderable()) {
    ipc.send('canReader')
  }
}

document.addEventListener('DOMContentLoaded', checkReaderStatus)
window.addEventListener('load', checkReaderStatus)
;
/* detects terms in the webpage to show as search suggestions */

function isScrolledIntoView (el, doc, win) {
  var rect = el.getBoundingClientRect()
  var x = rect.x, y = rect.y

  if (win.frameElement) {
    // this item is inside an iframe, adjust the coordinates to account for the coordinates of the frame
    var frameRect = win.frameElement.getBoundingClientRect()
    x += frameRect.x, y += frameRect.y
  }

  var isVisible = y > 0 && y < window.innerHeight && x > 0 && x < window.innerWidth

  return isVisible
}

ipc.on('getKeywordsData', function (e) {
  function extractPageText (doc, win) {
    var ignore = ['LINK', 'STYLE', 'SCRIPT', 'NOSCRIPT', 'svg', 'symbol', 'title', 'path', 'style']
    var text = ''
    var pageElements = doc.querySelectorAll('p, h2, h3, h4, li, [name=author], [itemprop=name], .article-author')

    var scrollY = window.scrollY
    for (var i = 0; i < pageElements.length; i++) {
      var el = pageElements[i]

      if (!isScrolledIntoView(pageElements[i], doc, win) || (pageElements[i].tagName === 'META' && scrollY > 500) || pageElements[i].textContent.length < 100 || pageElements[i].querySelector('time, span, div, menu')) {
        continue
      }

      if (ignore.indexOf(el.tagName) === -1) {
        var elText = el.textContent || el.content

        if (pageElements[i - 1] && /\.\s*$/g.test(pageElements[i - 1].textContent)) {
          text += ' ' + elText
        } else {
          text += '. ' + elText
        }
      }
    }

    text = text.replace(/[\n\t]/g, ' ') // remove useless newlines/tabs that increase filesize

    return text
  }

  function extractKeywords (text) {
    /* attempt to identify strings of capitalized words in the text */
    /* TODO add support for languages other than English */

    var words = text
      .replace(/[\u201C\u201D]/g, '"') // convert curly quotes to straight quotes
      .split(/\s+/g)

    // discard empty words
    words = words.filter(function (word) {
      return !!word
    })

    var keywords = []
    var ignoreWords = ['a', 'an', 'the', 'on', 'of', 'or', 'i', 'for']
    var sentenceEndingCharacters = ['.', '."', '?', '?"', '!', '!"']
    var phraseEndingCharcters = [',', ':', ';', '.']
    var thisKeyword = []
    for (var i = 0; i < words.length; i++) {
      // skip the first word after a sentence
      if (words[i - 1] && words[i - 1].length > 2 && sentenceEndingCharacters.find(char => words[i - 1].endsWith(char))) {
        thisKeyword = []
        continue
      }

      // if this word is capitalized (but not all upper-case), it should be part of the keyword
      if (words[i][0].toUpperCase() === words[i][0] && /[A-Z]/g.test(words[i][0]) && words[i] !== words[i].toUpperCase()) {
        thisKeyword.push(words[i])

        // if this word ends with a phrase-ending character, we should jump to saving or discarding
        if (words[i].length > 2 && phraseEndingCharcters.includes(words[i][words[i].length - 1])) {
        } else {
          // otherwise, we should skip the save-or-discard and continue adding words
          continue
        }
      }

      // add ignorable words to an existing keyword
      if (thisKeyword.length > 0 && ignoreWords.includes(words[i].toLowerCase())) {
        thisKeyword.push(words[i])
        continue
      }

      // otherwise, decide whether to keep the keyword.
      // only keep it if it is > 1 word
      if (thisKeyword.length > 1) {
        // discard ignorable words at the end
        while(ignoreWords.includes(thisKeyword[thisKeyword.length - 1].toLowerCase())) {
          thisKeyword.pop()
        }
        if (thisKeyword.length > 1) { // make sure there are still two words left after discarding ignorables
          let keywordText = thisKeyword.join(' ').replace(/^\W+/g, '').replace(/\W+$/g, '').trim()
          if (!keywords.includes(keywordText)) {
            keywords.push(keywordText)
          }
        }
        thisKeyword = []
      } else {
        thisKeyword = []
      }
    }

    return keywords
  }

  if (!pageIsReaderable() && window.location.toString().indexOf('reader/index.html') === -1) {
    return
  }

  var text = extractPageText(document, window)

  var frames = document.querySelectorAll('iframe')

  if (frames) {
    for (var i = 0; i < frames.length; i++) {
      try { // reading contentDocument will throw an error if the frame is not same-origin
        text += ' ' + extractPageText(frames[i].contentDocument, frames[i].contentWindow)
      } catch (e) {}
    }
  }

  var entities = extractKeywords(text)

  ipc.send('keywordsData', {
    entities: entities
  })
})
;
var scriptsToRun = []

/* a collection of various hacks to unbreak sites, mainly due to missing window.open() support */

/* all sites - re-implements window.close, since the built-in function doesn't work correctly */

window.addEventListener('message', function (e) {
  if (e.data === 'close-window') {
    ipc.send('close-window')
  }
})

scriptsToRun.push(`
  window.close = function () {
    postMessage('close-window', '*')
  }
`)

if (window.location.hostname === 'google.com' || window.location.hostname.endsWith('.google.com')) {
  /* define window.chrome
     this is necessary because some websites (such as the Google Drive file viewer, see issue #378) check for a
     Chrome user agent, and then do things like if(chrome.<module>) {}
     so we need to create a chrome object to prevent errors
     (https://github.com/electron/electron/issues/16587)
     */

  scriptsToRun.push(`
    window.chrome = {
      runtime: {
        connect: () => {
          return {
            onMessage: {
              addListener: () => {console.warn('chrome.runtime is not implemented')},
              removeListener: () => {console.warn('chrome.runtime is not implemented')},
            },
            postMessage: () => {console.warn('chrome.runtime is not implemented')},
            disconnect: () => {console.warn('chrome.runtime is not implemented')},
          }
        }
      }
    }
  `)
}

/* drive.google.com - fixes clicking on files to open them */

if (window.location.hostname === 'drive.google.com') {
  scriptsToRun.push(`
    var realWindowOpen = window.open

    window.open = function (url) {
      if (url) {
        return realWindowOpen(url)
      }
      return {
        document: new Proxy({}, {
          get: function () {
            return function () {
              return document.createElement('div')
            }
          },
          set: function () {
            console.warn('unpatched set', arguments)}
        }
        ),
        location: {
          replace: function (location) {
            realWindowOpen(location)
          }
        }
      }
    }
  `)
}

/* news.google.com - fixes clicking on news articles */

if (window.location.hostname === 'news.google.com') {
  scriptsToRun.push(`
    window.open = null
  `)
}

if (scriptsToRun.length > 0) {
  setTimeout(function () {
    electron.webFrame.executeJavaScript(scriptsToRun.join(';'))
  }, 0)
}
;
window.addEventListener('message', function (e) {
  if (!e.origin.startsWith('file://')) {
    return
  }

  if (e.data && e.data.message && e.data.message === 'getSettingsData') {
    ipc.send('getSettingsData')
  }

  if (e.data && e.data.message && e.data.message === 'setSetting') {
    ipc.send('setSetting', {key: e.data.key, value: e.data.value})
  }
})

ipc.on('receiveSettingsData', function (e, data) {
  if (window.location.toString().startsWith('file://')) { // probably redundant, but might as well check
    window.postMessage({message: 'receiveSettingsData', settings: data}, 'file://')
  }
})
;
