/*!
 * compress.js - coin compressor for bcoin
 * Copyright (c) 2014-2016, Christopher Jeffrey (MIT License).
 * https://github.com/bcoin-org/bcoin
 */

'use strict';

var assert = require('assert');
var ec = require('../lib/crypto/ec');

/*
 * Compression
 */

/**
 * Compress a script, write directly to the buffer.
 * @param {Script} script
 * @param {BufferWriter} bw
 */

function compressScript(script, bw) {
  var data;

  // Attempt to compress the output scripts.
  // We can _only_ ever compress them if
  // they are serialized as minimaldata, as
  // we need to recreate them when we read
  // them.

  // P2PKH -> 1 | key-hash
  // Saves 5 bytes.
  if (script.isPubkeyhash(true)) {
    data = script.code[2].data;
    bw.writeU8(1);
    bw.writeBytes(data);
    return bw;
  }

  // P2SH -> 2 | script-hash
  // Saves 3 bytes.
  if (script.isScripthash()) {
    data = script.code[1].data;
    bw.writeU8(2);
    bw.writeBytes(data);
    return bw;
  }

  // P2PK -> 3 | compressed-key
  // Only works if the key is valid.
  // Saves up to 34 bytes.
  if (script.isPubkey(true)) {
    data = script.code[0].data;
    if (ec.publicKeyVerify(data)) {
      data = compressKey(data);
      bw.writeU8(3);
      bw.writeBytes(data);
      return bw;
    }
  }

  // Raw -> 0 | varlen | script
  bw.writeU8(0);
  bw.writeVarBytes(script.toRaw());

  return bw;
}

/**
 * Decompress a script from buffer reader.
 * @param {Script} script
 * @param {BufferReader} br
 */

function decompressScript(script, br) {
  var data;

  // Decompress the script.
  switch (br.readU8()) {
    case 0:
      data = br.readVarBytes();
      script.fromRaw(data);
      break;
    case 1:
      data = br.readBytes(20, true);
      script.fromPubkeyhash(data);
      break;
    case 2:
      data = br.readBytes(20, true);
      script.fromScripthash(data);
      break;
    case 3:
      data = br.readBytes(33, true);
      // Decompress the key. If this fails,
      // we have database corruption!
      data = decompressKey(data);
      script.fromPubkey(data);
      break;
    default:
      throw new Error('Bad prefix.');
  }

  return script;
}

/**
 * Compress value using an exponent. Takes advantage of
 * the fact that many bitcoin values are divisible by 10.
 * @see https://github.com/btcsuite/btcd/blob/master/blockchain/compress.go
 * @param {Amount} value
 * @returns {Number}
 */

function compressValue(value) {
  var exp, last;

  if (value === 0)
    return 0;

  exp = 0;
  while (value % 10 === 0 && exp < 9) {
    value /= 10;
    exp++;
  }

  if (exp < 9) {
    last = value % 10;
    value = (value - last) / 10;
    return 1 + 10 * (9 * value + last - 1) + exp;
  }

  return 10 + 10 * (value - 1);
}

/**
 * Decompress value.
 * @param {Number} value - Compressed value.
 * @returns {Amount} value
 */

function decompressValue(value) {
  var exp, n, last;

  if (value === 0)
    return 0;

  value--;

  exp = value % 10;
  value = (value - exp) / 10;

  if (exp < 9) {
    last = value % 9;
    value = (value - last) / 9;
    n = value * 10 + last + 1;
  } else {
    n = value + 1;
  }

  while (exp > 0) {
    n *= 10;
    exp--;
  }

  return n;
}

/**
 * Compress a public key to coins compression format.
 * @param {Buffer} key
 * @returns {Buffer}
 */

function compressKey(key) {
  var out;

  switch (key[0]) {
    case 0x02:
    case 0x03:
      // Key is already compressed.
      out = key;
      break;
    case 0x04:
    case 0x06:
    case 0x07:
      // Compress the key normally.
      out = ec.publicKeyConvert(key, true);
      // Store the original format (which
      // may be a hybrid byte) in the hi
      // 3 bits so we can restore it later.
      // The hi bits being set also lets us
      // know that this key was originally
      // decompressed.
      out[0] |= key[0] << 2;
      break;
    default:
      throw new Error('Bad point format.');
  }

  assert(out.length === 33);

  return out;
}

/**
 * Decompress a public key from the coins compression format.
 * @param {Buffer} key
 * @returns {Buffer}
 */

function decompressKey(key) {
  var format = key[0] >>> 2;
  var out;

  assert(key.length === 33);

  // Hi bits are not set. This key
  // is not meant to be decompressed.
  if (format === 0)
    return key;

  // Decompress the key, and off the
  // low bits so publicKeyConvert
  // actually understands it.
  key[0] &= 0x03;
  out = ec.publicKeyConvert(key, false);

  // Reset the hi bits so as not to
  // mutate the original buffer.
  key[0] |= format << 2;

  // Set the original format, which
  // may have been a hybrid prefix byte.
  out[0] = format;

  return out;
}

/*
 * Expose
 */

exports.compress = {
  script: compressScript,
  value: compressValue,
  key: compressKey
};

exports.decompress = {
  script: decompressScript,
  value: decompressValue,
  key: decompressKey
};
