text: 'use strict'
  
  let { SourceMapConsumer, SourceMapGenerator } = require('source-map-js')
  let { dirname, resolve, relative, sep } = require('path')
  let { pathToFileURL } = require('url')
  
  let sourceMapAvailable = Boolean(SourceMapConsumer && SourceMapGenerator)
  let pathAvailable = Boolean(dirname && resolve && relative && sep)
  
  class MapGenerator {
    constructor(stringify, root, opts, cssString) {
      this.stringify = stringify
      this.mapOpts = opts.map || {}
      this.root = root
      this.opts = opts
      this.css = cssString
    }
  
    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
  
      if (!this.root && typeof this.css === 'string') {
        this.css = this.css.replace(/(\n)?\/\*#[\S\s]*?\*\/$/gm, '')
      } else {
        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 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 {
        /* c8 ignore next 2 */
        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'
      }
      /* c8 ignore next 6 */
      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]
    }
  
    generateSimpleMap() {
      this.map = new SourceMapGenerator({ file: this.outputFile() })
      this.previousMaps = []
  
      let source
      if (this.opts.from) {
        source = this.toUrl(this.opts.from)
      } else {
        source = '<no source>'
      }
  
      this.map.addMapping({
        source,
        generated: { line: 1, column: 0 },
        original: { line: 1, column: 0 }
      })
  
      if (this.isAnnotation()) this.addAnnotation()
  
      if (this.isInline()) {
        return [this.css]
      }
  
      return [this.cssString, 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 === '\\') {
        /* c8 ignore next 2 */
        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) {
        if (pathToFileURL) {
          return pathToFileURL(node.source.input.from).toString()
        } else {
          /* c8 ignore next 4 */
          throw new Error(
            '`map.absolute` option is not available in this PostCSS build'
          )
        }
      } else {
        return this.toUrl(this.path(node.source.input.from))
      }
    }
  
    generateString() {
      this.css = ''
      this.map = new SourceMapGenerator({ file: this.outputFile() })
  
      let line = 1
      let column = 1
  
      let noSource = '<no source>'
      let mapping = {
        source: '',
        generated: { line: 0, column: 0 },
        original: { line: 0, column: 0 }
      }
  
      let lines, last
      this.stringify(this.root, (str, node, type) => {
        this.css += str
  
        if (node && type !== 'end') {
          mapping.generated.line = line
          /* c8 ignore next */
          mapping.generated.column = column - 1
          if (node.source && node.source.start) {
            mapping.source = this.sourcePath(node)
            mapping.original.line = node.source.start.line
            mapping.original.column = node.source.start.column - 1
            this.map.addMapping(mapping)
          } else {
            /* c8 ignore next 8 */
            mapping.source = noSource
            mapping.original.line = 1
            mapping.original.column = 0
            this.map.addMapping(mapping)
          }
        }
  
        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) {
              mapping.source = this.sourcePath(node)
              mapping.original.line = node.source.end.line
              mapping.original.column = node.source.end.column - 1
              mapping.generated.line = line
              mapping.generated.column = column - 2
              this.map.addMapping(mapping)
            } else {
              mapping.source = noSource
              mapping.original.line = 1
              mapping.original.column = 0
              mapping.generated.line = line
              mapping.generated.column = column - 1
              this.map.addMapping(mapping)
            }
          }
        }
      })
    }
  
    generate() {
      this.clearAnnotation()
  
      if (pathAvailable && sourceMapAvailable && this.isMap() && !this.root) {
        return this.generateSimpleMap()
      }
  
      if (pathAvailable && sourceMapAvailable && this.isMap()) {
        return this.generateMap()
      }
  
      let result = ''
      this.stringify(this.root, i => {
        result += i
      })
      return [result]
    }
  }
  
  module.exports = MapGenerator
  