/*!
 * rbt.js - iterative red black tree for bcoin
 * Copyright (c) 2016, Christopher Jeffrey (MIT License).
 * https://github.com/bcoin-org/bcoin
 */

'use strict';

var utils = require('../utils/utils');
var assert = require('assert');
var DUMMY = new Buffer([0]);
var RED = 0;
var BLACK = 1;
var SENTINEL;

/**
 * An iterative red black tree.
 * Many of its options, parameters,
 * and methods mimic the leveldown
 * interface.
 * @exports RBT
 * @constructor
 * @param {String?} location - Phony location.
 * @param {Object?} options
 * @param {Function} options.compare - Comparator.
 */

function RBT(location, options) {
  if (!(this instanceof RBT))
    return new RBT(location, options);

  if (typeof location !== 'string') {
    options = location;
    location = null;
  }

  if (!options)
    options = {};

  if (typeof options === 'function')
    options = { compare: options };

  this.options = options;
  this.root = SENTINEL;
  this.compare = options.compare || utils.cmp;
}

/**
 * Do a key lookup.
 * @param {Buffer|String} key
 * @returns {Buffer?} value
 */

RBT.prototype.search = function search(key) {
  var current = this.root;
  var cmp;

  if (typeof key === 'string')
    key = new Buffer(key, 'utf8');

  while (!current.isNull()) {
    cmp = this.compare(key, current.key);

    if (cmp === 0)
      return current.value;

    if (cmp < 0)
      current = current.left;
    else
      current = current.right;
  }
};

/**
 * Insert a record.
 * @param {Buffer|String} key
 * @param {Buffer} value
 */

RBT.prototype.insert = function insert(key, value) {
  var current = this.root;
  var left = false;
  var parent, cmp, node;

  if (typeof key === 'string')
    key = new Buffer(key, 'utf8');

  if (typeof value === 'string')
    value = new Buffer(value, 'utf8');

  while (!current.isNull()) {
    cmp = this.compare(key, current.key);

    if (cmp === 0) {
      current.value = value;
      return;
    }

    parent = current;

    if (cmp < 0) {
      left = true;
      current = current.left;
    } else {
      left = false;
      current = current.right;
    }
  }

  node = new RBTNode(key, value);

  if (!parent) {
    this.root = node;
    this.insertFixup(node);
    return;
  }

  node.parent = parent;

  if (left)
    parent.left = node;
  else
    parent.right = node;

  this.insertFixup(node);
};

/**
 * Repaint necessary nodes after insertion.
 * @private
 * @param {RBTNode} x
 */

RBT.prototype.insertFixup = function insertFixup(x) {
  var y;

  x.color = RED;

  while (x !== this.root && x.parent.color === RED) {
    if (x.parent === x.parent.parent.left) {
      y = x.parent.parent.right;
      if (!y.isNull() && y.color === RED) {
        x.parent.color = BLACK;
        y.color = BLACK;
        x.parent.parent.color = RED;
        x = x.parent.parent;
      } else {
        if (x === x.parent.right) {
          x = x.parent;
          this.rotl(x);
        }
        x.parent.color = BLACK;
        x.parent.parent.color = RED;
        this.rotr(x.parent.parent);
      }
    } else {
      y = x.parent.parent.left;
      if (!y.isNull() && y.color === RED) {
        x.parent.color = BLACK;
        y.color = BLACK;
        x.parent.parent.color = RED;
        x = x.parent.parent;
      } else {
        if (x === x.parent.left) {
          x = x.parent;
          this.rotr(x);
        }
        x.parent.color = BLACK;
        x.parent.parent.color = RED;
        this.rotl(x.parent.parent);
      }
    }
  }

  this.root.color = BLACK;
};

/**
 * Remove a record.
 * @param {Buffer|String} key
 * @returns {Boolean}
 */

RBT.prototype.remove = function remove(key) {
  var current = this.root;
  var cmp;

  if (typeof key === 'string')
    key = new Buffer(key, 'utf8');

  while (!current.isNull()) {
    cmp = this.compare(key, current.key);

    if (cmp === 0) {
      this.removeNode(current);
      return true;
    }

    if (cmp < 0)
      current = current.left;
    else
      current = current.right;
  }

  return false;
};

/**
 * Remove a single node.
 * @private
 * @param {RBTNode} z
 */

RBT.prototype.removeNode = function removeNode(z) {
  var y = z;
  var x;

  if (!z.left.isNull() && !z.right.isNull())
    y = this.successor(z);

  x = y.left.isNull() ? y.right : y.left;
  x.parent = y.parent;

  if (y.parent.isNull()) {
    this.root = x;
  } else {
    if (y === y.parent.left)
      y.parent.left = x;
    else
      y.parent.right = x;
  }

  if (y !== z) {
    z.key = y.key;
    z.value = y.value;
  }

  if (y.color === BLACK)
    this.removeFixup(x);
};

/**
 * Repaint necessary nodes after removal.
 * @private
 * @param {RBTNode} x
 */

RBT.prototype.removeFixup = function removeFixup(x) {
  var w;

  while (x !== this.root && x.color === BLACK) {
    if (x === x.parent.left) {
      w = x.parent.right;

      if (w.color === RED) {
        w.color = BLACK;
        x.parent.color = RED;
        this.rotl(x.parent);
        w = x.parent.right;
      }

      if (w.left.color === BLACK && w.right.color === BLACK) {
        w.color = RED;
        x = x.parent;
      } else {
        if (w.right.color === BLACK) {
          w.left.color = BLACK;
          w.color = RED;
          this.rotr(w);
          w = x.parent.right;
        }
        w.color = x.parent.color;
        x.parent.color = BLACK;
        w.right.color = BLACK;
        this.rotl(x.parent);
        x = this.root;
      }
    } else {
      w = x.parent.left;

      if (w.color === RED) {
        w.color = BLACK;
        x.parent.color = RED;
        this.rotr(x.parent);
        w = x.parent.left;
      }

      if (w.right.color === BLACK && w.left.color === BLACK) {
        w.color = RED;
        x = x.parent;
      } else {
        if (w.left.color === BLACK) {
          w.right.color = BLACK;
          w.color = RED;
          this.rotl(w);
          w = x.parent.left;
        }
        w.color = x.parent.color;
        x.parent.color = BLACK;
        w.left.color = BLACK;
        this.rotr(x.parent);
        x = this.root;
      }
    }
  }

  x.color = BLACK;
};

/**
 * Do a left rotate.
 * @private
 * @param {RBTNode} x
 */

RBT.prototype.rotl = function rotl(x) {
  var y = x.right;
  x.right = y.left;

  if (!y.left.isNull())
    y.left.parent = x;

  y.parent = x.parent;

  if (x.parent.isNull()) {
    this.root = y;
  } else {
    if (x === x.parent.left)
      x.parent.left = y;
    else
      x.parent.right = y;
  }

  y.left = x;
  x.parent = y;
};

/**
 * Do a right rotate.
 * @private
 * @param {RBTNode} x
 */

RBT.prototype.rotr = function rotr(x) {
  var y = x.left;
  x.left = y.right;

  if (!y.right.isNull())
    y.right.parent = x;

  y.parent = x.parent;

  if (x.parent.isNull()) {
    this.root = y;
  } else {
    if (x === x.parent.right)
      x.parent.right = y;
    else
      x.parent.left = y;
  }

  y.right = x;
  x.parent = y;
};

/**
 * Minimum subtree.
 * @private
 * @param {RBTNode} z
 * @returns {RBTNode}
 */

RBT.prototype.min = function min(z) {
  if (z.isNull())
    return z;
  while (!z.left.isNull())
    z = z.left;
  return z;
};

/**
 * Maximum subtree.
 * @private
 * @param {RBTNode} z
 * @returns {RBTNode}
 */

RBT.prototype.max = function max(z) {
  if (z.isNull())
    return z;
  while (!z.right.isNull())
    z = z.right;
  return z;
};

/**
 * Successor node.
 * @private
 * @param {RBTNode} x
 * @returns {RBTNode}
 */

RBT.prototype.successor = function successor(x) {
  var y;
  if (!x.right.isNull()) {
    x = x.right;
    while (!x.left.isNull())
      x = x.left;
    return x;
  }
  y = x.parent;
  while (!y.isNull() && x === y.right) {
    x = y;
    y = y.parent;
  }
  return y;
};

/**
 * Predecessor node.
 * @private
 * @param {RBTNode} x
 * @returns {RBTNode}
 */

RBT.prototype.predecessor = function predecessor(x) {
  var y;
  if (!x.left.isNull()) {
    x = x.left;
    while (!x.right.isNull())
      x = x.right;
    return x;
  }
  y = x.parent;
  while (!y.isNull() && x === y.left) {
    x = y;
    y = y.parent;
  }
  return y;
};

/**
 * Take a snapshot and return a cloned root node.
 * @returns {RBTNode}
 */

RBT.prototype.snapshot = function snapshot() {
  var current = this.root;
  var stack = [];
  var left = true;
  var parent, copy, snapshot;

  for (;;) {
    if (!current.isNull()) {
      if (left) {
        copy = current.clone();
        if (parent)
          parent.left = copy;
        else
          snapshot = copy;
      } else {
        copy = current.clone();
        if (parent)
          parent.right = copy;
        else
          snapshot = copy;
      }
      stack.push(copy);
      parent = copy;
      left = true;
      current = current.left;
      continue;
    }

    if (stack.length === 0)
      break;

    current = stack.pop();
    parent = current;
    left = false;
    current = current.right;
  }

  return snapshot;
};

/**
 * Traverse the key and filter records.
 * @param {Function} test
 * @returns {RBTNode[]} Records.
 */

RBT.prototype.traverse = function traverse(test) {
  var current = this.min(this.root);
  var items = [];

  while (!current.isNull()) {
    if (test(current))
      items.push(current.copy());
    current = this.successor(current);
  }

  return items;
};

/**
 * Dump all records.
 * @returns {RBTNode[]} Records.
 */

RBT.prototype.dump = function dump() {
  return this.traverse(function() { return true; });
};

/**
 * Traverse between a range of keys and collect records.
 * @param {Buffer} gte
 * @param {Buffer} lte
 * @returns {RBTNode[]} Records.
 */

RBT.prototype.range = function range(gte, lte) {
  var root = this.root;
  var current = SENTINEL;
  var items = [];
  var cmp;

  if (typeof gte === 'string')
    gte = new Buffer(gte, 'utf8');

  if (typeof lte === 'string')
    lte = new Buffer(lte, 'utf8');

  if (gte) {
    // Find the node closest to our gte key.
    while (!root.isNull()) {
      cmp = this.compare(gte, root.key);

      if (cmp === 0) {
        current = root;
        break;
      }

      if (cmp < 0) {
        current = root;
        root = root.left;
      } else {
        root = root.right;
      }
    }
  } else {
    // Descend into the left subtree.
    current = this.min(root);
  }

  // Walk the tree in order.
  while (!current.isNull()) {
    if (lte) {
      // Stop once we hit a key above our lte key.
      cmp = this.compare(lte, current.key);
      if (cmp < 0)
        break;
    }

    items.push(current.copy());
    current = this.successor(current);
  }

  return items;
};

/**
 * Open the database (leveldown method).
 * @param {Object?} options
 * @returns {Promise}
 */

RBT.prototype.open = function open(options, callback) {
  if (!callback) {
    callback = options;
    options = null;
  }

  if (!options)
    options = {};

  this.options = options;

  utils.nextTick(callback);
};

/**
 * Close the database (leveldown method).
 * @returns {Promise}
 */

RBT.prototype.close = function close(callback) {
  utils.nextTick(callback);
};

/**
 * Retrieve a record (leveldown method).
 * @param {Buffer|String} key
 * @param {Object?} options
 * @returns {Promise} - Returns Buffer.
 */

RBT.prototype.get = function get(key, options, callback) {
  var value, err;

  if (!callback) {
    callback = options;
    options = null;
  }

  if (!options)
    options = {};

  value = this.search(key);

  if (!value) {
    err = new Error('RBT_NOTFOUND: Key not found.');
    err.notFound = true;
    err.type = 'NotFoundError';
    utils.nextTick(function() {
      callback(err);
    });
    return;
  }

  if (options.asBuffer === false)
    value = value.toString('utf8');

  utils.nextTick(function() {
    callback(null, value);
  });
};

/**
 * Insert a record (leveldown method).
 * @param {Buffer|String} key
 * @param {Buffer} value
 * @param {Object?} options
 * @returns {Promise}
 */

RBT.prototype.put = function put(key, value, options, callback) {
  if (!callback) {
    callback = options;
    options = null;
  }

  this.insert(key, value);

  utils.nextTick(callback);
};

/**
 * Remove a record (leveldown method).
 * @param {Buffer|String} key
 * @param {Object?} options
 * @returns {Promise}
 */

RBT.prototype.del = function del(key, options, callback) {
  if (!callback) {
    callback = options;
    options = null;
  }

  this.remove(key);

  utils.nextTick(callback);
};

/**
 * Create an atomic batch (leveldown method).
 * @see Leveldown.Batch
 * @param {Object[]?} ops
 * @param {Object?} options
 * @returns {Promise}
 * @returns {Leveldown.Batch}
 */

RBT.prototype.batch = function batch(ops, options, callback) {
  var batch;

  if (!callback) {
    callback = options;
    options = null;
  }

  if (!options)
    options = {};

  batch = new Batch(this, options);

  if (ops) {
    batch.ops = ops.slice();
    return batch.write(callback);
  }

  return batch;
};

/**
 * Create an iterator (leveldown method).
 * @param {Object} options - See {Leveldown.Iterator}.
 * @returns {Leveldown.Iterator}.
 */

RBT.prototype.iterator = function iterator(options) {
  return new Iterator(this, options);
};

/**
 * Get a database property (leveldown method) (NOP).
 * @param {String} name - Property name.
 * @returns {String}
 */

RBT.prototype.getProperty = function getProperty(name) {
  return '';
};

/**
 * Calculate approximate database size (leveldown method).
 * @param {Buffer|String} start - Start key.
 * @param {Buffer|String} end - End key.
 * @returns {Promise} - Returns Number.
 */

RBT.prototype.approximateSize = function approximateSize(start, end, callback) {
  var items = this.range(start, end);
  var size = 0;
  var i, item;

  for (i = 0; i < items.length; i++) {
    item = items[i];
    size += item.key.length;
    size += item.value.length;
  }

  utils.nextTick(function() {
    callback(null, size);
  });
};

/**
 * Destroy the database (leveldown function) (NOP).
 * @param {String} location
 * @returns {Promise}
 */

RBT.destroy = function destroy(location, callback) {
  utils.nextTick(callback);
};

/**
 * Repair the database (leveldown function) (NOP).
 * @param {String} location
 * @returns {Promise}
 */

RBT.repair = function repair(location, callback) {
  utils.nextTick(callback);
};

/**
 * RBT Node
 * @constructor
 * @private
 * @param {Buffer} key
 * @param {Buffer} value
 * @property {Buffer} key
 * @property {Buffer} value
 * @property {Number} color
 * @property {RBTNode|RBTSentinel} parent
 * @property {RBTNode|RBTSentinel} left
 * @property {RBTNode|RBTSentinel} right
 */

function RBTNode(key, value) {
  this.key = key;
  this.value = value;
  this.color = RED;
  this.parent = SENTINEL;
  this.left = SENTINEL;
  this.right = SENTINEL;
}

/**
 * Clone the node.
 * @returns {RBTNode}
 */

RBTNode.prototype.clone = function clone() {
  var node = new RBTNode(this.key, this.value);
  node.color = this.color;
  node.parent = this.parent;
  node.left = this.left;
  node.right = this.right;
  return node;
};

/**
 * Clone the node (key/value only).
 * @returns {RBTData}
 */

RBTNode.prototype.copy = function copy() {
  return new RBTData(this.key, this.value);
};

/**
 * Inspect the rbt node.
 * @returns {Object}
 */

RBTNode.prototype.inspect = function inspect() {
  return {
    key: stringify(this.key),
    value: this.value.toString('hex'),
    color: this.color === RED ? 'red' : 'black',
    left: this.left,
    right: this.right
  };
};

/**
 * Test whether the node is a leaf.
 * Always returns false.
 * @returns {Boolean}
 */

RBTNode.prototype.isNull = function isNull() {
  return false;
};

/**
 * RBT Sentinel Node
 * @constructor
 * @property {null} key
 * @property {null} value
 * @property {Number} [color=BLACK]
 * @property {null} parent
 * @property {null} left
 * @property {null} right
 */

function RBTSentinel() {
  this.key = null;
  this.value = null;
  this.color = BLACK;
  this.parent = null;
  this.left = null;
  this.right = null;
}

/**
 * Inspect the rbt node.
 * @returns {String}
 */

RBTSentinel.prototype.inspect = function inspect() {
  return 'NIL';
};

/**
 * Test whether the node is a leaf.
 * Always returns true.
 * @returns {Boolean}
 */

RBTSentinel.prototype.isNull = function isNull() {
  return true;
};

/**
 * RBT key/value pair
 * @constructor
 * @param {Buffer} key
 * @param {Buffer} value
 * @property {Buffer} key
 * @property {Buffer} value
 */

function RBTData(key, value) {
  this.key = key;
  this.value = value;
}

/**
 * Inspect the rbt data.
 * @returns {Object}
 */

RBTData.prototype.inspect = function inspect() {
  return {
    key: stringify(this.key),
    value: this.value.toString('hex')
  };
};

/**
 * Batch
 * @constructor
 * @private
 * @param {RBT} tree
 * @param {Object?} options
 */

function Batch(tree, options) {
  this.options = options || {};
  this.ops = [];
  this.tree = tree;
}

/**
 * Insert a record.
 * @param {Buffer|String} key
 * @param {Buffer} value
 */

Batch.prototype.put = function(key, value) {
  assert(this.tree, 'Already written.');
  this.ops.push(new BatchOp('put', key, value));
  return this;
};

/**
 * Remove a record.
 * @param {Buffer|String} key
 */

Batch.prototype.del = function del(key) {
  assert(this.tree, 'Already written.');
  this.ops.push(new BatchOp('del', key));
  return this;
};

/**
 * Commit the batch.
 * @returns {Promise}
 */

Batch.prototype.write = function write(callback) {
  var i, op;

  if (!this.tree) {
    utils.nextTick(function() {
      callback(new Error('Already written.'));
    });
    return;
  }

  for (i = 0; i < this.ops.length; i++) {
    op = this.ops[i];
    switch (op.type) {
      case 'put':
        this.tree.insert(op.key, op.value);
        break;
      case 'del':
        this.tree.remove(op.key);
        break;
      default:
        utils.nextTick(function() {
          callback(new Error('Bad operation: ' + op.type));
        });
        return;
    }
  }

  this.ops.length = 0;
  this.ops = null;
  this.options = null;
  this.tree = null;

  utils.nextTick(callback);

  return this;
};

/**
 * Clear batch of all ops.
 */

Batch.prototype.clear = function clear() {
  assert(this.tree, 'Already written.');
  this.ops.length = 0;
  return this;
};

/**
 * Batch Operation
 * @constructor
 * @private
 * @param {String} type
 * @param {Buffer} key
 * @param {Buffer|null} value
 */

function BatchOp(type, key, value) {
  this.type = type;
  this.key = key;
  this.value = value;
}

/**
 * Iterator
 * @constructor
 * @private
 * @param {RBT} tree
 * @param {Object?} options
 */

function Iterator(tree, options) {
  if (!options)
    options = {};

  assert(!options.lt, 'LT is not implemented.');
  assert(!options.gt, 'GT is not implemented.');

  this.options = {
    keys: options.keys,
    values: options.values,
    gte: options.gte || options.start,
    lte: options.lte || options.end,
    keyAsBuffer: options.keyAsBuffer,
    valueAsBuffer: options.valueAsBuffer,
    reverse: options.reverse,
    limit: options.limit
  };

  this.tree = tree;
  this.ended = false;
  this.snapshot = this.tree.range(this.options.gte, this.options.lte);
  this.index = this.options.reverse ? this.snapshot.length - 1 : 0;
  this.total = 0;
}

/**
 * Seek to the next key.
 * @returns {Promise}
 */

Iterator.prototype.next = function(callback) {
  var item, key, value;

  if (this.ended) {
    utils.nextTick(function() {
      callback(new Error('Cannot call next after end.'));
    });
    return;
  }

  if (this.options.reverse)
    item = this.snapshot[this.index--];
  else
    item = this.snapshot[this.index++];

  if (this.options.limit != null) {
    if (this.total++ >= this.options.limit) {
      this._end();
      utils.nextTick(callback);
      return;
    }
  }

  if (!item) {
    this._end();
    utils.nextTick(callback);
    return;
  }

  key = item.key;
  value = item.value;

  if (this.options.keys === false)
    key = DUMMY;

  if (this.options.values === false)
    value = DUMMY;

  if (this.options.keyAsBuffer === false)
    key = stringify(key);

  if (this.options.valueAsBuffer === false)
    value = value.toString('utf8');

  utils.nextTick(function() {
    callback(null, key, value);
  });
};

/**
 * Seek to a key gte to `key`.
 * @param {String|Buffer} key
 */

Iterator.prototype.seek = function seek(key) {
  var self = this;

  assert(!this.ended, 'Already ended.');

  if (typeof key === 'string')
    key = new Buffer(key, 'utf8');

  this.index = utils.binarySearch(this.snapshot, key, function(a, b) {
    return self.tree.compare(a.key, b);
  }, true);
};

/**
 * Clean up the iterator.
 * @private
 */

Iterator.prototype._end = function end() {
  if (!this.tree)
    return;

  this.tree = null;
  this.snapshot.length = 0;
  this.snapshot = null;
};

/**
 * End the iterator. Free up snapshot.
 * @param {Buffer} callback
 */

Iterator.prototype.end = function end(callback) {
  if (this.ended) {
    utils.nextTick(function() {
      callback(new Error('Already ended.'));
    });
    return;
  }

  this.ended = true;
  this._end();

  utils.nextTick(callback);
};

/*
 * Helpers
 */

SENTINEL = new RBTSentinel();

function stringify(value) {
  if (Buffer.isBuffer(value))
    return value.toString('utf8');
  return value;
}

/*
 * Expose
 */

module.exports = RBT;
