'use strict'

let { dirname, resolve, relative, sep } = require('path')
let { pathToFileURL } = require('url')
let mozilla = require('source-map')

class MapGenerator {
  constructor (stringify, root, opts) {
    this.stringify = stringify
    this.mapOpts = opts.map || {}
    this.root = root
    this.opts = opts
  }

  isMap () {
    if (typeof this.opts.map !== 'undefined') {
      return !!this.opts.map
    }
    return this.previous().length > 0
  }

  previous () {
    if (!this.previousMaps) {
      this.previousMaps = []
      this.root.walk(node => {
        if (node.source && node.source.input.map) {
          let map = node.source.input.map
          if (!this.previousMaps.includes(map)) {
            this.previousMaps.push(map)
          }
        }
      })
    }

    return this.previousMaps
  }

  isInline () {
    if (typeof this.mapOpts.inline !== 'undefined') {
      return this.mapOpts.inline
    }

    let annotation = this.mapOpts.annotation
    if (typeof annotation !== 'undefined' && annotation !== true) {
      return false
    }

    if (this.previous().length) {
      return this.previous().some(i => i.inline)
    }
    return true
  }

  isSourcesContent () {
    if (typeof this.mapOpts.sourcesContent !== 'undefined') {
      return this.mapOpts.sourcesContent
    }
    if (this.previous().length) {
      return this.previous().some(i => i.withContent())
    }
    return true
  }

  clearAnnotation () {
    if (this.mapOpts.annotation === false) return

    let node
    for (let i = this.root.nodes.length - 1; i >= 0; i--) {
      node = this.root.nodes[i]
      if (node.type !== 'comment') continue
      if (node.text.indexOf('# sourceMappingURL=') === 0) {
        this.root.removeChild(i)
      }
    }
  }

  setSourcesContent () {
    let already = {}
    this.root.walk(node => {
      if (node.source) {
        let from = node.source.input.from
        if (from && !already[from]) {
          already[from] = true
          this.map.setSourceContent(
            this.toUrl(this.path(from)),
            node.source.input.css
          )
        }
      }
    })
  }

  applyPrevMaps () {
    for (let prev of this.previous()) {
      let from = this.toUrl(this.path(prev.file))
      let root = prev.root || dirname(prev.file)
      let map

      if (this.mapOpts.sourcesContent === false) {
        map = new mozilla.SourceMapConsumer(prev.text)
        if (map.sourcesContent) {
          map.sourcesContent = map.sourcesContent.map(() => null)
        }
      } else {
        map = prev.consumer()
      }

      this.map.applySourceMap(map, from, this.toUrl(this.path(root)))
    }
  }

  isAnnotation () {
    if (this.isInline()) {
      return true
    }
    if (typeof this.mapOpts.annotation !== 'undefined') {
      return this.mapOpts.annotation
    }
    if (this.previous().length) {
      return this.previous().some(i => i.annotation)
    }
    return true
  }

  toBase64 (str) {
    if (Buffer) {
      return Buffer.from(str).toString('base64')
    } else {
      // istanbul ignore next
      return window.btoa(unescape(encodeURIComponent(str)))
    }
  }

  addAnnotation () {
    let content

    if (this.isInline()) {
      content =
        'data:application/json;base64,' + this.toBase64(this.map.toString())
    } else if (typeof this.mapOpts.annotation === 'string') {
      content = this.mapOpts.annotation
    } else if (typeof this.mapOpts.annotation === 'function') {
      content = this.mapOpts.annotation(this.opts.to, this.root)
    } else {
      content = this.outputFile() + '.map'
    }

    let eol = '\n'
    if (this.css.includes('\r\n')) eol = '\r\n'

    this.css += eol + '/*# sourceMappingURL=' + content + ' */'
  }

  outputFile () {
    if (this.opts.to) {
      return this.path(this.opts.to)
    }
    if (this.opts.from) {
      return this.path(this.opts.from)
    }
    return 'to.css'
  }

  generateMap () {
    this.generateString()
    if (this.isSourcesContent()) this.setSourcesContent()
    if (this.previous().length > 0) this.applyPrevMaps()
    if (this.isAnnotation()) this.addAnnotation()

    if (this.isInline()) {
      return [this.css]
    }
    return [this.css, this.map]
  }

  path (file) {
    if (file.indexOf('<') === 0) return file
    if (/^\w+:\/\//.test(file)) return file
    if (this.mapOpts.absolute) return file

    let from = this.opts.to ? dirname(this.opts.to) : '.'

    if (typeof this.mapOpts.annotation === 'string') {
      from = dirname(resolve(from, this.mapOpts.annotation))
    }

    file = relative(from, file)
    return file
  }

  toUrl (path) {
    if (sep === '\\') {
      // istanbul ignore next
      path = path.replace(/\\/g, '/')
    }
    return encodeURI(path).replace(/[#?]/g, encodeURIComponent)
  }

  sourcePath (node) {
    if (this.mapOpts.from) {
      return this.toUrl(this.mapOpts.from)
    } else if (this.mapOpts.absolute) {
      return pathToFileURL(node.source.input.from).toString()
    } else {
      return this.toUrl(this.path(node.source.input.from))
    }
  }

  generateString () {
    this.css = ''
    this.map = new mozilla.SourceMapGenerator({ file: this.outputFile() })

    let line = 1
    let column = 1

    let lines, last
    this.stringify(this.root, (str, node, type) => {
      this.css += str

      if (node && type !== 'end') {
        if (node.source && node.source.start) {
          this.map.addMapping({
            source: this.sourcePath(node),
            generated: { line, column: column - 1 },
            original: {
              line: node.source.start.line,
              column: node.source.start.column - 1
            }
          })
        } else {
          this.map.addMapping({
            source: '<no source>',
            original: { line: 1, column: 0 },
            generated: { line, column: column - 1 }
          })
        }
      }

      lines = str.match(/\n/g)
      if (lines) {
        line += lines.length
        last = str.lastIndexOf('\n')
        column = str.length - last
      } else {
        column += str.length
      }

      if (node && type !== 'start') {
        let p = node.parent || { raws: {} }
        if (node.type !== 'decl' || node !== p.last || p.raws.semicolon) {
          if (node.source && node.source.end) {
            this.map.addMapping({
              source: this.sourcePath(node),
              generated: { line, column: column - 2 },
              original: {
                line: node.source.end.line,
                column: node.source.end.column - 1
              }
            })
          } else {
            this.map.addMapping({
              source: '<no source>',
              original: { line: 1, column: 0 },
              generated: { line, column: column - 1 }
            })
          }
        }
      }
    })
  }

  generate () {
    this.clearAnnotation()

    if (this.isMap()) {
      return this.generateMap()
    }

    let result = ''
    this.stringify(this.root, i => {
      result += i
    })
    return [result]
  }
}

module.exports = MapGenerator
