// const jsdiff = require('diff')

const { longestCommonSubstring, longestCommonSubsequence } = require('./util/lcs')
const { isCharNumber } = require('./util/identify')
const StringStream = require('./util/StringStream')
const { tokenizeWords } = require('./util/tokenize')
const { ADD_STRING, REMOVE_STRING, KEEP_STRING, ADD_STRING_COMPACT, REMOVE_STRING_COMPACT, KEEP_STRING_COMPACT } = require('./instructions')


function diffStrings(s1, s2, options={}) {
  // const words = options.words===true || false

  // if (!words) {
    const compact = diffStringsCompact(s1,s2,options)
    // console.log('compact: '+compact)
    return stringDiffToOperations(compact)
  // } else {
  //   const result = jsdiff.diffWords(s1,s2)
  //   console.log(JSON.stringify(result,null,2))
  //   const ops = []
  //   for (let part of result) {  
  //     if (part.added) {
  //       ops.push({[ADD_STRING]:part.value})
  //     } else if (part.removed) {
  //       ops.push({[REMOVE_STRING]:part.value})
  //     } else {
  //       ops.push({[KEEP_STRING]:part.value})
  //     }
  //   }
  //   return ops
  // }
}


function diffStringsCompact(s1, s2, options={}) {

  const words = options.words===true || false
  
  // find LCS
  let info
  if (!words) {
    info = longestCommonSubstring(s1, s2)
  }
  else {
    const w1 = tokenizeWords(s1)
    const w2 = tokenizeWords(s2)
    seq = longestCommonSubsequence(w1, w2)

    if (seq.length==0) {
      info = { offset1: null, offset2: null, length: 0 }
    } else {

      // console.log('seq\n'+JSON.stringify(seq,null,2))

      // convert seq info back to string info
      info = { offset1: 0, offset2: 0, length: 0 }
      for (let i=0; i<seq.offset1; i+=1) {
        info.offset1 += w1[i].length
      }
      info.offset1 += seq.offset1 // spaces
      for (let i=0; i<seq.offset2; i+=1) {
        info.offset2 += w2[i].length
      }
      info.offset2 += seq.offset2 // spaces
      for (let i=seq.offset1; i<seq.offset1+seq.length; i+=1) {
        info.length += w1[i].length
      }
      info.length += seq.length-1 // spaces

      // console.log('info\n'+JSON.stringify(info,null,2))
    }
  }

  // no LCS?
  if (info.length==0) {
    let ret = ''
    s1 = s1.replace(/"/g, '\\"')
    s2 = s2.replace(/"/g, '\\"')
    if (s1.length>0) ret += REMOVE_STRING_COMPACT + '"' + s1 + '"'
    if (s2.length>0) ret += ADD_STRING_COMPACT + '"' + s2 + '"'
    return ret
  }

  const lcs = s1.substring(info.offset1, info.offset1+info.length)

  // LCS indexes
  const ix1 = info.offset1
  const ix2 = info.offset2

  // split  
  const left1 = s1.substring(0, ix1)
  const left2 = s2.substring(0, ix2)
  const right1 = s1.substring(ix1+lcs.length)
  const right2 = s2.substring(ix2+lcs.length)

  // recurse
  const lefts = diffStringsCompact(left1, left2, options)
  const rights = diffStringsCompact(right1, right2, options)

  // combine
  // const ret = lefts+'^'+lcs.length+rights // just the count
  const ret = lefts + KEEP_STRING_COMPACT + '"' + lcs + '"' + rights

  return ret
}


function stringDiffToOperations(diff) {
  const parts = []
  let ix = 0
  const stream = new StringStream(diff)
  while (!stream.atEnd()) {
    const instr = stream.readChar()
    if (instr == KEEP_STRING_COMPACT) { // skip
      // const skipCount = Number(stream.readWhile(ch => isCharNumber(ch)))
      // const skipped = diff.substring(ix,ix+skipCount)
      // parts.push({'^':skipCount})
      stream.readChar('"')
      const kept = stream.readUntil((ch,info) => ch=='"' && info.prev()!='\\')
      stream.readChar('"')
      parts.push({[KEEP_STRING]:kept})
      ix += kept.length
    }
    else if (instr == ADD_STRING_COMPACT) { // add
      stream.readChar('"')
      const added = stream.readUntil((ch,info) => ch=='"' && info.prev()!='\\')
      stream.readChar('"')
      // parts.push(added)
      parts.push({[ADD_STRING]:added})
    }
    else if (instr == REMOVE_STRING_COMPACT) { // remove
      stream.readChar('"')
      const removed = stream.readUntil((ch,info) => ch=='"' && info.prev()!='\\')
      stream.readChar('"')
      ix += removed.length
      parts.push({[REMOVE_STRING]:removed})
    }
    else {
      throw 'bad instruction '+instr
    }
  }
  return parts
}


function forwardString(diff) {
  const parts = []
  let ix = 0
  // stringDiffToOperations(diff).forEach(part => {
  diff.forEach(part => {
    if (KEEP_STRING in part) {
      // const skipCount = part['^']
      // const skipped = text.substring(ix,ix+skipCount)
      const skipped = part[KEEP_STRING]
      parts.push(skipped)
      ix += skipped.length
    }
    else if (ADD_STRING in part) {
      parts.push(part[ADD_STRING])
    }
    else if (REMOVE_STRING in part) {
      ix += part[REMOVE_STRING].length
    }
    else throw 'bad instruction '+JSON.stringify(part)
  })
  // parts.push(text.substring(ix))
  return parts.join('')
}


function backwardString(diff) {
  const parts = []
  let ix = 0
  // stringDiffToOperations(diff).forEach(part => {
  diff.forEach(part => {
    if (KEEP_STRING in part) {
      // const skipCount = part['^']
      // const skipped = text.substring(ix,ix+skipCount)
      const skipped = part[KEEP_STRING]
      parts.push(skipped)
      ix += skipped.length
    }
    else if (ADD_STRING in part) {
      ix += part[ADD_STRING].length
    }
    else if (REMOVE_STRING in part) {
      parts.push(part[REMOVE_STRING])
    }
    else throw 'bad instruction '+JSON.stringify(part)
  })
  // parts.push(text.substring(ix))
  return parts.join('')
}


module.exports = {
  diffStrings,
  diffStringsCompact,
  stringDiffToOperations, 
  forwardString, 
  backwardString,
}

