import { basename } from 'node:path'
import type { HeaderData } from './header.js'
import { Header } from './header.js'

export class Pax implements HeaderData {
  atime?: Date
  mtime?: Date
  ctime?: Date

  charset?: string
  comment?: string

  gid?: number
  uid?: number

  gname?: string
  uname?: string
  linkpath?: string
  dev?: number
  ino?: number
  nlink?: number
  path?: string
  size?: number
  mode?: number

  global: boolean

  constructor(obj: HeaderData, global: boolean = false) {
    this.atime = obj.atime
    this.charset = obj.charset
    this.comment = obj.comment
    this.ctime = obj.ctime
    this.dev = obj.dev
    this.gid = obj.gid
    this.global = global
    this.gname = obj.gname
    this.ino = obj.ino
    this.linkpath = obj.linkpath
    this.mtime = obj.mtime
    this.nlink = obj.nlink
    this.path = obj.path
    this.size = obj.size
    this.uid = obj.uid
    this.uname = obj.uname
  }

  encode() {
    const body = this.encodeBody()
    if (body === '') {
      return Buffer.allocUnsafe(0)
    }

    const bodyLen = Buffer.byteLength(body)
    // round up to 512 bytes
    // add 512 for header
    const bufLen = 512 * Math.ceil(1 + bodyLen / 512)
    const buf = Buffer.allocUnsafe(bufLen)

    // 0-fill the header section, it might not hit every field
    for (let i = 0; i < 512; i++) {
      buf[i] = 0
    }

    new Header({
      // XXX split the path
      // then the path should be PaxHeader + basename, but less than 99,
      // prepend with the dirname
      /* c8 ignore start */
      path: ('PaxHeader/' + basename(this.path ?? '')).slice(0, 99),
      /* c8 ignore stop */
      mode: this.mode || 0o644,
      uid: this.uid,
      gid: this.gid,
      size: bodyLen,
      mtime: this.mtime,
      type: this.global ? 'GlobalExtendedHeader' : 'ExtendedHeader',
      linkpath: '',
      uname: this.uname || '',
      gname: this.gname || '',
      devmaj: 0,
      devmin: 0,
      atime: this.atime,
      ctime: this.ctime,
    }).encode(buf)

    buf.write(body, 512, bodyLen, 'utf8')

    // null pad after the body
    for (let i = bodyLen + 512; i < buf.length; i++) {
      buf[i] = 0
    }

    return buf
  }

  encodeBody() {
    return (
      this.encodeField('path') +
      this.encodeField('ctime') +
      this.encodeField('atime') +
      this.encodeField('dev') +
      this.encodeField('ino') +
      this.encodeField('nlink') +
      this.encodeField('charset') +
      this.encodeField('comment') +
      this.encodeField('gid') +
      this.encodeField('gname') +
      this.encodeField('linkpath') +
      this.encodeField('mtime') +
      this.encodeField('size') +
      this.encodeField('uid') +
      this.encodeField('uname')
    )
  }

  encodeField(field: keyof Pax): string {
    if (this[field] === undefined) {
      return ''
    }
    const r = this[field]
    const v = r instanceof Date ? r.getTime() / 1000 : r
    const s =
      ' ' +
      (field === 'dev' || field === 'ino' || field === 'nlink' ?
        'SCHILY.'
      : '') +
      field +
      '=' +
      v +
      '\n'
    const byteLen = Buffer.byteLength(s)
    // the digits includes the length of the digits in ascii base-10
    // so if it's 9 characters, then adding 1 for the 9 makes it 10
    // which makes it 11 chars.
    let digits = Math.floor(Math.log(byteLen) / Math.log(10)) + 1
    if (byteLen + digits >= Math.pow(10, digits)) {
      digits += 1
    }
    const len = digits + byteLen
    return len + s
  }

  static parse(str: string, ex?: HeaderData, g: boolean = false) {
    return new Pax(merge(parseKV(str), ex), g)
  }
}

const merge = (a: HeaderData, b?: HeaderData) =>
  b ? Object.assign({}, b, a) : a

const parseKV = (str: string) =>
  str
    .replace(/\n$/, '')
    .split('\n')
    .reduce(parseKVLine, Object.create(null))

const parseKVLine = (
  set: Record<string, string | number | Date>,
  line: string,
) => {
  const n = parseInt(line, 10)

  // XXX Values with \n in them will fail this.
  // Refactor to not be a naive line-by-line parse.
  if (n !== Buffer.byteLength(line) + 1) {
    return set
  }

  line = line.slice((n + ' ').length)
  const kv = line.split('=')
  const r = kv.shift()

  if (!r) {
    return set
  }

  const k = r.replace(/^SCHILY\.(dev|ino|nlink)/, '$1')

  const v = kv.join('=').replace(/\0.*/, '')
  switch (k) {
    case 'path':
    case 'linkpath':
    case 'type':
    case 'charset':
    case 'comment':
    case 'gname':
    case 'uname':
      set[k] = v
      break
    case 'ctime':
    case 'atime':
    case 'mtime':
      set[k] = new Date(Number(v) * 1000)
      break
    case 'size':
      const s = +v
      if (s >= 0) set[k] = s
      break
    case 'gid':
    case 'uid':
    case 'dev':
    case 'ino':
    case 'nlink':
    case 'mode':
      set[k] = +v
      break
  }
  return set
}
