'use strict'
var trimOffNewlines = require('trim-off-newlines')
var _ = require('lodash')

var CATCH_ALL = /()(.+)/gi
var SCISSOR = '# ------------------------ >8 ------------------------'

function append (src, line) {
  if (src) {
    src += '\n' + line
  } else {
    src = line
  }

  return src
}

function getCommentFilter (char) {
  return function (line) {
    return line.charAt(0) !== char
  }
}

function truncateToScissor (lines) {
  var scissorIndex = lines.indexOf(SCISSOR)

  if (scissorIndex === -1) {
    return lines
  }

  return lines.slice(0, scissorIndex)
}

function getReferences (input, regex) {
  var references = []
  var referenceSentences
  var referenceMatch

  var reApplicable = input.match(regex.references) !== null
    ? regex.references
    : CATCH_ALL

  while ((referenceSentences = reApplicable.exec(input))) {
    var action = referenceSentences[1] || null
    var sentence = referenceSentences[2]

    while ((referenceMatch = regex.referenceParts.exec(sentence))) {
      var owner = null
      var repository = referenceMatch[1] || ''
      var ownerRepo = repository.split('/')

      if (ownerRepo.length > 1) {
        owner = ownerRepo.shift()
        repository = ownerRepo.join('/')
      }

      var reference = {
        action: action,
        owner: owner,
        repository: repository || null,
        issue: referenceMatch[3],
        raw: referenceMatch[0],
        prefix: referenceMatch[2]
      }

      references.push(reference)
    }
  }

  return references
}

function passTrough () {
  return true
}

function parser (raw, options, regex) {
  if (!raw || !raw.trim()) {
    throw new TypeError('Expected a raw commit')
  }

  if (_.isEmpty(options)) {
    throw new TypeError('Expected options')
  }

  if (_.isEmpty(regex)) {
    throw new TypeError('Expected regex')
  }

  var headerMatch
  var mergeMatch
  var currentProcessedField
  var mentionsMatch
  var revertMatch
  var otherFields = {}
  var commentFilter = typeof options.commentChar === 'string'
    ? getCommentFilter(options.commentChar)
    : passTrough

  var rawLines = trimOffNewlines(raw).split(/\r?\n/)
  var lines = truncateToScissor(rawLines).filter(commentFilter)

  var continueNote = false
  var isBody = true
  var headerCorrespondence = _.map(options.headerCorrespondence, function (part) {
    return part.trim()
  })
  var revertCorrespondence = _.map(options.revertCorrespondence, function (field) {
    return field.trim()
  })
  var mergeCorrespondence = _.map(options.mergeCorrespondence, function (field) {
    return field.trim()
  })

  var body = null
  var footer = null
  var header = null
  var mentions = []
  var merge = null
  var notes = []
  var references = []
  var revert = null

  if (lines.length === 0) {
    return {
      body: body,
      footer: footer,
      header: header,
      mentions: mentions,
      merge: merge,
      notes: notes,
      references: references,
      revert: revert,
      scope: null,
      subject: null,
      type: null
    }
  }

  // msg parts
  merge = lines.shift()
  var mergeParts = {}
  var headerParts = {}
  body = ''
  footer = ''

  mergeMatch = merge.match(options.mergePattern)
  if (mergeMatch && options.mergePattern) {
    merge = mergeMatch[0]

    header = lines.shift()
    while (!header.trim()) {
      header = lines.shift()
    }

    _.forEach(mergeCorrespondence, function (partName, index) {
      var partValue = mergeMatch[index + 1] || null
      mergeParts[partName] = partValue
    })
  } else {
    header = merge
    merge = null

    _.forEach(mergeCorrespondence, function (partName) {
      mergeParts[partName] = null
    })
  }

  headerMatch = header.match(options.headerPattern)
  if (headerMatch) {
    _.forEach(headerCorrespondence, function (partName, index) {
      var partValue = headerMatch[index + 1] || null
      headerParts[partName] = partValue
    })
  } else {
    _.forEach(headerCorrespondence, function (partName) {
      headerParts[partName] = null
    })
  }

  Array.prototype.push.apply(references, getReferences(header, {
    references: regex.references,
    referenceParts: regex.referenceParts
  }))

  // body or footer
  _.forEach(lines, function (line) {
    if (options.fieldPattern) {
      var fieldMatch = options.fieldPattern.exec(line)

      if (fieldMatch) {
        currentProcessedField = fieldMatch[1]

        return
      }

      if (currentProcessedField) {
        otherFields[currentProcessedField] = append(otherFields[currentProcessedField], line)

        return
      }
    }

    var referenceMatched

    // this is a new important note
    var notesMatch = line.match(regex.notes)
    if (notesMatch) {
      continueNote = true
      isBody = false
      footer = append(footer, line)

      var note = {
        title: notesMatch[1],
        text: notesMatch[2]
      }

      notes.push(note)

      return
    }

    var lineReferences = getReferences(line, {
      references: regex.references,
      referenceParts: regex.referenceParts
    })

    if (lineReferences.length > 0) {
      isBody = false
      referenceMatched = true
      continueNote = false
    }

    Array.prototype.push.apply(references, lineReferences)

    if (referenceMatched) {
      footer = append(footer, line)

      return
    }

    if (continueNote) {
      notes[notes.length - 1].text = append(notes[notes.length - 1].text, line)
      footer = append(footer, line)

      return
    }

    if (isBody) {
      body = append(body, line)
    } else {
      footer = append(footer, line)
    }
  })

  while ((mentionsMatch = regex.mentions.exec(raw))) {
    mentions.push(mentionsMatch[1])
  }

  // does this commit revert any other commit?
  revertMatch = raw.match(options.revertPattern)
  if (revertMatch) {
    revert = {}
    _.forEach(revertCorrespondence, function (partName, index) {
      var partValue = revertMatch[index + 1] || null
      revert[partName] = partValue
    })
  } else {
    revert = null
  }

  _.map(notes, function (note) {
    note.text = trimOffNewlines(note.text)

    return note
  })

  var msg = _.merge(headerParts, mergeParts, {
    merge: merge,
    header: header,
    body: body ? trimOffNewlines(body) : null,
    footer: footer ? trimOffNewlines(footer) : null,
    notes: notes,
    references: references,
    mentions: mentions,
    revert: revert
  }, otherFields)

  return msg
}

module.exports = parser
