'use strict'
module.exports = stdoutToComments

const acorn = require('acorn')
const walk = require('acorn/dist/walk')
const fs = require('fs')
const spawn = require('cross-spawn-async')
const path = require('path')
const SourceMapConsumer = require('source-map').SourceMapConsumer
const normalizePath = require('normalize-path')
const hookPath = normalizePath(path.resolve(__dirname, './hook-console-log'))
const position = require('file-position')
const normalizeNewline = require('normalize-newline')
const consoleStringify = require('./console-stringify')

function stdoutToComments (opts) {
  if (typeof opts === 'string') opts = { code: opts }

  const cwd = opts.cwd || process.cwd()

  return new Promise((resolve, reject) => {
    const content = normalizeNewline(opts.code)

    const tmpFileName = normalizePath(path.resolve(cwd, `_${Math.random()}.js`))
    fs.writeFileSync(tmpFileName, addHook({
      code: content,
      filePath: tmpFileName,
    }), 'utf8')

    const outputs = []
    let failed = false

    const cp = spawn('node', [tmpFileName])
    cp.stdout.setEncoding('utf8')
    cp.stderr.setEncoding('utf8')
    cp.stdout.on('data', data => {
      try {
        data.split('\n$\n')
          .filter(outputJSON => !!outputJSON)
          .map(outputJSON => JSON.parse(outputJSON))
          .forEach(outputInfo => outputs.push(outputInfo))
      } catch (err) {
        failed = true
        reject(err)
      }
    })
    let errData = ''
    cp.stderr.on('data', data => { errData += data })
    cp.on('close', code => {
      fs.unlinkSync(tmpFileName)

      if (errData) {
        return reject(new Error(errData))
      }

      if (failed) {
        return
      }

      if (!opts.map) {
        return resolve(insertOutputsToCode(content, outputs))
      }
      const consumer = new SourceMapConsumer(opts.map)
      const originalOutputs = outputs
        .map(output => Object.assign({}, output, consumer.originalPositionFor({
          line: output.line,
          column: output.column,
        })))
      const sourcesContent = JSON.parse(opts.map).sourcesContent
      const sourceContent = sourcesContent[sourcesContent.length - 1]
      resolve(insertOutputsToCode(sourceContent, originalOutputs))
    })
  })
}

function insertOutputsToCode (content, outputs) {
  const codeLines = splitIntoLines(content)
  const sOutputs = moveOutputsBeloveStatement(outputs, codeLines, content)

  return codeLines.reduce((contentLines, line, index) => {
    contentLines.push(line)
    const lineNo = index + 1
    while (sOutputs.length && sOutputs[0].line === lineNo) {
      const matches = (contentLines[contentLines.length - 1] || '').match(/^(\s*)/)
      const linePadding = matches && matches[0] || ''
      const output = consoleStringify.apply(null, sOutputs.shift().args)
      contentLines.push(outputToComment(output, linePadding))
    }
    return contentLines
  }, []).join('\n')
}

function outputToComment (output, padding) {
  return `${padding}//> ${output.replace(/\r?\n/g, `\n${padding}//  `)}`
}

function moveOutputsBeloveStatement (outputs, codeLines, content) {
  const ast = acorn.parse(content, {locations: true, sourceType: 'module'})
  const getPosition = position(content)

  let semanticLineNo = 0
  const newOutputs = []
  for (let i = 0, lineNo = 0; i < outputs.length; i++) {
    if (outputs[i].line !== lineNo) {
      const pos = getPosition(outputs[i].line - 1, outputs[i].column)
      semanticLineNo = outputSemanticPosition(ast, pos)
    }
    newOutputs[i] = Object.assign(
      {},
      outputs[i],
      {
        line: semanticLineNo,
      }
    )
  }
  return newOutputs
}

function splitIntoLines (txt) {
  return txt.split('\n')
}

function outputSemanticPosition (ast, pos) {
  const node = walk.findNodeAround(ast, pos, 'ExpressionStatement')
  return node.node.loc.end.line
}

function addHook (opts) {
  if (!opts.code.match(/['"]use strict['"]/)) {
    return `require('${hookPath}')('${opts.filePath}');\n` + opts.code
  }
  return `'use strict';require('${hookPath}')('${opts.filePath}');\n` + opts.code
}

@@ -3238,152 +3238,60 @@
 %0A%0A  
-let semanticLineNo = 0%0A  const newOutputs = %5B%5D%0A  for (let i = 0, lineNo = 0; i %3C outputs.length; i++) %7B%0A    if (outputs%5Bi%5D.line !== lineNo)
+return outputs.reduce((semanticOutput, output) =%3E
  %7B%0A
-  
     
@@ -3320,20 +3320,16 @@
 n(output
-s%5Bi%5D
 .line - 
@@ -3337,20 +3337,16 @@
 , output
-s%5Bi%5D
 .column)
@@ -3350,17 +3350,21 @@
 mn)%0A    
- 
+const
  semanti
@@ -3414,30 +3414,37 @@
     
-%7D%0A    newOutputs%5Bi%5D = 
+return semanticOutput.concat(
 Obje
@@ -3480,12 +3480,8 @@
 tput
-s%5Bi%5D
 ,%0A  
@@ -3533,32 +3533,18 @@
    )
+)
 %0A  %7D
-%0A  return newOutputs
+, %5B%5D)
 %0A%7D%0A%0A

use reduse in moveOutputsBeloveStatement