const v4Regex = /^(\d{1,3}\.){3,3}\d{1,3}$/
const v4Size = 4
const v6Regex = /^(::)?(((\d{1,3}\.){3}(\d{1,3}){1})?([0-9a-f]){0,4}:{0,2}){1,8}(::)?$/i
const v6Size = 16

const v4 = {
  name: 'v4',
  size: v4Size,
  isFormat: ip => v4Regex.test(ip),
  encode (ip, buff, offset) {
    offset = ~~offset
    buff = buff || new Uint8Array(offset + v4Size)
    const max = ip.length
    let n = 0
    for (let i = 0; i < max;) {
      const c = ip.charCodeAt(i++)
      if (c === 46) { // "."
        buff[offset++] = n
        n = 0
      } else {
        n = n * 10 + (c - 48)
      }
    }
    buff[offset] = n
    return buff
  },
  decode (buff, offset) {
    offset = ~~offset
    return `${buff[offset++]}.${buff[offset++]}.${buff[offset++]}.${buff[offset]}`
  }
}

const v6 = {
  name: 'v6',
  size: v6Size,
  isFormat: ip => ip.length > 0 && v6Regex.test(ip),
  encode (ip, buff, offset) {
    offset = ~~offset
    let end = offset + v6Size
    let fill = -1
    let hexN = 0
    let decN = 0
    let prevColon = true
    let useDec = false
    buff = buff || new Uint8Array(offset + v6Size)
    // Note: This algorithm needs to check if the offset
    // could exceed the buffer boundaries as it supports
    // non-standard compliant encodings that may go beyond
    // the boundary limits. if (offset < end) checks should
    // not be necessary...
    for (let i = 0; i < ip.length; i++) {
      let c = ip.charCodeAt(i)
      if (c === 58) { // :
        if (prevColon) {
          if (fill !== -1) {
            // Not Standard! (standard doesn't allow multiple ::)
            // We need to treat
            if (offset < end) buff[offset] = 0
            if (offset < end - 1) buff[offset + 1] = 0
            offset += 2
          } else if (offset < end) {
            // :: in the middle
            fill = offset
          }
        } else {
          // : ends the previous number
          if (useDec === true) {
            // Non-standard! (ipv4 should be at end only)
            // A ipv4 address should not be found anywhere else but at
            // the end. This codec also support putting characters
            // after the ipv4 address..
            if (offset < end) buff[offset] = decN
            offset++
          } else {
            if (offset < end) buff[offset] = hexN >> 8
            if (offset < end - 1) buff[offset + 1] = hexN & 0xff
            offset += 2
          }
          hexN = 0
          decN = 0
        }
        prevColon = true
        useDec = false
      } else if (c === 46) { // . indicates IPV4 notation
        if (offset < end) buff[offset] = decN
        offset++
        decN = 0
        hexN = 0
        prevColon = false
        useDec = true
      } else {
        prevColon = false
        if (c >= 97) {
          c -= 87 // a-f ... 97~102 -87 => 10~15
        } else if (c >= 65) {
          c -= 55 // A-F ... 65~70 -55 => 10~15
        } else {
          c -= 48 // 0-9 ... starting from charCode 48
          decN = decN * 10 + c
        }
        // We don't know yet if its a dec or hex number
        hexN = (hexN << 4) + c
      }
    }
    if (prevColon === false) {
      // Commiting last number
      if (useDec === true) {
        if (offset < end) buff[offset] = decN
        offset++
      } else {
        if (offset < end) buff[offset] = hexN >> 8
        if (offset < end - 1) buff[offset + 1] = hexN & 0xff
        offset += 2
      }
    } else if (fill === 0) {
      // Not Standard! (standard doesn't allow multiple ::)
      // This means that a : was found at the start AND end which means the
      // end needs to be treated as 0 entry...
      if (offset < end) buff[offset] = 0
      if (offset < end - 1) buff[offset + 1] = 0
      offset += 2
    } else if (fill !== -1) {
      // Non-standard! (standard doens't allow multiple ::)
      // Here we find that there has been a :: somewhere in the middle
      // and the end. To treat the end with priority we need to move all
      // written data two bytes to the right.
      offset += 2
      for (let i = Math.min(offset - 1, end - 1); i >= fill + 2; i--) {
        buff[i] = buff[i - 2]
      }
      buff[fill] = 0
      buff[fill + 1] = 0
      fill = offset
    }
    if (fill !== offset && fill !== -1) {
      // Move the written numbers to the end while filling the everything
      // "fill" to the bytes with zeros.
      if (offset > end - 2) {
        // Non Standard support, when the cursor exceeds bounds.
        offset = end - 2
      }
      while (end > fill) {
        buff[--end] = offset < end && offset > fill ? buff[--offset] : 0
      }
    } else {
      // Fill the rest with zeros
      while (offset < end) {
        buff[offset++] = 0
      }
    }
    return buff
  },
  decode (buff, offset) {
    offset = ~~offset
    let result = ''
    for (let i = 0; i < v6Size; i += 2) {
      if (i !== 0) {
        result += ':'
      }
      result += (buff[offset + i] << 8 | buff[offset + i + 1]).toString(16)
    }
    return result
      .replace(/(^|:)0(:0)*:0(:|$)/, '$1::$3')
      .replace(/:{3,4}/, '::')
  }
}

function sizeOf (ip) {
  if (v4.isFormat(ip)) return v4.size
  if (v6.isFormat(ip)) return v6.size
  throw Error(`Invalid ip address: ${ip}`)
}

module.exports = Object.freeze({
  name: 'ip',
  sizeOf,
  familyOf: string => sizeOf(string) === v4.size ? 1 : 2,
  v4,
  v6,
  encode (ip, buff, offset) {
    offset = ~~offset
    const size = sizeOf(ip)
    if (typeof buff === 'function') {
      buff = buff(offset + size)
    }
    if (size === v4.size) {
      return v4.encode(ip, buff, offset)
    }
    return v6.encode(ip, buff, offset)
  },
  decode (buff, offset, length) {
    offset = ~~offset
    length = length || (buff.length - offset)
    if (length === v4.size) {
      return v4.decode(buff, offset, length)
    }
    if (length === v6.size) {
      return v6.decode(buff, offset, length)
    }
    throw Error(`Invalid buffer size needs to be ${v4.size} for v4 or ${v6.size} for v6.`)
  }
})
