const breakword = require("breakword")
const stripansi = require("strip-ansi")
const wcwidth = require("wcwidth")
const flat = require("array.prototype.flat")
if (!Array.prototype.flat) flat.shim()

const ANSIPattern = [
  "[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)",
  "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))"
].join("|")
const ANSIRegex = new RegExp(ANSIPattern, "g")

const defaults = () => {
  let obj = {}

  obj.breakword = false
  obj.input = [] // input string split by whitespace
  obj.minWidth = 2 // fallback to if width set too narrow
  obj.paddingLeft = 0
  obj.paddingRight = 0
  obj.errorChar = "�"
  obj.returnFormat = "string" // or 'array'
  obj.skipPadding = false // set to true when padding set too wide for line length
  obj.splitAt = [" ", "\t"]
  obj.trim = true
  obj.width = 10

  return obj
}


const calculateSpaceRemaining = function(lineLength, spacesUsed, config) {
  return Math.max(lineLength - spacesUsed - config.paddingLeft - config.paddingRight, 0)
} // function to set starting line length


const validateInput = (text, options) => {

  // options validation
  let config = Object.assign({}, defaults(), options || {})

  if (config.errorChar) {
    // only allow a single errorChar
    config.errorChar = config.errorChar.split("")[0]

    // errorChar must not be wide character
    if (wcwidth(config.errorChar) > 1)
      throw new Error(`Error character cannot be a wide character (${config.errorChar})`)
  }

  // make sure correct sign on padding
  config.paddingLeft = Math.abs(config.paddingLeft)
  config.paddingRight = Math.abs(config.paddingRight)

  let lineLength = config.width
    - config.paddingLeft
    - config.paddingRight

  if(lineLength < config.minWidth) {
    // skip padding if lineLength too narrow
    config.skipPadding = true
    lineLength = config.minWidth
  }

  // to trim or not to trim...
  if(config.trim) {
    text = text.trim()
  }

  return { text, config, lineLength }
}


const wrap = (input, options) => {

  let { text, config, lineLength } = validateInput(input, options)

  // array of characters split by whitespace and/or tabs
  let words = []

  if(!config.breakword) {
    // break string into words
    if(config.splitAt.indexOf("\t")!==-1) {
      // split at both spaces and tabs
      words = text.split(/ |\t/i)
    } else{
      // split at whitespace
      words = text.split(" ")
    }
  } else {
    // do not break string into words
    words = [text]
  }

  // remove empty array elements
  words = words.filter(val => {
    if (val.length > 0) {
      return true
    }
  })

  // assume at least one line
  let lines = [
    []
  ]

  let spaceRemaining, splitIndex, word
  let currentLine = 0 // index of current line in 'lines[]'
  let spacesUsed = 0 // spaces used so far on current line

  while(words.length > 0) {
    spaceRemaining = calculateSpaceRemaining(lineLength, spacesUsed, config)
    word = words.shift()
    let wordLength = wcwidth(word)

    switch(true) {

      // too long for an empty line and is a single character
      case(lineLength < wordLength && [...word].length === 1):
        words.unshift(config.errorChar)
        break

        // too long for an empty line, must be broken between 2 lines
      case(lineLength < wordLength):
        // break it, then re-insert its parts into words
        // so can loop back to re-handle each word
        splitIndex = breakword(word, lineLength)
        let splitWord = [...word]
        words.unshift(splitWord.slice(0, splitIndex + 1).join(""))
        words.splice(1, 0, splitWord.slice(splitIndex + 1).join("")) // +1 for substr fn
        break

      // not enough space remaining in line, must be wrapped to next line
      case(spaceRemaining < wordLength):
        // add a new line to our array of lines
        lines.push([])
        // note carriage to new line in counter
        currentLine++
        // reset the spacesUsed to 0
        spacesUsed = 0
        /* falls through */

      // fits on current line
      // eslint-disable-next-line
      default:
        // add word to line
        lines[currentLine].push(word)
        // reduce space remaining (add a space between words)
        spacesUsed += wordLength + 1
    }
  }

  lines = lines.map( line => {

    // restore spaces to line
    line = line.join(" ")

    // add padding to ends of line
    if(!config.skipPadding) {
      line = Array(config.paddingLeft + 1).join(" ")
        + line
        + Array(config.paddingRight + 1).join(" ")
    }

    return line
  })

  return lines.join("\n")
}


const splitAnsiInput = (text) => {
  // get start and end positions for matches
  let matches = []
  let textArr = [...text]
  let textLength = textArr.length

  /* eslint-disable */
  while((result = ANSIRegex.exec(text)) !== null) {
    matches.push({
      start: result.index,
      end: result.index + result[0].length,
      match: result[0],
      length: result[0].length
    })
  }
  /* eslint-enable */


  if (matches.length < 1) return [] // we have no ANSI escapes, we're done here

  // add start and end positions for non matches
  matches = matches.reduce((prev, curr) => {
    // check if space exists between this and last match
    // get end of previous match
    let prevEnd = prev[prev.length -1]

    if (prevEnd.end < curr.start) {
      // insert placeholder
      prev.push({
        start: prevEnd.end,
        end: curr.start,
        length: curr.start - prevEnd.end,
        expand: true
      }, curr)
    } else {
      prev.push(curr)
    }
    return prev
  }, [{start: 0, end: 0}])
    .splice(1) // removes starting accumulator object


  // add trailing match if necessary
  let lastMatchEnd = matches[matches.length - 1].end
  if (lastMatchEnd < textLength) {
    matches.push({
      start: lastMatchEnd,
      end: textLength,
      expand: true
    })
  }


  let savedArr = matches.map(match => {
    let value = text.substring(match.start, match.end)
    return (match.expand) ? [...value] : [value]
  }).flat(2)

  return savedArr
}


const restoreANSI = (savedArr, processedArr) => {
  return processedArr.map((char) => {
    let result

    if (char === "\n") {
      result = [char]
    } else {
      // add everything saved before character match
      let splicePoint = savedArr.findIndex(element => element === char ) + 1
      result = savedArr.splice(0, splicePoint)
    }

    // add all following, consecutive closing tags in case linebreak inerted next
    const ANSIClosePattern = "^\\x1b\\[([0-9]+)*m"
    const ANSICloseRegex = new RegExp(ANSIClosePattern) // eslint-disable-line no-control-regex
    const closeCodes = ["0", "21", "22", "23", "24", "25", "27", "28", "29", "39", "49", "54", "55"]

    let match
    while (savedArr.length && (match = savedArr[0].match(ANSICloseRegex))) {
      if (!closeCodes.includes(match[1])) break
      result.push(savedArr.shift())
    }

    return result.join("")
  }).concat(savedArr)
}


module.exports = (input, options) => {

  // process each existing line separately to respect existing line breaks
  const processedLines = input.toString().split("\n").map( string => {

    // save input ANSI escape codes to be restored later
    const savedANSI = splitAnsiInput(string)

    // strip ANSI
    string = stripansi(string)

    // add newlines to string
    string = wrap(string, options)

    // convert into array of characters
    let charArr = [...string]

    // restore input ANSI escape codes
    charArr = (savedANSI.length > 0) ? restoreANSI(savedANSI, charArr) : charArr

    // convert array of single characters into array of lines
    let outArr  = charArr.join("").split("\n")

    return outArr
  })

  return processedLines.flat(2).join("\n")
}
