#!/usr/bin/env node

import process from 'process'
import { decode, encode } from '../cborg.js'
import { tokensToDiagnostic, fromDiag } from './diagnostic.js'
import { fromHex as _fromHex, toHex } from './byte-utils.js'

/**
 * @param {number} code
 */
function usage (code) {
  console.error('Usage: cborg <command> <args>')
  console.error('Valid commands:')
  console.error('\tbin2diag [--width <width>] [binary input]')
  console.error('\tbin2hex [binary input]')
  console.error('\tbin2json [--pretty] [binary input]')
  console.error('\tdiag2bin [diagnostic input]')
  console.error('\tdiag2hex [diagnostic input]')
  console.error('\tdiag2json [--pretty] [diagnostic input]')
  console.error('\thex2bin [hex input]')
  console.error('\thex2diag [--width <width>] [hex input]')
  console.error('\thex2json [--pretty] [hex input]')
  console.error('\tjson2bin \'[json input]\'')
  console.error('\tjson2diag [--width <width>] \'[json input]\'')
  console.error('\tjson2hex \'[json input]\'')
  console.error('Input may either be supplied as an argument or piped via stdin')
  process.exit(code || 0)
}

async function fromStdin () {
  const chunks = []
  for await (const chunk of process.stdin) {
    chunks.push(chunk)
  }
  return Buffer.concat(chunks)
}

/**
 * @param {string} str
 * @returns {Uint8Array}
 */
function fromHex (str) {
  str = str.replace(/\r?\n/g, '') // let's be charitable
  if (!(/^([0-9a-f]{2})*$/i).test(str)) {
    throw new Error('Input string is not hexadecimal format')
  }
  return _fromHex(str)
}

function argvPretty () {
  const argv = process.argv.filter((s) => s !== '--pretty')
  const pretty = argv.length !== process.argv.length
  return { argv, pretty }
}

function argvWidth () {
  const widthIndex = process.argv.findIndex((s) => s === '--width')
  if (widthIndex <= 0 || widthIndex + 1 >= process.argv.length) {
    return { argv: process.argv.filter((s) => s !== '--width'), width: undefined }
  }
  const width = parseInt(process.argv[widthIndex + 1], 10)
  if (!width || width < 20) {
    return { argv: process.argv.filter((s) => s !== '--width'), width: undefined }
  }
  const argv = process.argv
  argv.splice(widthIndex, 2)
  return { argv, width }
}

async function run () {
  const cmd = process.argv[2]

  switch (cmd) {
    case 'help': {
      return usage(0)
    }

    case 'bin2diag': {
      const { argv, width } = argvWidth()
      const bin = argv.length < 4 ? (await fromStdin()) : new TextEncoder().encode(argv[3])
      for (const line of tokensToDiagnostic(bin, width)) {
        console.log(line)
      }
      return
    }

    case 'bin2hex': {
      // this is really nothing to do with cbor.. just handy
      const bin = process.argv.length < 4 ? (await fromStdin()) : new TextEncoder().encode(process.argv[3])
      return console.log(toHex(bin))
    }

    case 'bin2json': {
      const { argv, pretty } = argvPretty()
      const bin = argv.length < 4 ? (await fromStdin()) : new TextEncoder().encode(argv[3])
      return console.log(JSON.stringify(decode(bin), undefined, pretty ? 2 : undefined))
    }

    case 'diag2bin': {
      const bin = fromDiag(process.argv.length < 4 ? (await fromStdin()).toString() : process.argv[3])
      return process.stdout.write(bin)
    }

    case 'diag2hex': {
      const bin = fromDiag(process.argv.length < 4 ? (await fromStdin()).toString() : process.argv[3])
      return console.log(toHex(bin))
    }

    case 'diag2json': {
      const { argv, pretty } = argvPretty()
      const bin = fromDiag(argv.length < 4 ? (await fromStdin()).toString() : argv[3])
      return console.log(JSON.stringify(decode(bin), undefined, pretty ? 2 : undefined))
    }

    case 'hex2bin': {
      // this is really nothing to do with cbor.. just handy
      const bin = fromHex(process.argv.length < 4 ? (await fromStdin()).toString() : process.argv[3])
      return process.stdout.write(bin)
    }

    case 'hex2diag': {
      const { argv, width } = argvWidth()
      const bin = fromHex(argv.length < 4 ? (await fromStdin()).toString() : argv[3])
      for (const line of tokensToDiagnostic(bin, width)) {
        console.log(line)
      }
      return
    }

    case 'hex2json': {
      const { argv, pretty } = argvPretty()
      const bin = fromHex(argv.length < 4 ? (await fromStdin()).toString() : argv[3])
      return console.log(JSON.stringify(decode(bin), undefined, pretty ? 2 : undefined))
    }

    case 'json2bin': {
      const inp = process.argv.length < 4 ? (await fromStdin()).toString() : process.argv[3]
      const obj = JSON.parse(inp)
      return process.stdout.write(encode(obj))
    }

    case 'json2diag': {
      const { argv, width } = argvWidth()
      const inp = argv.length < 4 ? (await fromStdin()).toString() : argv[3]
      const obj = JSON.parse(inp)
      for (const line of tokensToDiagnostic(encode(obj), width)) {
        console.log(line)
      }
      return
    }

    case 'json2hex': {
      const inp = process.argv.length < 4 ? (await fromStdin()).toString() : process.argv[3]
      const obj = JSON.parse(inp)
      return console.log(toHex(encode(obj)))
    }

    default: { // no, or unknown cmd
      // this is a dirty hack to allow import of this package by the tests
      // for inclusion in ipjs bundling, but to silently ignore it so we don't
      // print usage and exit(1).
      if (process.argv.findIndex((a) => a.endsWith('mocha')) === -1) {
        if (cmd) {
          console.error(`Unknown command: '${cmd}'`)
        }
        usage(1)
      }
    }
  }
}

run().catch((err) => {
  console.error(err)
  process.exit(1)
})

// for ipjs, to get it to compile
export default true
