const {isArray} = require('core-util-is')
const {sort} = require('array-timsort')

const {
  SYMBOL_PREFIXES,

  UNDEFINED,

  symbol,
  copy_comments,
  swap_comments
} = require('./common')

const reverse_comments = array => {
  const {length} = array
  let i = 0
  const max = length / 2

  for (; i < max; i ++) {
    swap_comments(array, i, length - i - 1)
  }
}

const move_comment = (target, source, i, offset, remove) => {
  copy_comments(target, source, i + offset, i, remove)
}

const move_comments = (
  // `Array` target array
  target,
  // `Array` source array
  source,
  // `number` start index
  start,
  // `number` number of indexes to move
  count,
  // `number` offset to move
  offset,
  // `boolean` whether should remove the comments from source
  remove
) => {
  if (offset > 0) {
    let i = count
    //         |   count   | offset |
    // source: -------------
    // target:          -------------
    //         | remove |
    // => remove === offset

    // From [count - 1, 0]
    while (i -- > 0) {
      move_comment(target, source, start + i, offset, remove)
    }
    return
  }

  let i = 0
  // | remove  |  count    |
  //           -------------
  // -------------
  //             | offset  |

  // From [0, count - 1]
  while (i < count) {
    const ii = i ++
    move_comment(target, source, start + ii, offset, remove)
  }
}

const remove_comments = (array, key) => {
  SYMBOL_PREFIXES.forEach(prefix => {
    const prop = symbol(prefix, key)
    delete array[prop]
  })
}

const get_mapped = (map, key) => {
  let mapped = key

  while (mapped in map) {
    mapped = map[mapped]
  }

  return mapped
}

class CommentArray extends Array {
  // - deleteCount + items.length

  // We should avoid `splice(begin, deleteCount, ...items)`,
  // because `splice(0, undefined)` is not equivalent to `splice(0)`,
  // as well as:
  // - slice
  splice (...args) {
    const {length} = this
    const ret = super.splice(...args)

    // #16
    // If no element removed, we might still need to move comments,
    //   because splice could add new items

    // if (!ret.length) {
    //   return ret
    // }

    // JavaScript syntax is silly
    // eslint-disable-next-line prefer-const
    let [begin, deleteCount, ...items] = args

    if (begin < 0) {
      begin += length
    }

    if (arguments.length === 1) {
      deleteCount = length - begin
    } else {
      deleteCount = Math.min(length - begin, deleteCount)
    }

    const {
      length: item_length
    } = items

    // itemsToDelete: -
    // itemsToAdd: +
    //        |    dc      |  count   |
    // =======-------------============
    // =======++++++============
    //        | il |
    const offset = item_length - deleteCount
    const start = begin + deleteCount
    const count = length - start

    move_comments(this, this, start, count, offset, true)

    return ret
  }

  slice (...args) {
    const {length} = this
    const array = super.slice(...args)
    if (!array.length) {
      return new CommentArray()
    }

    let [begin, before] = args

    // Ref:
    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice
    if (before === UNDEFINED) {
      before = length
    } else if (before < 0) {
      before += length
    }

    if (begin < 0) {
      begin += length
    } else if (begin === UNDEFINED) {
      begin = 0
    }

    move_comments(array, this, begin, before - begin, - begin)

    return array
  }

  unshift (...items) {
    const {length} = this
    const ret = super.unshift(...items)
    const {
      length: items_length
    } = items

    if (items_length > 0) {
      move_comments(this, this, 0, length, items_length, true)
    }

    return ret
  }

  shift () {
    const ret = super.shift()
    const {length} = this

    remove_comments(this, 0)
    move_comments(this, this, 1, length, - 1, true)

    return ret
  }

  reverse () {
    super.reverse()

    reverse_comments(this)

    return this
  }

  pop () {
    const ret = super.pop()

    // Removes comments
    remove_comments(this, this.length)

    return ret
  }

  concat (...items) {
    let {length} = this
    const ret = super.concat(...items)

    if (!items.length) {
      return ret
    }

    move_comments(ret, this, 0, this.length, 0)

    items.forEach(item => {
      const prev = length
      length += isArray(item)
        ? item.length
        : 1

      if (!(item instanceof CommentArray)) {
        return
      }

      move_comments(ret, item, 0, item.length, prev)
    })

    return ret
  }

  sort (...args) {
    const result = sort(
      this,
      // Make sure there is no more than one argument
      ...args.slice(0, 1)
    )

    // For example,
    // if we sort ['b', 'd', 'c', 'a'],
    // then `result` will be [3, 0, 2, 1], and the array is ['a', 'b', 'c', 'd']

    // First, we swap index 0 (b) and index 3 (a), then the array comments are
    // ['a.comments', 'd.comments', 'c.comments', 'b.comments']
    // index 0 is finalized
    // index 3 is actually mapped to original index 0, we present as 0 -> 3

    // Then swap index 1 (d) and index 0 (-> 3, b)
    // 1 (index) -> 0 (new index) -> 3 (real_index)
    // ['d.comments', 'b.comments', 'c.comments', 'd.comments']
    // index 1 is finalized
    // index 3 is contains the item of original index 1
    // - we present as 1 -> 3
    // - it is ok that we don't remove mapping 0 -> 3

    // Then index 2 should be skipped

    // Then swap index 3 (d) and index 1 (-> 3, b), skipped

    const map = Object.create(null)

    result.forEach((source_index, index) => {
      if (source_index === index) {
        return
      }

      const real_source_index = get_mapped(map, source_index)

      if (real_source_index === index) {
        return
      }

      // The item of index `index` gets the final value
      // delete map[index]
      map[index] = real_source_index

      swap_comments(this, index, real_source_index)
    })

    return this
  }
}

module.exports = {
  CommentArray
}
