'use strict'

const path = require('path')
const execa = require('execa')
const gStatus = require('g-status')
const del = require('del')
const debug = require('debug')('lint-staged:git')
const resolveGitDir = require('./resolveGitDir')

let workingCopyTree = null
let indexTree = null
let formattedIndexTree = null

function getAbsolutePath(dir) {
  return path.isAbsolute(dir) ? dir : path.resolve(dir)
}

async function execGit(cmd, options) {
  const cwd = options && options.cwd ? options.cwd : resolveGitDir()
  debug('Running git command', cmd)
  try {
    const { stdout } = await execa('git', [].concat(cmd), {
      ...options,
      cwd: getAbsolutePath(cwd)
    })
    return stdout
  } catch (err) {
    throw new Error(err)
  }
}

async function writeTree(options) {
  return execGit(['write-tree'], options)
}

async function getDiffForTrees(tree1, tree2, options) {
  debug(`Generating diff between trees ${tree1} and ${tree2}...`)
  return execGit(
    [
      'diff-tree',
      '--ignore-submodules',
      '--binary',
      '--no-color',
      '--no-ext-diff',
      '--unified=0',
      tree1,
      tree2
    ],
    options
  )
}

async function hasPartiallyStagedFiles(options) {
  const cwd = options && options.cwd ? options.cwd : resolveGitDir()
  const files = await gStatus({ cwd })
  const partiallyStaged = files.filter(
    file =>
      file.index !== ' ' &&
      file.workingTree !== ' ' &&
      file.index !== '?' &&
      file.workingTree !== '?'
  )
  return partiallyStaged.length > 0
}

// eslint-disable-next-line
async function gitStashSave(options) {
  debug('Stashing files...')
  // Save ref to the current index
  indexTree = await writeTree(options)
  // Add working copy changes to index
  await execGit(['add', '.'], options)
  // Save ref to the working copy index
  workingCopyTree = await writeTree(options)
  // Restore the current index
  await execGit(['read-tree', indexTree], options)
  // Remove all modifications
  await execGit(['checkout-index', '-af'], options)
  // await execGit(['clean', '-dfx'], options)
  debug('Done stashing files!')
  return [workingCopyTree, indexTree]
}

async function updateStash(options) {
  formattedIndexTree = await writeTree(options)
  return formattedIndexTree
}

async function applyPatchFor(tree1, tree2, options) {
  const diff = await getDiffForTrees(tree1, tree2, options)
  /**
   * This is crucial for patch to work
   * For some reason, git-apply requires that the patch ends with the newline symbol
   * See http://git.661346.n2.nabble.com/Bug-in-Git-Gui-Creates-corrupt-patch-td2384251.html
   * and https://stackoverflow.com/questions/13223868/how-to-stage-line-by-line-in-git-gui-although-no-newline-at-end-of-file-warnin
   */
  // TODO: Figure out how to test this. For some reason tests were working but in the real env it was failing
  const patch = `${diff}\n` // TODO: This should also work on Windows but test would be good
  if (patch) {
    try {
      /**
       * Apply patch to index. We will apply it with --reject so it it will try apply hunk by hunk
       * We're not interested in failied hunks since this mean that formatting conflicts with user changes
       * and we prioritize user changes over formatter's
       */
      await execGit(
        ['apply', '-v', '--whitespace=nowarn', '--reject', '--recount', '--unidiff-zero'],
        {
          ...options,
          input: patch
        }
      )
    } catch (err) {
      debug('Could not apply patch to the stashed files cleanly')
      debug(err)
      debug('Patch content:')
      debug(patch)
      throw new Error('Could not apply patch to the stashed files cleanly.', err)
    }
  }
}

async function gitStashPop(options) {
  if (workingCopyTree === null) {
    throw new Error('Trying to restore from stash but could not find working copy stash.')
  }

  debug('Restoring working copy')
  // Restore the stashed files in the index
  await execGit(['read-tree', workingCopyTree], options)
  // and sync it to the working copy (i.e. update files on fs)
  await execGit(['checkout-index', '-af'], options)

  // Then, restore the index after working copy is restored
  if (indexTree !== null && formattedIndexTree === null) {
    // Restore changes that were in index if there are no formatting changes
    debug('Restoring index')
    await execGit(['read-tree', indexTree], options)
  } else {
    /**
     * There are formatting changes we want to restore in the index
     * and in the working copy. So we start by restoring the index
     * and after that we'll try to carry as many as possible changes
     * to the working copy by applying the patch with --reject option.
     */
    debug('Restoring index with formatting changes')
    await execGit(['read-tree', formattedIndexTree], options)
    try {
      await applyPatchFor(indexTree, formattedIndexTree, options)
    } catch (err) {
      debug(
        'Found conflicts between formatters and local changes. Formatters changes will be ignored for conflicted hunks.'
      )
      /**
       * Clean up working directory from *.rej files that contain conflicted hanks.
       * These hunks are coming from formatters so we'll just delete them since they are irrelevant.
       */
      try {
        const rejFiles = await del(['*.rej'], options)
        debug('Deleted files and folders:\n', rejFiles.join('\n'))
      } catch (delErr) {
        debug('Error deleting *.rej files', delErr)
      }
    }
  }
  // Clean up references
  workingCopyTree = null
  indexTree = null
  formattedIndexTree = null

  return null
}

module.exports = {
  execGit,
  gitStashSave,
  gitStashPop,
  hasPartiallyStagedFiles,
  updateStash
}
