// javascript-astar 0.4.1
// http://github.com/bgrins/javascript-astar
// Freely distributable under the MIT License.
// Implements the astar search algorithm in javascript using a Binary Heap.
// Includes Binary Heap (with modifications) from Marijn Haverbeke.
// http://eloquentjavascript.net/appendix2.html
(function(definition) {
  /* global module, define */
  if (typeof module === 'object' && typeof module.exports === 'object') {
    module.exports = definition();
  } else if (typeof define === 'function' && define.amd) {
    define([], definition);
  } else {
    var exports = definition();
    window.astar = exports.astar;
    window.Graph = exports.Graph;
  }
})(function() {

function pathTo(node) {
  var curr = node;
  var path = [];
  while (curr.parent) {
    path.unshift(curr);
    curr = curr.parent;
  }
  return path;
}

function getHeap() {
  return new BinaryHeap(function(node) {
    return node.f;
  });
}

var astar = {
  /**
  * Perform an A* Search on a graph given a start and end node.
  * @param {Graph} graph
  * @param {GridNode} start
  * @param {GridNode} end
  * @param {Object} [options]
  * @param {bool} [options.closest] Specifies whether to return the
             path to the closest node if the target is unreachable.
  * @param {Function} [options.heuristic] Heuristic function (see
  *          astar.heuristics).
  */
  search: function(graph, start, end, options) {
    graph.cleanDirty();
    options = options || {};
    var heuristic = options.heuristic || astar.heuristics.manhattan;
    var closest = options.closest || false;

    var openHeap = getHeap();
    var closestNode = start; // set the start node to be the closest if required

    start.h = heuristic(start, end);
    graph.markDirty(start);

    openHeap.push(start);

    while (openHeap.size() > 0) {

      // Grab the lowest f(x) to process next.  Heap keeps this sorted for us.
      var currentNode = openHeap.pop();

      // End case -- result has been found, return the traced path.
      if (currentNode === end) {
        return pathTo(currentNode);
      }

      // Normal case -- move currentNode from open to closed, process each of its neighbors.
      currentNode.closed = true;

      // Find all neighbors for the current node.
      var neighbors = graph.neighbors(currentNode);

      for (var i = 0, il = neighbors.length; i < il; ++i) {
        var neighbor = neighbors[i];

        if (neighbor.closed || neighbor.isWall()) {
          // Not a valid node to process, skip to next neighbor.
          continue;
        }

        // The g score is the shortest distance from start to current node.
        // We need to check if the path we have arrived at this neighbor is the shortest one we have seen yet.
        var gScore = currentNode.g + neighbor.getCost(currentNode);
        var beenVisited = neighbor.visited;

        if (!beenVisited || gScore < neighbor.g) {

          // Found an optimal (so far) path to this node.  Take score for node to see how good it is.
          neighbor.visited = true;
          neighbor.parent = currentNode;
          neighbor.h = neighbor.h || heuristic(neighbor, end);
          neighbor.g = gScore;
          neighbor.f = neighbor.g + neighbor.h;
          graph.markDirty(neighbor);
          if (closest) {
            // If the neighbour is closer than the current closestNode or if it's equally close but has
            // a cheaper path than the current closest node then it becomes the closest node
            if (neighbor.h < closestNode.h || (neighbor.h === closestNode.h && neighbor.g < closestNode.g)) {
              closestNode = neighbor;
            }
          }

          if (!beenVisited) {
            // Pushing to heap will put it in proper place based on the 'f' value.
            openHeap.push(neighbor);
          } else {
            // Already seen the node, but since it has been rescored we need to reorder it in the heap
            openHeap.rescoreElement(neighbor);
          }
        }
      }
    }

    if (closest) {
      return pathTo(closestNode);
    }

    // No result was found - empty array signifies failure to find path.
    return [];
  },
  // See list of heuristics: http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html
  heuristics: {
    manhattan: function(pos0, pos1) {
      var d1 = Math.abs(pos1.x - pos0.x);
      var d2 = Math.abs(pos1.y - pos0.y);
      return d1 + d2;
    },
    diagonal: function(pos0, pos1) {
      var D = 1;
      var D2 = Math.sqrt(2);
      var d1 = Math.abs(pos1.x - pos0.x);
      var d2 = Math.abs(pos1.y - pos0.y);
      return (D * (d1 + d2)) + ((D2 - (2 * D)) * Math.min(d1, d2));
    }
  },
  cleanNode: function(node) {
    node.f = 0;
    node.g = 0;
    node.h = 0;
    node.visited = false;
    node.closed = false;
    node.parent = null;
  }
};

/**
 * A graph memory structure
 * @param {Array} gridIn 2D array of input weights
 * @param {Object} [options]
 * @param {bool} [options.diagonal] Specifies whether diagonal moves are allowed
 */
function Graph(gridIn, options) {
  options = options || {};
  this.nodes = [];
  this.diagonal = !!options.diagonal;
  this.grid = [];
  for (var x = 0; x < gridIn.length; x++) {
    this.grid[x] = [];

    for (var y = 0, row = gridIn[x]; y < row.length; y++) {
      var node = new GridNode(x, y, row[y]);
      this.grid[x][y] = node;
      this.nodes.push(node);
    }
  }
  this.init();
}

Graph.prototype.init = function() {
  this.dirtyNodes = [];
  for (var i = 0; i < this.nodes.length; i++) {
    astar.cleanNode(this.nodes[i]);
  }
};

Graph.prototype.cleanDirty = function() {
  for (var i = 0; i < this.dirtyNodes.length; i++) {
    astar.cleanNode(this.dirtyNodes[i]);
  }
  this.dirtyNodes = [];
};

Graph.prototype.markDirty = function(node) {
  this.dirtyNodes.push(node);
};

Graph.prototype.neighbors = function(node) {
  var ret = [];
  var x = node.x;
  var y = node.y;
  var grid = this.grid;

  // West
  if (grid[x - 1] && grid[x - 1][y]) {
    ret.push(grid[x - 1][y]);
  }

  // East
  if (grid[x + 1] && grid[x + 1][y]) {
    ret.push(grid[x + 1][y]);
  }

  // South
  if (grid[x] && grid[x][y - 1]) {
    ret.push(grid[x][y - 1]);
  }

  // North
  if (grid[x] && grid[x][y + 1]) {
    ret.push(grid[x][y + 1]);
  }

  if (this.diagonal) {
    // Southwest
    if (grid[x - 1] && grid[x - 1][y - 1]) {
      ret.push(grid[x - 1][y - 1]);
    }

    // Southeast
    if (grid[x + 1] && grid[x + 1][y - 1]) {
      ret.push(grid[x + 1][y - 1]);
    }

    // Northwest
    if (grid[x - 1] && grid[x - 1][y + 1]) {
      ret.push(grid[x - 1][y + 1]);
    }

    // Northeast
    if (grid[x + 1] && grid[x + 1][y + 1]) {
      ret.push(grid[x + 1][y + 1]);
    }
  }

  return ret;
};

Graph.prototype.toString = function() {
  var graphString = [];
  var nodes = this.grid;
  for (var x = 0; x < nodes.length; x++) {
    var rowDebug = [];
    var row = nodes[x];
    for (var y = 0; y < row.length; y++) {
      rowDebug.push(row[y].weight);
    }
    graphString.push(rowDebug.join(" "));
  }
  return graphString.join("\n");
};

function GridNode(x, y, weight) {
  this.x = x;
  this.y = y;
  this.weight = weight;
}

GridNode.prototype.toString = function() {
  return "[" + this.x + " " + this.y + "]";
};

GridNode.prototype.getCost = function(fromNeighbor) {
  // Take diagonal weight into consideration.
  if (fromNeighbor && fromNeighbor.x != this.x && fromNeighbor.y != this.y) {
    return this.weight * 1.41421;
  }
  return this.weight;
};

GridNode.prototype.isWall = function() {
  return this.weight === 0;
};

function BinaryHeap(scoreFunction) {
  this.content = [];
  this.scoreFunction = scoreFunction;
}

BinaryHeap.prototype = {
  push: function(element) {
    // Add the new element to the end of the array.
    this.content.push(element);

    // Allow it to sink down.
    this.sinkDown(this.content.length - 1);
  },
  pop: function() {
    // Store the first element so we can return it later.
    var result = this.content[0];
    // Get the element at the end of the array.
    var end = this.content.pop();
    // If there are any elements left, put the end element at the
    // start, and let it bubble up.
    if (this.content.length > 0) {
      this.content[0] = end;
      this.bubbleUp(0);
    }
    return result;
  },
  remove: function(node) {
    var i = this.content.indexOf(node);

    // When it is found, the process seen in 'pop' is repeated
    // to fill up the hole.
    var end = this.content.pop();

    if (i !== this.content.length - 1) {
      this.content[i] = end;

      if (this.scoreFunction(end) < this.scoreFunction(node)) {
        this.sinkDown(i);
      } else {
        this.bubbleUp(i);
      }
    }
  },
  size: function() {
    return this.content.length;
  },
  rescoreElement: function(node) {
    this.sinkDown(this.content.indexOf(node));
  },
  sinkDown: function(n) {
    // Fetch the element that has to be sunk.
    var element = this.content[n];

    // When at 0, an element can not sink any further.
    while (n > 0) {

      // Compute the parent element's index, and fetch it.
      var parentN = ((n + 1) >> 1) - 1;
      var parent = this.content[parentN];
      // Swap the elements if the parent is greater.
      if (this.scoreFunction(element) < this.scoreFunction(parent)) {
        this.content[parentN] = element;
        this.content[n] = parent;
        // Update 'n' to continue at the new position.
        n = parentN;
      }
      // Found a parent that is less, no need to sink any further.
      else {
        break;
      }
    }
  },
  bubbleUp: function(n) {
    // Look up the target element and its score.
    var length = this.content.length;
    var element = this.content[n];
    var elemScore = this.scoreFunction(element);

    while (true) {
      // Compute the indices of the child elements.
      var child2N = (n + 1) << 1;
      var child1N = child2N - 1;
      // This is used to store the new position of the element, if any.
      var swap = null;
      var child1Score;
      // If the first child exists (is inside the array)...
      if (child1N < length) {
        // Look it up and compute its score.
        var child1 = this.content[child1N];
        child1Score = this.scoreFunction(child1);

        // If the score is less than our element's, we need to swap.
        if (child1Score < elemScore) {
          swap = child1N;
        }
      }

      // Do the same checks for the other child.
      if (child2N < length) {
        var child2 = this.content[child2N];
        var child2Score = this.scoreFunction(child2);
        if (child2Score < (swap === null ? elemScore : child1Score)) {
          swap = child2N;
        }
      }

      // If the element needs to be moved, swap it, and continue.
      if (swap !== null) {
        this.content[n] = this.content[swap];
        this.content[swap] = element;
        n = swap;
      }
      // Otherwise, we are done.
      else {
        break;
      }
    }
  }
};

return {
  astar: astar,
  Graph: Graph
};

});


/*  demo.js http://github.com/bgrins/javascript-astar
    MIT License

    Set up the demo page for the A* Search
*/
/* global Graph, astar, $ */

var WALL = 0,
    performance = window.performance;

$(function() {

    var $grid = $("#search_grid"),
        $selectWallFrequency = $("#selectWallFrequency"),
        $selectGridSize = $("#selectGridSize"),
        $checkDebug = $("#checkDebug"),
        $searchDiagonal = $("#searchDiagonal"),
        $checkClosest = $("#checkClosest");

    var opts = {
        wallFrequency: $selectWallFrequency.val(),
        gridSize: $selectGridSize.val(),
        debug: $checkDebug.is("checked"),
        diagonal: $searchDiagonal.is("checked"),
        closest: $checkClosest.is("checked")
    };

    var grid = new GraphSearch($grid, opts, astar.search);

    $("#btnGenerate").click(function() {
        grid.initialize();
    });

    $selectWallFrequency.change(function() {
        grid.setOption({wallFrequency: $(this).val()});
        grid.initialize();
    });

    $selectGridSize.change(function() {
        grid.setOption({gridSize: $(this).val()});
        grid.initialize();
    });

    $checkDebug.change(function() {
        grid.setOption({debug: $(this).is(":checked")});
    });

    $searchDiagonal.change(function() {
        var val = $(this).is(":checked");
        grid.setOption({diagonal: val});
        grid.graph.diagonal = val;
    });

    $checkClosest.change(function() {
        grid.setOption({closest: $(this).is(":checked")});
    });

    $("#generateWeights").click( function () {
        if ($("#generateWeights").prop("checked")) {
            $('#weightsKey').slideDown();
        } else {
            $('#weightsKey').slideUp();
        }
    });

});

var css = { start: "start", finish: "finish", wall: "wall", active: "active" };

function GraphSearch($graph, options, implementation) {
    this.$graph = $graph;
    this.search = implementation;
    this.opts = $.extend({wallFrequency:0.1, debug:true, gridSize:10}, options);
    this.initialize();
}
GraphSearch.prototype.setOption = function(opt) {
    this.opts = $.extend(this.opts, opt);
    this.drawDebugInfo();
};
GraphSearch.prototype.initialize = function() {
    this.grid = [];
    var self = this,
        nodes = [],
        $graph = this.$graph;

    $graph.empty();

    var cellWidth = ($graph.width()/this.opts.gridSize)-2,  // -2 for border
        cellHeight = ($graph.height()/this.opts.gridSize)-2,
        $cellTemplate = $("").addClass("grid_item").width(cellWidth).height(cellHeight),
        startSet = false;

    for(var x = 0; x < this.opts.gridSize; x++) {
        var $row = $("
"),
            nodeRow = [],
            gridRow = [];

        for(var y = 0; y < this.opts.gridSize; y++) {
            var id = "cell_"+x+"_"+y,
                $cell = $cellTemplate.clone();
            $cell.attr("id", id).attr("x", x).attr("y", y);
            $row.append($cell);
            gridRow.push($cell);

            var isWall = Math.floor(Math.random()*(1/self.opts.wallFrequency));
            if(isWall === 0) {
                nodeRow.push(WALL);
                $cell.addClass(css.wall);
            }
            else  {
                var cell_weight = ($("#generateWeights").prop("checked") ? (Math.floor(Math.random() * 3)) * 2 + 1 : 1);
                nodeRow.push(cell_weight);
                $cell.addClass('weight' + cell_weight);
                if ($("#displayWeights").prop("checked")) {
                    $cell.html(cell_weight);
                }
                if (!startSet) {
                    $cell.addClass(css.start);
                    startSet = true;
                }
            }
        }
        $graph.append($row);

        this.grid.push(gridRow);
        nodes.push(nodeRow);
    }

    this.graph = new Graph(nodes);

    // bind cell event, set start/wall positions
    this.$cells = $graph.find(".grid_item");
    this.$cells.click(function() {
        self.cellClicked($(this));
    });
};
GraphSearch.prototype.cellClicked = function($end) {

    var end = this.nodeFromElement($end);

    if($end.hasClass(css.wall) || $end.hasClass(css.start)) {
        return;
    }

    this.$cells.removeClass(css.finish);
    $end.addClass("finish");
    var $start = this.$cells.filter("." + css.start),
        start = this.nodeFromElement($start);

    var sTime = performance ? performance.now() : new Date().getTime();

    var path = this.search(this.graph, start, end, {
        closest: this.opts.closest
    });
    var fTime = performance ? performance.now() : new Date().getTime(),
        duration = (fTime-sTime).toFixed(2);

    if(path.length === 0) {
        $("#message").text("couldn't find a path (" + duration + "ms)");
        this.animateNoPath();
    }
    else {
        $("#message").text("search took " + duration + "ms.");
        this.drawDebugInfo();
        this.animatePath(path);
    }
};
GraphSearch.prototype.drawDebugInfo = function() {
    this.$cells.html(" ");
    var that = this;
    if(this.opts.debug) {
        that.$cells.each(function() {
            var node = that.nodeFromElement($(this)),
                debug = false;
            if (node.visited) {
                debug = "F: " + node.f + "
G: " + node.g + "
H: " + node.h;
            }

            if (debug) {
                $(this).html(debug);
            }
        });
    }
};
GraphSearch.prototype.nodeFromElement = function($cell) {
    return this.graph.grid[parseInt($cell.attr("x"))][parseInt($cell.attr("y"))];
};
GraphSearch.prototype.animateNoPath = function() {
    var $graph = this.$graph;
    var jiggle = function(lim, i) {
        if(i>=lim) { $graph.css("top", 0).css("left", 0); return; }
        if(!i) i=0;
        i++;
        $graph.css("top", Math.random()*6).css("left", Math.random()*6);
        setTimeout(function() {
            jiggle(lim, i);
        }, 5);
    };
    jiggle(15);
};
GraphSearch.prototype.animatePath = function(path) {
    var grid = this.grid,
        timeout = 1000 / grid.length,
        elementFromNode = function(node) {
        return grid[node.x][node.y];
    };

    var self = this;
    // will add start class if final
    var removeClass = function(path, i) {
        if(i >= path.length) { // finished removing path, set start positions
            return setStartClass(path, i);
        }
        elementFromNode(path[i]).removeClass(css.active);
        setTimeout(function() {
            removeClass(path, i+1);
        }, timeout*path[i].getCost());
    };
    var setStartClass = function(path, i) {
        if(i === path.length) {
            self.$graph.find("." + css.start).removeClass(css.start);
            elementFromNode(path[i-1]).addClass(css.start);
        }
    };
    var addClass = function(path, i) {
        if(i >= path.length) { // Finished showing path, now remove
            return removeClass(path, 0);
        }
        elementFromNode(path[i]).addClass(css.active);
        setTimeout(function() {
            addClass(path, i+1);
        }, timeout*path[i].getCost());
    };

    addClass(path, 0);
    this.$graph.find("." + css.start).removeClass(css.start);
    this.$graph.find("." + css.finish).removeClass(css.finish).addClass(css.start);
};


'use strict';

const util = require('util');
const Q = require('q');
const hooks = require('hooks');
const Attribute = require('./Attribute');
const Table = require('./Table');
const Query = require('./Query');
const Scan = require('./Scan');
const Plugin = require('./Plugin');
const errors = require('./errors');
const reservedKeywords = require('./reserved-keywords');
const objectPath = require('object-path');

// const MAX_BATCH_READ_SIZE   = 100;
const MAX_BATCH_WRITE_SIZE = 25;
const debug = require('debug')('dynamoose:model');

function Model (obj) {
  this.$__.isNew = true;

  for (const key in obj) {
    this[key] = obj[key];
  }
}

async function processCondition (req, options, model) {
  if (options.condition) {
    if (req.ConditionExpression) {
      req.ConditionExpression = `(${req.ConditionExpression}) and (${options.condition})`;
    } else {
      req.ConditionExpression = options.condition;
    }

    if (options.conditionNames) {
      req.ExpressionAttributeNames = {};
      for (const name in options.conditionNames) {
        req.ExpressionAttributeNames[`#${name}`] = options.conditionNames[name];
      }
    }
    if (options.conditionValues) {
      req.ExpressionAttributeValues = {};
      const keys = Object.keys(options.conditionValues);
      for (let i = 0; i < keys.length; i += 1) {
        const k = keys[i];

        const val = options.conditionValues[k];
        const attr = model.$__.schema.attributes[k];
        if (attr) {
          req.ExpressionAttributeValues[`:${k}`] = await attr.toDynamo(val, undefined, model, {'updateTimestamps': false});
        } else {
          throw new errors.ModelError(`Invalid condition value: ${k}. The name must either be in the schema or a full DynamoDB object must be specified.`);
        }
      }
    }
  }
}

/*!
* Register virtuals for this model
* @param {Model} model
* @param {Schema} schema
*/
const applyVirtuals = function (model, schema) {
  debug('applying virtuals');
  for (const i in schema.virtuals) {
    schema.virtuals[i].applyVirtuals(model);
  }
};

/*!
* Register methods for this model
*
* @param {Model} model
* @param {Schema} schema
*/
const applyMethods = function (model, schema) {
  debug('applying methods');
  for (const i in schema.methods) {
    model.prototype[i] = schema.methods[i];
  }
};

/*!
* Register statics for this model
* @param {Model} model
* @param {Schema} schema
*/
const applyStatics = function (model, schema) {
  debug('applying statics');
  for (const i in schema.statics) {
    model[i] = schema.statics[i].bind(model);
  }
};

function sendErrorToCallback (error, options, next) {
  if (typeof options === 'function') {
    next = options;
  }
  if (typeof next === 'function') {
    return next(error);
  }
}

Model.compile = function compile (name, schema, options, base) {
  debug('compiling NewModel %s', name);

  const table = new Table(name, schema, options, base);

  function NewModel (obj) {
    Model.call(this, obj);
    applyVirtuals(this, schema);
    this.$__.originalItem = obj;
  }

  // Set NewModel.name to match name of table. Original property descriptor
  // values are reused.
  const nameDescriptor = Object.getOwnPropertyDescriptor(NewModel, 'name');
  // Skip if 'name' property can not be redefined. This probably means
  // code is running in a "non-standard, pre-ES2015 implementations",
  // like node 0.12.
  if (nameDescriptor.configurable) {
    nameDescriptor.value = `Model-${name}`;
    Object.defineProperty(NewModel, 'name', nameDescriptor);
  }

  util.inherits(NewModel, Model);

  // minimize what is restricted
  NewModel.prototype.$__ = {
    table,
    base,
    name,
    schema,
    options,
    NewModel,
    'plugins': []
  };
  NewModel.$__ = NewModel.prototype.$__;

  NewModel.plugin = function (pluginPackage, pluginOptions) {
    const obj = {
      'event': {
        'plugin': pluginPackage,
        pluginOptions
      }
    };
    // Emit plugin type `plugin:register` with stage `pre`
    this._emit('plugin:register', 'pre', obj);

    // Run actual action to create plugin
    this.$__.plugins.push(new Plugin(this, pluginPackage, pluginOptions, this.plugin.bind(this)));

    // Emit plugin type `plugin:register` with stage `post`
    this._emit('plugin:register', 'post', obj);
  };

  NewModel.clearAllPlugins = function () {
    debug('Clearing all plugins');
    this.$__.plugins = [];
  };

  // This is a private function that is only used to emit signals to plugins within the Dynamoose plugin, this function should not be documentated or used outside of Dynamoose.
  NewModel._emit = async function (type, stage, obj, deferred) {

    if (!obj) {
      obj = {};
    }

    // If obj.actions is undefined set to empty object
    if (!obj.actions) {
      obj.actions = {};
    }

    for (let i = 0; i < this.$__.plugins.length; i += 1) {
      const plugin = this.$__.plugins[i];
      const result = await plugin.emit(type, stage, obj);
      if (result) {
        if (result.resolve) {
          deferred.resolve(result.resolve);
          return true;
        } else if (result.reject) {
          deferred.reject(result.reject);
          return false;
        }
      }
    }
  };

  NewModel.prototype.originalItem = function () {
    return this.$__.originalItem;
  };
  NewModel.prototype.model = function (modelName) {
    const {models} = this.$__.base;
    if (!Object.prototype.hasOwnProperty.call(models, modelName)) {
      const errorMsg = `Schema hasn't been registered for model "${modelName}".\nUse dynamoose.model(name, schema)`;
      throw new errors.ModelError(errorMsg);
    }
    return models[modelName];
  };

  NewModel.get = function (key, getOptions, next) {
    try {
      return Model.get(NewModel, key, getOptions, next);
    } catch (err) {
      sendErrorToCallback(err, getOptions, next);
      return Q.reject(err);
    }
  };

  NewModel.populate = function (populateOptions, resultObj, fillPath) {
    try {
      return Model.populate(populateOptions, resultObj, fillPath);
    } catch (err) {
      sendErrorToCallback(err, populateOptions);
      return Q.reject(err);
    }
  };

  NewModel.update = function (key, update, updateOptions, next) {
    try {
      return Model.update(NewModel, key, update, updateOptions, next);
    } catch (err) {
      sendErrorToCallback(err, updateOptions, next);
      return Q.reject(err);
    }
  };

  NewModel.delete = function (key, deleteOptions, next) {
    try {
      return Model.delete(NewModel, key, deleteOptions, next);
    } catch (err) {
      sendErrorToCallback(err, deleteOptions, next);
      return Q.reject(err);
    }
  };

  NewModel.query = function (query, queryOptions, next) {
    try {
      return Model.query(NewModel, query, queryOptions, next);
    } catch (err) {
      sendErrorToCallback(err, queryOptions, next);
      return Q.reject(err);
    }
  };

  NewModel.queryOne = function (query, queryOneOptions, next) {
    try {
      return Model.queryOne(NewModel, query, queryOneOptions, next);
    } catch (err) {
      sendErrorToCallback(err, queryOneOptions, next);
      return Q.reject(err);
    }
  };

  NewModel.scan = function (filter, scanOptions, next) {
    try {
      return Model.scan(NewModel, filter, scanOptions, next);
    } catch (err) {
      sendErrorToCallback(err, scanOptions, next);
      return Q.reject(err);
    }
  };

  NewModel.create = function (obj, createOptions, next) {
    try {
      return Model.create(NewModel, obj, createOptions, next);
    } catch (err) {
      sendErrorToCallback(err, createOptions, next);
      return Q.reject(err);
    }
  };

  NewModel.batchGet = function (keys, batchGetOptions, next) {
    try {
      return Model.batchGet(NewModel, keys, batchGetOptions, next);
    } catch (err) {
      sendErrorToCallback(err, batchGetOptions, next);
      return Q.reject(err);
    }
  };

  NewModel.batchPut = function (keys, batchPutOptions, next) {
    try {
      return Model.batchPut(NewModel, keys, batchPutOptions, next);
    } catch (err) {
      sendErrorToCallback(err, batchPutOptions, next);
      return Q.reject(err);
    }
  };

  NewModel.batchDelete = function (keys, batchDeleteOptions, next) {
    try {
      return Model.batchDelete(NewModel, keys, batchDeleteOptions, next);
    } catch (err) {
      sendErrorToCallback(err, batchDeleteOptions, next);
      return Q.reject(err);
    }
  };

  NewModel.waitForActive = function (timeout, next) {
    return table.waitForActive(timeout, next);
  };

  NewModel.getTableReq = function () {
    return table.buildTableReq(table.name, table.schema);
  };

  NewModel.conditionCheck = function (key, conditionCheckOptions, next) {
    try {
      return Model.conditionCheck(NewModel, key, conditionCheckOptions, next);
    } catch (err) {
      sendErrorToCallback(err, conditionCheckOptions, next);
      return Q.reject(err);
    }
  };

  function createTransactionFunction (func, val, optionsIndex) {
    return async function (...args) {
      if (typeof args[args.length - 1] === 'function') {
        console.warning('Dynamoose Warning: Passing callback function into transaction method not allowed. Removing callback function from list of arguments.');
        // Callback function passed in which is not allowed, removing that from arguments list
        args.pop();
      }

      // Adding returnRequest to options
      if (args.length >= optionsIndex + 1) {
        args[optionsIndex] = {...args[optionsIndex], 'returnRequest': true};
      } else if (args.length < optionsIndex) {
        args[optionsIndex] = {'returnRequest': true};
      } else {
        args.push({'returnRequest': true});
      }

      const requestObj = await func(...args);

      if (val === 'Update') {
        delete requestObj.ReturnValues;
      }

      return {[val]: requestObj};
    };
  }
  NewModel.transaction = [
    // method - the method to be called when the user calls this method
    // key - the key to be attached to the returned request object to pass into the DynamoDB transaciton request
    // name - the name to attach to the resulting object for NewModel.transaction
    // optionsIndex - the zero-based index for where the options parameter should be in the method arguments list
    {'method': NewModel.get, 'key': 'Get', 'name': 'get', 'optionsIndex': 1},
    {'method': NewModel.delete, 'key': 'Delete', 'name': 'delete', 'optionsIndex': 1},
    {'method': NewModel.create, 'key': 'Put', 'name': 'create', 'optionsIndex': 1},
    {'method': NewModel.update, 'key': 'Update', 'name': 'update', 'optionsIndex': 2},
    {'method': NewModel.conditionCheck, 'key': 'ConditionCheck', 'name': 'conditionCheck', 'optionsIndex': 1}
  ].reduce((original, newItem) => {
    original[newItem.name] = createTransactionFunction(newItem.method, newItem.key, newItem.optionsIndex);
    return original;
  }, {});

  // apply methods and statics
  applyMethods(NewModel, schema);
  applyStatics(NewModel, schema);

  // set up middleware
  for (const k in hooks) {
    NewModel[k] = hooks[k];
  }

  table.init((err) => {
    if (err) {
      throw err;
    }
  });

  return NewModel;
};

function validKeyValue (value) {
  return value !== undefined && value !== null && value !== '';
}

Model.conditionCheck = async function (NewModel, key, options, next) {
  debug('Condition Check', this);
  const deferred = Q.defer();

  try {
    options = options || {};
    if (typeof options === 'function') {
      next = options;
      options = {};
    }

    const {schema} = NewModel.$__;
    const hashKeyName = schema.hashKey.name;

    if (!validKeyValue(key[hashKeyName])) {
      const keyVal = key;
      key = {};
      key[hashKeyName] = keyVal;
    }

    if (schema.rangeKey && !validKeyValue(key[schema.rangeKey.name])) {
      deferred.reject(new errors.ModelError(`Range key required: ${schema.rangeKey.name}`));
      return deferred.promise.nodeify(next);
    }

    const conditionReq = {
      'TableName': NewModel.$__.name,
      'Key': {}
    };
    try {
      conditionReq.Key[hashKeyName] = schema.hashKey.toDynamo(key[hashKeyName], undefined, key);
    } catch (e) {
      deferred.reject(e);
    }

    if (schema.rangeKey) {
      const rangeKeyName = schema.rangeKey.name;
      conditionReq.Key[rangeKeyName] = schema.rangeKey.toDynamo(key[rangeKeyName], undefined, key);
    }
    await processCondition(conditionReq, options, NewModel);

    debug('Condition Check', conditionReq);
    deferred.resolve(conditionReq);
  } catch (err) {
    deferred.reject(err);
  }
  return deferred.promise.nodeify(next);
};

Model.prototype.put = async function (options, next) {
  debug('put', this);
  const deferred = Q.defer();
  const shouldContinuePutCalled = await this.$__.NewModel._emit('model:put', 'put:called', {'event': {'callback': next, options}, 'actions': {'updateCallback' (cb) { next = cb; }, 'updateOptions' (newOptions) { options = newOptions; }}}, deferred);
  if (shouldContinuePutCalled === false) { return; }
  let item;

  const putItem = async () => {
    const shouldContinueRequestPre = await this.$__.NewModel._emit('model:put', 'request:pre', {'event': {options, item}, 'actions': {'updateItem' (newItem) { item = newItem; }}}, deferred);
    if (shouldContinueRequestPre === false) { return; }
    this.$__.base.ddb().putItem(item, async (err) => {
      const shouldContinueRequestPost = await this.$__.NewModel._emit('model:put', 'request:post', {'event': {options, item, 'error': err}, 'actions': {'updateError' (newErr) { err = newErr; }}}, deferred);
      if (shouldContinueRequestPost === false) { return; }
      if (err) {
        deferred.reject(err);
      }
      deferred.resolve(this);
    });
  };

  try {
    options = options || {};
    if (typeof options === 'function') {
      next = options;
      options = {};
    }
    if (options.overwrite === null || options.overwrite === undefined) {
      options.overwrite = true;
    }

    const toDynamoOptions = {
      'updateTimestamps': true
    };

    if (options.updateTimestamps === false) {
      toDynamoOptions.updateTimestamps = false;
    }

    if (options.updateExpires === true) {
      toDynamoOptions.updateExpires = true;
    }

    const {schema} = this.$__;
    item = {
      'TableName': this.$__.name,
      'Item': await schema.toDynamo(this, toDynamoOptions)
    };

    await schema.parseDynamo(this, item.Item);

    if (!options.overwrite) {
      if (!reservedKeywords.isReservedKeyword(schema.hashKey.name) && !schema.hashKey.name.startsWith('_')) {
        item.ConditionExpression = `attribute_not_exists(${schema.hashKey.name})`;
      } else {
        item.ConditionExpression = 'attribute_not_exists(#__hash_key)';
        item.ExpressionAttributeNames = item.ExpressionAttributeNames || {};
        item.ExpressionAttributeNames['#__hash_key'] = schema.hashKey.name;
      }
    }
    await processCondition(item, options, this);

    debug('putItem', item);

    if (options.returnRequest) {
      deferred.resolve(item);
    } else if (this.$__.options.waitForActive) {
      this.$__.table.waitForActive().then(putItem).catch(deferred.reject);
    } else {
      putItem();
    }
  } catch (err) {
    deferred.reject(err);
  }
  return deferred.promise.nodeify(next);
};

Model.prototype.save = Model.prototype.put;

Model.create = function (NewModel, obj, options, next) {
  options = options || {};

  if (typeof options === 'function') {
    next = options;
    options = {};
  }

  if (options.overwrite === null || options.overwrite === undefined) {
    options.overwrite = false;
  }

  const model = new NewModel(obj);
  return model.save(options, next);
};

Model.get = async function (NewModel, key, options, next) {
  debug('Get %j', key);
  const deferred = Q.defer();
  const shouldContinueGetCalled = await NewModel._emit('model:get', 'get:called', {'event': {'callback': next, key, options}, 'actions': {'updateCallback' (cb) { next = cb; }, 'updateKey' (newKey) { key = newKey; }, 'updateOptions' (newOptions) { options = newOptions; }}}, deferred);
  if (shouldContinueGetCalled === false) { return; }

  options = options || {};
  if (typeof options === 'function') {
    next = options;
    options = {};
  }

  if (key === null || key === undefined) {
    deferred.reject(new errors.ModelError('Key required to get item'));
    return deferred.promise.nodeify(next);
  }

  const {schema} = NewModel.$__;
  const hashKeyName = schema.hashKey.name;

  if (typeof key === 'object' && !validKeyValue(key[hashKeyName])) {
    deferred.reject(new errors.ModelError(`Hash key required: ${schema.hashKey.name}`));
    return deferred.promise.nodeify(next);
  }

  if (!validKeyValue(key[hashKeyName])) {
    const keyVal = key;
    key = {};
    key[hashKeyName] = keyVal;
  }

  if (schema.rangeKey && !validKeyValue(key[schema.rangeKey.name])) {
    deferred.reject(new errors.ModelError(`Range key required: ${schema.rangeKey.name}`));
    return deferred.promise.nodeify(next);
  }

  let getReq = {
    'TableName': NewModel.$__.name,
    'Key': {}
  };

  try {
    getReq.Key[hashKeyName] = await schema.hashKey.toDynamo(key[hashKeyName], undefined, key);
  } catch (e) {
    deferred.reject(e);
  }

  if (schema.rangeKey) {
    const rangeKeyName = schema.rangeKey.name;
    getReq.Key[rangeKeyName] = await schema.rangeKey.toDynamo(key[rangeKeyName], undefined, key);
  }

  if (options.attributes) {
    getReq.AttributesToGet = options.attributes;
  }

  if (options.consistent) {
    getReq.ConsistentRead = true;
  }

  const newModel$ = NewModel.$__;

  async function get () {
    debug('getItem', getReq);

    const emitEventObjectRequestPre = {'callback': next, key, options, 'getRequest': getReq};
    const emitObjectRequestPre = {'event': emitEventObjectRequestPre, 'actions': {'updateCallback' (cb) { next = cb; }, 'updateKey' (newKey) { key = newKey; }, 'updateOptions' (newOptions) { options = newOptions; }, 'updateGetRequest' (newReq) { getReq = newReq; }}};
    const shouldContinueRequestPre = await NewModel._emit('model:get', 'request:pre', emitObjectRequestPre, deferred);
    if (shouldContinueRequestPre === false) { return; }

    newModel$.base.ddb().getItem(getReq, async (err, data) => {

      const emitEventObjectRequestPost = {'callback': next, key, options, 'error': err, data};
      const emitObjectRequestPost = {'event': emitEventObjectRequestPost, 'actions': {'updateCallback' (cb) { next = cb; }, 'updateKey' (newKey) { key = newKey; }, 'updateOptions' (newOptions) { options = newOptions; }, 'updateError' (newErr) { err = newErr; }, 'updateData' (newData) { data = newData; }}};
      const shouldContinueRequestPost = await NewModel._emit('model:get', 'request:post', emitObjectRequestPost, deferred);
      if (shouldContinueRequestPost === false) { return; }

      if (err) {
        debug('Error returned by getItem', err);
        return deferred.reject(err);
      }

      debug('getItem response', data);

      if (!Object.keys(data).length) {
        return deferred.resolve();
      }

      const model = new NewModel();

      model.$__.isNew = false;
      try {
        await schema.parseDynamo(model, data.Item);
      } catch (e) {
        debug('cannot parse data', data);
        return deferred.reject(e);
      }

      if (schema.expires && schema.expires.returnExpiredItems === false && model[schema.expires.attribute] < new Date()) {
        deferred.resolve();
      }

      debug('getItem parsed model', model);
      deferred.resolve(model);
    });
  }

  if (options.returnRequest) {
    deferred.resolve(getReq);
  } else if (newModel$.options.waitForActive) {
    newModel$.table.waitForActive().then(get).catch(deferred.reject);
  } else {
    get();
  }
  return deferred.promise.nodeify(next);
};

Model.prototype.populate = function (options, resultObj, fillPath) {
  if (!fillPath) {
    fillPath = [];
  }
  const returnObj = resultObj || this;

  let ModelPathName = '';

  if (typeof options === 'string') {
    ModelPathName = this.$__.table.schema.attributes[options].options.ref;
  } else if (options.path) {
    ModelPathName = this.$__.table.schema.attributes[options.path].options.ref;
  } else if (!options.path && !options.model) {
    throw new Error('Invalid parameters provided to the populate method');
  }

  const modelPropName = Object.keys(this.$__.table.base.models).filter((key) => {
    const model = this.$__.table.base.models[key];
    return (
      model.$__.name === model.$__.options.prefix + options.model + model.$__.options.suffix ||
      model.$__.name === model.$__.options.prefix + options.model ||
      model.$__.name === options.model + model.$__.options.suffix ||
      model.$__.name === options.model ||
      model.$__.name === model.$__.options.prefix + ModelPathName + model.$__.options.suffix ||
      model.$__.name === model.$__.options.prefix + ModelPathName ||
      model.$__.name === ModelPathName + model.$__.options.suffix ||
      model.$__.name === ModelPathName
    );
  }).pop();
  const ConnectedModel = this.$__.table.base.models[modelPropName];

  if (!ConnectedModel) {
    throw new Error('The provided model doesn\'t exists');
  }

  return ConnectedModel
    .get(this[options.path || options])
    .then((target) => {
      if (!target) {
        throw new Error('Invalid reference');
      }
      this[options.path || options] = target;
      fillPath = fillPath.concat(options.path || options);
      objectPath.set(returnObj, fillPath, target);
      if (options.populate) {
        return this[options.path || options].populate(options.populate, returnObj, fillPath);
      }
      return returnObj;
    });
};

/* NewModel.update({id: 123},
// {
//   $PUT: {a: 1, b: 2},
//   $DELETE: {c: 'a'},
//   $ADD: {count: 1}
// });
// NewModel.update({id: 123}, { $PUT: {a: 1, b: 2} });
// NewModel.update({id: 123}, {a: 1, b: 2} ); // Defaults to put (same as above)*/
Model.update = async function (NewModel, key, update, options, next) {
  debug('Update %j', key);
  const deferred = Q.defer();
  const {schema} = NewModel.$__;

  if (typeof update === 'function') {
    next = update;
    update = null;
  }

  if (update === undefined || update === null) {
    update = key;
  }

  options = options || {};
  if (typeof options === 'function') {
    next = options;
    options = {};
  }

  // default createRequired to false
  if (typeof options.createRequired === 'undefined') {
    options.createRequired = false;
  }

  // default updateTimestamps to true
  if (typeof options.updateTimestamps === 'undefined') {
    options.updateTimestamps = true;
  }

  // default return values to 'ALL_NEW' unless `defaultReturnValues` is set;
  if (typeof options.returnValues === 'undefined') {
    options.returnValues = NewModel.$__.options.defaultReturnValues || 'ALL_NEW';
  }

  // if the key part was emtpy, try the key defaults before giving up...
  if (key === null || key === undefined) {
    key = {};

    // first figure out the primary/hash key
    const hashKeyDefault = schema.attributes[schema.hashKey.name].options.default;

    if (typeof hashKeyDefault === 'undefined') {
      deferred.reject(new errors.ModelError('Key required to get item'));
      return deferred.promise.nodeify(next);
    }

    key[schema.hashKey.name] = typeof hashKeyDefault === 'function' ? hashKeyDefault() : hashKeyDefault;

    // now see if you have to figure out a range key
    if (schema.rangeKey) {
      const rangeKeyDefault = schema.attributes[schema.rangeKey.name].options.default;

      if (typeof rangeKeyDefault === 'undefined') {
        deferred.reject(new errors.ModelError(`Range key required: ${schema.rangeKey.name}`));
        return deferred.promise.nodeify(next);
      }

      key[schema.rangeKey.name] = typeof rangeKeyDefault === 'function' ? rangeKeyDefault() : rangeKeyDefault;
    }
  }

  const hashKeyName = schema.hashKey.name;
  if (!key[hashKeyName]) {
    const keyVal = key;
    key = {};
    key[hashKeyName] = keyVal;
  }

  const updateReq = {
    'TableName': NewModel.$__.name,
    'Key': {},
    'ExpressionAttributeNames': {},
    'ExpressionAttributeValues': {},
    'ReturnValues': options.returnValues
  };
  await processCondition(updateReq, options, NewModel);

  updateReq.Key[hashKeyName] = await schema.hashKey.toDynamo(key[hashKeyName], undefined, key);

  if (schema.rangeKey) {
    const rangeKeyName = schema.rangeKey.name;
    updateReq.Key[rangeKeyName] = await schema.rangeKey.toDynamo(key[rangeKeyName], undefined, key);
  }

  // determine the set of operations to be executed
  function Operations () {
    this.ifNotExistsSet = {};
    this.SET = {};
    this.ADD = {};
    this.REMOVE = {};
    this.LISTAPPEND = {};

    this.addIfNotExistsSet = function (name, item) {
      this.ifNotExistsSet[name] = item;
    };

    this.addSet = function (name, item) {
      if (schema.hashKey.name !== name && (schema.rangeKey || {}).name !== name) {
        this.SET[name] = item;
      }
    };

    this.addListAppend = function (name, item) {
      if (schema.hashKey.name !== name && (schema.rangeKey || {}).name !== name) {
        this.LISTAPPEND[name] = item;
      }
    };

    this.addAdd = function (name, item) {
      if (schema.hashKey.name !== name && (schema.rangeKey || {}).name !== name) {
        this.ADD[name] = item;
      }
    };

    this.addRemove = function (name, item) {
      if (schema.hashKey.name !== name && (schema.rangeKey || {}).name !== name) {
        this.REMOVE[name] = item;
      }
    };

    this.getUpdateExpression = function (getUpdateReq) {
      let attrCount = 0;
      let updateExpression = '';

      let attrName;
      let valName;
      let name;
      let item;

      const setExpressions = [];
      for (name in this.ifNotExistsSet) {
        item = this.ifNotExistsSet[name];

        attrName = `#_n${attrCount}`;
        valName = `:_p${attrCount}`;

        getUpdateReq.ExpressionAttributeNames[attrName] = name;
        getUpdateReq.ExpressionAttributeValues[valName] = item;

        setExpressions.push(`${attrName} = if_not_exists(${attrName}, ${valName})`);

        attrCount += 1;
      }

      for (name in this.SET) {
        item = this.SET[name];

        attrName = `#_n${attrCount}`;
        valName = `:_p${attrCount}`;

        getUpdateReq.ExpressionAttributeNames[attrName] = name;
        getUpdateReq.ExpressionAttributeValues[valName] = item;

        setExpressions.push(`${attrName} = ${valName}`);

        attrCount += 1;
      }
      for (name in this.LISTAPPEND) {
        item = this.LISTAPPEND[name];

        attrName = `#_n${attrCount}`;
        valName = `:_p${attrCount}`;

        getUpdateReq.ExpressionAttributeNames[attrName] = name;
        getUpdateReq.ExpressionAttributeValues[valName] = item;

        setExpressions.push(`${attrName} = list_append(${attrName}, ${valName})`);

        attrCount += 1;
      }
      if (setExpressions.length > 0) {
        updateExpression += `SET ${setExpressions.join(',')} `;
      }

      const addExpressions = [];
      for (name in this.ADD) {
        item = this.ADD[name];

        attrName = `#_n${attrCount}`;
        valName = `:_p${attrCount}`;

        getUpdateReq.ExpressionAttributeNames[attrName] = name;
        getUpdateReq.ExpressionAttributeValues[valName] = item;

        addExpressions.push(`${attrName} ${valName}`);

        attrCount += 1;
      }
      if (addExpressions.length > 0) {
        updateExpression += `ADD ${addExpressions.join(',')} `;
      }

      const removeExpressions = [];
      for (name in this.REMOVE) {
        item = this.REMOVE[name];

        attrName = `#_n${attrCount}`;

        getUpdateReq.ExpressionAttributeNames[attrName] = name;

        removeExpressions.push(attrName);

        attrCount += 1;
      }
      if (removeExpressions.length > 0) {
        updateExpression += `REMOVE ${removeExpressions.join(',')}`;
      }

      getUpdateReq.UpdateExpression = updateExpression;
    };
  }

  const operations = new Operations();

  if (update.$PUT || !update.$PUT && !update.$DELETE && !update.$ADD) {
    const updatePUT = update.$PUT || update;

    for (const putItem in updatePUT) {
      const putAttr = schema.attributes[putItem];
      if (putAttr || schema.options.saveUnknown) {
        const val = updatePUT[putItem];

        let removeParams = val === null || val === undefined || val === '';

        if (!options.allowEmptyArray) {
          removeParams = removeParams || Array.isArray(val) && val.length === 0;
        }

        if (removeParams) {
          operations.addRemove(putItem, null);
        } else {
          try {
            if (putAttr) {
              operations.addSet(putItem, await putAttr.toDynamo(val));
            } else if (schema.options.saveUnknown) {
              operations.addSet(putItem, await Attribute.create(schema, putItem, val).toDynamo(val));
            }
          } catch (err) {
            deferred.reject(err);
            return deferred.promise.nodeify(next);
          }
        }
      }
    }
  }

  if (update.$DELETE) {
    for (const deleteItem in update.$DELETE) {
      const deleteAttr = schema.attributes[deleteItem];
      if (deleteAttr || schema.options.saveUnknown) {
        const delVal = update.$DELETE[deleteItem];
        if (delVal !== null && delVal !== undefined) {
          try {
            if (deleteAttr) {
              operations.addRemove(deleteItem, await deleteAttr.toDynamo(delVal));
            } else {
              operations.addRemove(deleteItem, await Attribute.create(schema, deleteItem, delVal).toDynamo(delVal));
            }
          } catch (err) {
            deferred.reject(err);
            return deferred.promise.nodeify(next);
          }
        } else {
          operations.addRemove(deleteItem, null);
        }
      }
    }
  }

  if (update.$ADD) {
    for (const addItem in update.$ADD) {
      const addAttr = schema.attributes[addItem];
      const addVal = update.$ADD[addItem];
      try {
        if (addAttr) {
          if (addAttr.type.name === 'list') {
            operations.addListAppend(addItem, await addAttr.toDynamo(addVal));
          } else {
            operations.addAdd(addItem, await addAttr.toDynamo(addVal));
          }
        } else if (schema.options.saveUnknown) {
          operations.addAdd(addItem, await Attribute.create(schema, addItem, addVal).toDynamo(addVal));
        }
      } catch (err) {
        deferred.reject(err);
        return deferred.promise.nodeify(next);
      }
    }
  }

  // update schema timestamps
  if (options.updateTimestamps && schema.timestamps) {
    const createdAtLabel = schema.timestamps.createdAt;
    const updatedAtLabel = schema.timestamps.updatedAt;

    const createdAtAttribute = schema.attributes[createdAtLabel];
    const updatedAtAttribute = schema.attributes[updatedAtLabel];

    const createdAtDefaultValue = createdAtAttribute.options.default();
    const updatedAtDefaultValue = updatedAtAttribute.options.default();

    operations.addIfNotExistsSet(createdAtLabel, await createdAtAttribute.toDynamo(createdAtDefaultValue));
    operations.addSet(updatedAtLabel, await updatedAtAttribute.toDynamo(updatedAtDefaultValue));
  }

  // update schema expires
  if (options.updateExpires && schema.expires) {
    const expiresLabel = schema.expires.attribute;
    const expiresAttribute = schema.attributes[expiresLabel];
    const expiresDefaultValue = expiresAttribute.options.default();

    operations.addSet(expiresLabel, await expiresAttribute.toDynamo(expiresDefaultValue));
  }

  // do the required items check. Throw an error if you have an item that is required and
  //  doesn't have a default.
  if (options.createRequired) {
    for (const attributeName in schema.attributes) {
      const attribute = schema.attributes[attributeName];
      if (attribute.required && // if the attribute is required...
        attributeName !== schema.hashKey.name && // ...and it isn't the hash key...
        (!schema.rangeKey || attributeName !== schema.rangeKey.name) && // ...and it isn't the range key...
        (!schema.timestamps || attributeName !== schema.timestamps.createdAt) && // ...and it isn't the createdAt attribute...
        (!schema.timestamps || attributeName !== schema.timestamps.updatedAt) && // ...and it isn't the updatedAt attribute...
        !operations.SET[attributeName] &&
        !operations.ADD[attributeName] &&
        !operations.REMOVE[attributeName]) {

        const defaultValueOrFunction = attribute.options.default;

        // throw an error if you have required attribute without a default (and you didn't supply
        //  anything to update with)
        if (typeof defaultValueOrFunction === 'undefined') {
          const err = `Required attribute "${attributeName}" does not have a default.`;
          debug('Error returned by updateItem', err);
          deferred.reject(err);
          return deferred.promise.nodeify(next);
        }

        const defaultValue = typeof defaultValueOrFunction === 'function' ? defaultValueOrFunction() : defaultValueOrFunction;

        operations.addIfNotExistsSet(attributeName, await attribute.toDynamo(defaultValue));
      }
    }
  }

  operations.getUpdateExpression(updateReq);

  // AWS doesn't allow empty expressions or attribute collections
  if (!updateReq.UpdateExpression) {
    delete updateReq.UpdateExpression;
  }
  if (!Object.keys(updateReq.ExpressionAttributeNames).length) {
    delete updateReq.ExpressionAttributeNames;
  }
  if (!Object.keys(updateReq.ExpressionAttributeValues).length) {
    delete updateReq.ExpressionAttributeValues;
  }

  const newModel$ = NewModel.$__;

  function updateItem () {
    debug('updateItem', updateReq);
    newModel$.base.ddb().updateItem(updateReq, async (err, data) => {
      if (err) {
        debug('Error returned by updateItem', err);
        return deferred.reject(err);
      }
      debug('updateItem response', data);

      if (!Object.keys(data).length) {
        return deferred.resolve();
      }

      const model = new NewModel();
      model.$__.isNew = false;
      try {
        await schema.parseDynamo(model, data.Attributes);
      } catch (e) {
        debug('cannot parse data', data);
        return deferred.reject(e);
      }

      debug('updateItem parsed model', model);

      deferred.resolve(model);
    });
  }

  if (options.returnRequest) {
    deferred.resolve(updateReq);
  } else if (newModel$.options.waitForActive) {
    newModel$.table.waitForActive().then(updateItem).catch(deferred.reject);
  } else {
    updateItem();
  }

  return deferred.promise.nodeify(next);
};

Model.delete = function (NewModel, key, options, next) {

  const {schema} = NewModel.$__;

  const hashKeyName = schema.hashKey.name;
  if (!key[hashKeyName]) {
    const keyVal = key;
    key = {};
    key[hashKeyName] = keyVal;
  }

  if (schema.rangeKey && !key[schema.rangeKey.name]) {
    const deferred = Q.defer();
    deferred.reject(new errors.ModelError('Range key required: %s', schema.hashKey.name));
    return deferred.promise.nodeify(next);
  }

  const model = new NewModel(key);
  return model.delete(options, next);
};

Model.prototype.delete = async function (options, next) {
  options = options || {};
  if (typeof options === 'function') {
    next = options;
    options = {};
  }

  const {schema} = this.$__;

  const hashKeyName = schema.hashKey.name;

  const deferred = Q.defer();

  if (this[hashKeyName] === null || this[hashKeyName] === undefined) {
    deferred.reject(new errors.ModelError('Hash key required: %s', hashKeyName));
    return deferred.promise.nodeify(next);
  }

  if (schema.rangeKey && (this[schema.rangeKey.name] === null || this[schema.rangeKey.name] === undefined)) {
    deferred.reject(new errors.ModelError('Range key required: %s', schema.hashKey.name));
    return deferred.promise.nodeify(next);
  }

  const getDelete = {
    'TableName': this.$__.name,
    'Key': {}
  };

  try {
    getDelete.Key[hashKeyName] = await schema.hashKey.toDynamo(this[hashKeyName], undefined, this);

    if (schema.rangeKey) {
      const rangeKeyName = schema.rangeKey.name;
      getDelete.Key[rangeKeyName] = await schema.rangeKey.toDynamo(this[rangeKeyName], undefined, this);
    }
  } catch (err) {
    deferred.reject(err);
    return deferred.promise.nodeify(next);
  }

  if (options.update) {
    getDelete.ReturnValues = 'ALL_OLD';
    getDelete.ConditionExpression = `attribute_exists(${schema.hashKey.name})`;
  }

  const deleteItem = () => {

    debug('deleteItem', getDelete);
    this.$__.base.ddb().deleteItem(getDelete, async (err, data) => {
      if (err) {
        debug('Error returned by deleteItem', err);
        return deferred.reject(err);
      }
      debug('deleteItem response', data);

      if (options.update && data.Attributes) {
        try {
          await schema.parseDynamo(this, data.Attributes);
          debug('deleteItem parsed model', this);
        } catch (parseDynamoError) {
          return deferred.reject(parseDynamoError);
        }
      }

      deferred.resolve(this);
    });
  };

  if (options.returnRequest) {
    deferred.resolve(getDelete);
  } else if (this.$__.options.waitForActive) {
    this.$__.table.waitForActive().then(deleteItem).catch(deferred.reject);
  } else {
    deleteItem();
  }

  return deferred.promise.nodeify(next);

};

/*
query(query, options, callback);
query(query, options)...exec(callback);
query(query, callback);
query(query)...exec(callback);
query({hash: {id: {eq : 1}}, range: {name: {beginwith: 'foxy'}}})
query({id: 1})
query('id').eq(1).where('name').begienwith('foxy')
*/
Model.query = function (NewModel, query, options, next) {
  if (typeof options === 'function') {
    next = options;
    options = null;
  }

  query = new Query(NewModel, query, options);

  if (next) {
    query.exec(next);
  }

  return query;
};

Model.queryOne = function (NewModel, query, options, next) {
  if (typeof options === 'function') {
    next = options;
    options = null;
  }

  query = new Query(NewModel, query, options);
  query.one();

  if (next) {
    query.exec(next);
  }

  return query;
};

/*

scan(filter, options, callback);
scan(filter, options)...exec(callback);
scan(filter, callback);
scan(filter)...exec(callback);

scan('id').between(a, b).and().where('name').begienwith('foxy').exec();

scan().exec(); // All records

*/
Model.scan = function (NewModel, filter, options, next) {
  if (typeof options === 'function') {
    next = options;
    options = null;
  }

  const scan = new Scan(NewModel, filter, options);

  if (next) {
    scan.exec(next);
  }

  return scan;
};

Model.batchGet = async function (NewModel, keys, options, next) {
  debug('BatchGet %j', keys);
  const deferred = Q.defer();
  if (!Array.isArray(keys)) {
    deferred.reject(new errors.ModelError('batchGet requires keys to be an array'));
    return deferred.promise.nodeify(next);
  }
  options = options || {};
  if (typeof options === 'function') {
    next = options;
    options = {};
  }

  const {schema} = NewModel.$__;

  const hashKeyName = schema.hashKey.name;
  keys = keys.map((key) => {
    if (!key[hashKeyName]) {
      const ret = {};
      ret[hashKeyName] = key;
      return ret;
    }
    return key;
  });

  if (schema.rangeKey && !keys.every((key) => validKeyValue(key[schema.rangeKey.name]))) {
    deferred.reject(new errors.ModelError(`Range key required: ${schema.rangeKey.name}`));
    return deferred.promise.nodeify(next);
  }

  const batchReq = {
    'RequestItems': {}
  };

  const getReq = {};
  batchReq.RequestItems[NewModel.$__.name] = getReq;

  getReq.Keys = await Promise.all(keys.map(async (key) => {
    const ret = {};
    ret[hashKeyName] = await schema.hashKey.toDynamo(key[hashKeyName], undefined, key);

    if (schema.rangeKey) {
      const rangeKeyName = schema.rangeKey.name;
      ret[rangeKeyName] = await schema.rangeKey.toDynamo(key[rangeKeyName], undefined, key);
    }
    return ret;
  }));

  if (options.attributes) {
    getReq.AttributesToGet = options.attributes;
  }

  if (options.consistent) {
    getReq.ConsistentRead = true;
  }

  const newModel$ = NewModel.$__;

  function batchGet () {
    debug('batchGetItem', batchReq);
    newModel$.base.ddb().batchGetItem(batchReq, async (err, data) => {
      if (err) {
        debug('Error returned by batchGetItem', err);
        return deferred.reject(err);
      }
      debug('batchGetItem response', data);

      if (!Object.keys(data).length) {
        return deferred.resolve();
      }

      async function toModel (item) {
        const model = new NewModel();
        model.$__.isNew = false;
        await schema.parseDynamo(model, item);

        debug('batchGet parsed model', model);

        return model;
      }

      const models = data.Responses[newModel$.name] ? (await Promise.all(data.Responses[newModel$.name].map(toModel))).filter((item) => !(schema.expires && schema.expires.returnExpiredItems === false && item[schema.expires.attribute] && item[schema.expires.attribute] < new Date())) : [];
      if (data.UnprocessedKeys[newModel$.name]) {
        // convert unprocessed keys back to dynamoose format
        models.unprocessed = await Promise.all(data.UnprocessedKeys[newModel$.name].Keys.map(async (key) => {
          const ret = {};
          ret[hashKeyName] = await schema.hashKey.parseDynamo(key[hashKeyName]);

          if (schema.rangeKey) {
            const rangeKeyName = schema.rangeKey.name;
            ret[rangeKeyName] = await schema.rangeKey.parseDynamo(key[rangeKeyName]);
          }
          return ret;
        }));
      }
      deferred.resolve(models);
    });
  }

  if (newModel$.options.waitForActive) {
    newModel$.table.waitForActive().then(batchGet).catch(deferred.reject);
  } else {
    batchGet();
  }
  return deferred.promise.nodeify(next);
};

async function toBatchChunks (modelName, list, chunkSize, requestMaker) {
  const listClone = list.slice(0);
  let chunk = [];
  const batchChunks = [];

  while ((chunk = listClone.splice(0, chunkSize)).length) {
    const requests = await Promise.all(chunk.map(requestMaker));
    const batchReq = {
      'RequestItems': {}
    };

    batchReq.RequestItems[modelName] = requests;
    batchChunks.push(batchReq);
  }

  return batchChunks;
}

function reduceBatchResult (resultList) {

  return resultList.reduce((acc, res) => {
    const responses = res.Responses ? res.Responses : {};
    const unprocessed = res.UnprocessedItems ? res.UnprocessedItems : {};

    // merge responses
    for (const tableName in responses) {
      if (Object.prototype.hasOwnProperty.call(responses, tableName)) {
        let consumed = acc.Responses[tableName] ? acc.Responses[tableName].ConsumedCapacityUnits : 0;
        consumed += responses[tableName].ConsumedCapacityUnits;

        acc.Responses[tableName] = {
          'ConsumedCapacityUnits': consumed
        };
      }
    }

    // merge unprocessed items
    for (const tableName2 in unprocessed) {
      if (Object.prototype.hasOwnProperty.call(unprocessed, tableName2)) {
        const items = acc.UnprocessedItems[tableName2] ? acc.UnprocessedItems[tableName2] : [];
        items.push(unprocessed[tableName2]);
        acc.UnprocessedItems[tableName2] = items;
      }
    }

    return acc;
  }, {'Responses': {}, 'UnprocessedItems': {}});
}

function batchWriteItems (NewModel, batchRequests) {
  debug('batchWriteItems');
  const newModel$ = NewModel.$__;

  const batchList = batchRequests.map((batchReq) => {
    const deferredBatch = Q.defer();

    newModel$.base.ddb().batchWriteItem(batchReq, (err, data) => {
      if (err) {
        debug('Error returned by batchWriteItems', err);
        return deferredBatch.reject(err);
      }

      deferredBatch.resolve(data);
    });

    return deferredBatch.promise;
  });

  return Q.all(batchList).then((resultList) => reduceBatchResult(resultList));
}

Model.batchPut = async function (NewModel, items, options, next) {
  debug('BatchPut %j', items);
  const deferred = Q.defer();

  if (!Array.isArray(items)) {
    deferred.reject(new errors.ModelError('batchPut requires items to be an array'));
    return deferred.promise.nodeify(next);
  }
  options = options || {};
  if (typeof options === 'function') {
    next = options;
    options = {};
  }

  const {schema} = NewModel.$__;
  const newModel$ = NewModel.$__;

  const toDynamoOptions = {
    'updateTimestamps': options.updateTimestamps || false,
    'updateExpires': options.updateExpires || false
  };

  const batchRequests = await toBatchChunks(newModel$.name, items, MAX_BATCH_WRITE_SIZE, async (item) => ({
    'PutRequest': {
      'Item': await schema.toDynamo(item, toDynamoOptions)
    }
  }));

  const batchPut = function () {
    batchWriteItems(NewModel, batchRequests).then((result) => {
      deferred.resolve(result);
    }).fail((err) => {
      deferred.reject(err);
    });
  };

  if (newModel$.options.waitForActive) {
    newModel$.table.waitForActive().then(batchPut).catch(deferred.reject);
  } else {
    batchPut();
  }
  return deferred.promise.nodeify(next);
};

Model.batchDelete = async function (NewModel, keys, options, next) {
  debug('BatchDel %j', keys);
  const deferred = Q.defer();

  if (!Array.isArray(keys)) {
    deferred.reject(new errors.ModelError('batchDelete requires keys to be an array'));
    return deferred.promise.nodeify(next);
  }

  options = options || {};
  if (typeof options === 'function') {
    next = options;
    options = {};
  }

  const {schema} = NewModel.$__;
  const newModel$ = NewModel.$__;
  const hashKeyName = schema.hashKey.name;

  const batchRequests = await toBatchChunks(newModel$.name, keys, MAX_BATCH_WRITE_SIZE, async (key) => {
    const key_element = {};
    key_element[hashKeyName] = await schema.hashKey.toDynamo(key[hashKeyName], undefined, key);

    if (schema.rangeKey) {
      key_element[schema.rangeKey.name] = await schema.rangeKey.toDynamo(key[schema.rangeKey.name], undefined, key);
    }

    return {
      'DeleteRequest': {
        'Key': key_element
      }
    };
  });

  const batchDelete = function () {
    batchWriteItems(NewModel, batchRequests).then((result) => {
      deferred.resolve(result);
    }).fail((err) => {
      deferred.reject(err);
    });
  };

  if (newModel$.options.waitForActive) {
    newModel$.table.waitForActive().then(batchDelete).catch(deferred.reject);
  } else {
    batchDelete();
  }
  return deferred.promise.nodeify(next);
};

module.exports = Model;


'use strict';
const debug = require('debug')('dynamoose:attribute');
const errors = require('./errors');

function Attribute (schema, name, value) {
  this.options = {};

  debug('Creating attribute %s %o', name, value);
  if (value.type) {
    this.options = value;
  }

  this.schema = schema;

  this.name = name;
  if (this.schema.options.saveUnknown) {
    this.setTypeFromRawValue(value);
  } else {
    this.setType(value);
  }

  if (!schema.useDocumentTypes) {
    if (this.type.name === 'map') {
      debug('Overwriting attribute %s type to object', name);
      this.type = this.types.object;
    } else if (this.type.name === 'list') {
      debug('Overwriting attribute %s type to array', name);
      this.type = this.types.array;
    }
  }

  if (schema.useNativeBooleans) {
    if (this.type.name === 'boolean') {
      debug('Overwriting attribute %s type to be a native boolean', name);
      this.type = this.types.nativeBoolean;
    }
  }

  this.attributes = {};

  if (this.type.name === 'map') {

    if (value.type) {
      if (!value.map) {
        throw new errors.SchemaError(`No map schema given for attribute: ${this.name}`);
      }
      value = value.map;
    }
    for (const subattrName in value) {
      if (this.attributes[subattrName]) {
        throw new errors.SchemaError(`Duplicate attribute: ${subattrName} in ${this.name}`);
      }

      this.attributes[subattrName] = module.exports.create(schema, subattrName, value[subattrName]);
    }

  } else if (this.type.name === 'list') {

    if (value.type) {
      if (!value.list || !value.list[0]) {
        throw new errors.SchemaError(`No list schema given for attribute: ${this.name}`);
      }
      value = value.list;
    }

    // stang: Don't know what this guard is for - had to remove because when parsing unknown attributes, this is legal?
    // if (value.length > 1){
    //   throw new errors.SchemaError(`Only one object can be defined as a list type in ${this.name}`);
    // }

    for (let i = 0; i < value.length; i += 1) {
      this.attributes[i] = module.exports.create(schema, 0, value[i]);
    }

  }

  if (this.options) {
    this.applyDefault(this.options.default);

    this.required = this.options.required;
    if (this.options.set) {
      if (typeof this.options.set === 'function') {
        this.set = this.options.set;
      } else if (this.options.set.isAsync) {
        this.set = function (val) {
          return new Promise((resolve) => this.options.set.set(val, (result) => resolve(result)));
        };
      } else {
        this.set = this.options.set.set;
      }
    }
    if (this.options.fromDynamo) {
      if (typeof this.options.fromDynamo === 'function') {
        this.parseDynamoCustom = this.options.fromDynamo;
      } else if (this.options.fromDynamo.isAsync) {
        this.parseDynamoCustom = function (val) {
          return new Promise((resolve) => this.options.fromDynamo.fromDynamo(val, (result) => resolve(result)));
        };
      } else {
        this.parseDynamoCustom = this.options.fromDynamo.fromDynamo;
      }
    }
    if (this.options.toDynamo) {
      if (typeof this.options.toDynamo === 'function') {
        this.toDynamoCustom = this.options.toDynamo;
      } else if (this.options.toDynamo.isAsync) {
        this.toDynamoCustom = function (val) {
          return new Promise((resolve) => this.options.toDynamo.toDynamo(val, (result) => resolve(result)));
        };
      } else {
        this.toDynamoCustom = this.options.toDynamo.toDynamo;
      }
    }
    if (this.options.get) {
      if (typeof this.options.get === 'function') {
        this.get = this.options.get;
      } else if (this.options.get.isAsync) {
        this.get = function (val) {
          return new Promise((resolve) => this.options.get.get(val, (result) => resolve(result)));
        };
      } else {
        this.get = this.options.get.get;
      }
    }

    this.applyValidation(this.options.validate);

    this.applyIndexes(this.options.index);
  }
}

function datifyMS (v) {
  if (!v.getTime) {
    v = new Date(v);
  }
  return JSON.stringify(v.getTime());
}

Attribute.prototype.types = {
  'string': {
    'name': 'string',
    'dynamo': 'S'
  },
  'number': {
    'name': 'number',
    'dynamo': 'N'
  },
  'boolean': {
    'name': 'boolean',
    'dynamo': 'S',
    'dynamofy': JSON.stringify
  },
  'nativeBoolean': {
    'name': 'boolean',
    'dynamo': 'BOOL'
  },
  'date': {
    'name': 'date',
    'dynamo': 'N',
    'dynamofy': datifyMS
  },
  'object': {
    'name': 'object',
    'dynamo': 'S',
    'dynamofy': JSON.stringify
  },
  'array': {
    'name': 'array',
    'dynamo': 'S',
    'dynamofy': JSON.stringify
  },
  'map': {
    'name': 'map',
    'dynamo': 'M',
    'dynamofy': JSON.stringify
  },
  'list': {
    'name': 'list',
    'dynamo': 'L',
    'dynamofy': JSON.stringify
  },
  'buffer': {
    'name': 'buffer',
    'dynamo': 'B'
  }
};

Attribute.prototype.setTypeFromRawValue = function (value) {
  // no type defined - assume this is not a type definition and we must grab type directly from value
  let type;
  let typeVal = value;
  if (value.type) {
    typeVal = value.type;
  }

  if (Array.isArray(typeVal) || typeVal === 'list') {
    type = 'List';
  } else if (Array.isArray(typeVal) && typeVal.length === 1 || typeof typeVal === 'function') {
    this.isSet = Array.isArray(typeVal);
    const regexFuncName = /^Function ([^(]+)\(/iu;
    const found = typeVal.toString().match(regexFuncName);
    [, type] = found;
    if (type === 'Object') {
      type = 'Map';
    }
  } else if (typeof typeVal === 'object' || typeVal === 'map') {
    type = 'Map';
  } else {
    type = typeof typeVal;
  }

  if (!type) {
    throw new errors.SchemaError(`Invalid attribute type: ${type}`);
  }

  type = type.toLowerCase();

  this.type = this.types[type];

  if (!this.type) {
    throw new errors.SchemaError(`Invalid attribute type: ${type}`);
  }

};

Attribute.prototype.setType = function (value) {
  if (!value) {
    throw new errors.SchemaError(`Invalid attribute value: ${value}`);
  }

  let type;
  let typeVal = value;
  if (value.type) {
    typeVal = value.type;
  }

  if (Array.isArray(typeVal) && typeVal.length === 1 && typeof typeVal[0] === 'object') {
    type = 'List';
  } else if (Array.isArray(typeVal) && typeVal.length === 1 || typeof typeVal === 'function') {
    this.isSet = Array.isArray(typeVal);
    const regexFuncName = /^Function ([^(]+)\(/iu;
    const found = typeVal.toString().match(regexFuncName);
    [, type] = found;
  } else if (typeof typeVal === 'object') {
    type = 'Map';
  } else if (typeof typeVal === 'string') {
    type = typeVal;
  }

  if (!type) {
    throw new errors.SchemaError(`Invalid attribute type: ${type}`);
  }

  type = type.toLowerCase();

  this.type = this.types[type];

  if (!this.type) {
    throw new errors.SchemaError(`Invalid attribute type: ${type}`);
  }
};

Attribute.prototype.applyDefault = function (dflt) {
  if (dflt === null || dflt === undefined) {
    delete this.default;
  } else if (typeof dflt === 'function') {
    this.default = dflt;
  } else {
    this.default = function () {
      return dflt;
    };
  }
};

Attribute.prototype.applyValidation = function (validator) {
  if (validator === null || validator === undefined) {
    delete this.validator;
  } else if (typeof validator === 'function') {
    this.validator = validator;
  } else if (validator.constructor.name === 'RegExp') {
    this.validator = function (val) {
      return validator.test(val);
    };
  } else if (typeof validator === 'object') {
    const method = validator.validator || validator.validate;
    if (validator.isAsync) {
      this.validator = function (val, model) {
        return new Promise((resolve) => {
          const argsArray = [val, model];
          if (validator.disableModelParameter) {
            argsArray.pop();
          }
          method(...argsArray, (success) => {
            resolve(success);
          });
        });
      };
    } else {
      this.validator = method;
    }
  } else {
    this.validator = function (val) {
      return validator === val;
    };
  }
};

Attribute.prototype.applyIndexes = function (indexes) {
  if (indexes === null || indexes === undefined) {
    delete this.indexes;
    return;
  }

  const that = this;
  that.indexes = {};

  function applyIndex (i) {
    if (typeof i !== 'object') {
      i = {};
    }

    const index = {};

    if (i.global) {
      index.global = true;

      if (i.rangeKey) {
        index.rangeKey = i.rangeKey;
      }

      if (i.throughput) {
        let {throughput} = i;
        if (typeof throughput === 'number') {
          throughput = {'read': throughput, 'write': throughput};
        }
        index.throughput = throughput;
        if ((!index.throughput.read || !index.throughput.write) && index.throughput.read >= 1 && index.throughput.write >= 1) {
          throw new errors.SchemaError(`Invalid Index throughput: ${index.throughput}`);
        }
      } else {
        index.throughput = that.schema.throughput;
      }
    }

    if (i.name) {
      index.name = i.name;
    } else {
      index.name = that.name + (i.global ? 'GlobalIndex' : 'LocalIndex');
    }

    if (i.project !== null && i.project !== undefined) {
      index.project = i.project;
    } else {
      index.project = true;
    }

    if (that.indexes[index.name]) {
      throw new errors.SchemaError(`Duplicate index names: ${index.name}`);
    }
    that.indexes[index.name] = index;
  }

  if (Array.isArray(indexes)) {
    indexes.map(applyIndex);
  } else {
    applyIndex(indexes);
  }
};

Attribute.prototype.setDefault = async function (model) {
  if (model === undefined || model === null) { return; }

  const val = model[this.name];
  if ((val === null || val === undefined || val === '' || this.options.forceDefault) && this.default) {
    model[this.name] = await this.default(model);
    debug('Defaulted %s to %s', this.name, model[this.name]);
  }
};

Attribute.prototype.toDynamo = async function (val, noSet, model, options) {
  if (this.toDynamoCustom) {
    return this.toDynamoCustom(val, noSet, model, options);
  }

  if (val === null || val === undefined || val === '') {
    if (this.required) {
      throw new errors.ValidationError(`Required value missing: ${this.name}`);
    }
    return null;
  }

  if (!noSet && this.isSet) {
    if (!Array.isArray(val)) {
      throw new errors.ValidationError(`Values must be array: ${this.name}`);
    }
    if (val.length === 0) {
      return null;
    }
  }

  if (this.validator && !await this.validator(val, model)) {
    throw new errors.ValidationError(`Validation failed: ${this.name}`);
  }

  // Check to see if attribute is a timestamp
  let runSet = true;
  let isExpires = false;
  let isTimestamp = false;

  if (model && model.$__ && model.$__.schema && model.$__.schema.timestamps && (model.$__.schema.timestamps.createdAt === this.name || model.$__.schema.timestamps.updatedAt === this.name)) {
    isTimestamp = true;
  }

  if (model && model.$__ && model.$__.schema && model.$__.schema.expires && model.$__.schema.expires.attribute === this.name) {
    isExpires = true;
  }

  if (isTimestamp && options.updateTimestamps === false) {
    runSet = false;
  }

  if (isExpires && options.updateExpires === true) {
    val = await this.default();
    runSet = true;
  }

  if (this.set && runSet) {
    val = await this.set(val);
  }

  const {type} = this;

  const isSet = this.isSet && !noSet;
  const dynamoObj = {};

  if (isSet) {
    dynamoObj[`${type.dynamo}S`] = val.map((v) => {
      if (type.dynamofy) {
        return type.dynamofy(v);
      }
      v = v.toString();
      if (type.dynamo === 'S') {
        if (this.options.trim) {
          v = v.trim();
        }
        if (this.options.lowercase) {
          v = v.toLowerCase();
        }
        if (this.options.uppercase) {
          v = v.toUpperCase();
        }
      }

      return v;
    });
  } else if (type.name === 'map') {
    const dynamoMapObj = {};
    for (const [name, value] of Object.entries(val)) {
      let subAttr = this.attributes[name];
      // if saveUnknown is activated the input has an unknown attribute, let's create one on the fly.
      if (!subAttr && (this.schema.options.saveUnknown || Array.isArray(this.options.saveUnknown) && this.options.saveUnknown.indexOf(name) >= 0)) {
        subAttr = module.exports.create(this.schema, name, value);
        this.attributes[name] = subAttr;
      }
      if (subAttr) {
        const attrVal = await subAttr.toDynamo(value);
        if (attrVal !== undefined && attrVal !== null) {
          dynamoMapObj[name] = attrVal;
        }
      }
    }
    dynamoObj.M = dynamoMapObj;
  } else if (type.name === 'list') {

    if (!Array.isArray(val)) {
      throw new errors.ValidationError(`Values must be array in a 'list': ${this.name}`);
    }

    const dynamoList = [];

    for (let i = 0; i < val.length; i += 1) {
      const item = val[i];

      // TODO currently only supports one attribute type
      /* eslint-disable-next-line prefer-destructuring */
      const objAttr = this.attributes[0];
      if (objAttr) {
        await objAttr.setDefault(model);
        dynamoList.push(await objAttr.toDynamo(item, undefined, model));
      }
    }
    dynamoObj.L = dynamoList;

  } else {

    if (type.dynamofy) {
      val = type.dynamofy(val);
    }

    if (type.dynamo !== 'BOOL' && (type.dynamo !== 'B' || !(val instanceof Buffer))) {
      val = val.toString();
    }

    if (type.dynamo === 'S') {
      if (this.options.enum) {
        if (this.options.enum.indexOf(val) === -1) {
          throw new errors.ValidationError(`Value must be one of : ${JSON.stringify(this.options.enum)}`);
        }
      }
      if (this.options.trim) {
        val = val.trim();
      }
      if (this.options.lowercase) {
        val = val.toLowerCase();
      }
      if (this.options.uppercase) {
        val = val.toUpperCase();
      }
    }
    dynamoObj[type.dynamo] = val;
  }

  debug('toDynamo %j', dynamoObj);

  return dynamoObj;
};

/*
* Converts DynamoDB document types (Map and List) to dynamoose
* attribute definition map and ist types
*
* For example, DynamoDB value:
* {
*   M: {
*     subAttr1: { S: '' },
*     subAttr2: { N: '' },
*   }
* }
*
* to
* {
*   type: 'map',
*   map: {
*     subAttr1: { type: String },
*     subAttr1: { type: Number },
*   },
* }
*/
function createAttrDefFromDynamo (dynamoAttribute) {
  let dynamoType;
  const attrDef = {
    'type': module.exports.lookupType(dynamoAttribute)
  };
  if (attrDef.type === Object) {
    attrDef.type = 'map';
    for (dynamoType in dynamoAttribute) {
      attrDef.map = {};
      for (const subAttrName in dynamoAttribute[dynamoType]) {
        attrDef.map[subAttrName] = createAttrDefFromDynamo(dynamoAttribute[dynamoType][subAttrName]);
      }
    }
  } else if (attrDef.type === Array) {
    attrDef.type = 'list';
    for (dynamoType in dynamoAttribute) {
      attrDef.list = dynamoAttribute[dynamoType].map(createAttrDefFromDynamo);
    }
  }
  return attrDef;
}

const createUnknownAttributeFromDynamo = function (schema, name, dynamoAttribute) {
  debug('createUnknownAttributeFromDynamo: %j : "%s" : %j', schema, name, dynamoAttribute);
  const attrDef = createAttrDefFromDynamo(dynamoAttribute);
  const attr = new Attribute(schema, name, attrDef);
  return attr;
};
module.exports.createUnknownAttributeFromDynamo = createUnknownAttributeFromDynamo;

Attribute.prototype.parseDynamo = async function (json) {
  if (this.parseDynamoCustom) {
    return this.parseDynamoCustom(json);
  }

  function dedynamofy (type, isSet, transform, attr) {
    const errorHandlingTransform = transform && async function (v) {
      try {
        return await transform(v, attr);
      } catch (e) {
        e.message += `\nAttribute "${attr.name}" of type "${type}" has an invalid value of ${JSON.stringify(json)}`;
        throw new errors.ParseError(e.message);
      }
    };

    if (!json) {
      return;
    }
    if (isSet) {
      const set = json[`${type}S`];
      return Promise.all(set.map((v) => {
        if (errorHandlingTransform) {
          return errorHandlingTransform(v);
        }
        return v;
      }));
    }
    const val = json[type];
    if (errorHandlingTransform) {
      return errorHandlingTransform(val === undefined ? json : val);
    }
    return val;
  }

  async function mapify (v, attr) {
    if (!v) { return; }
    const val = {};

    const {attributes, schema} = attr;
    // loop over all the properties of the input
    for (const [name, value] of Object.entries(v)) {
      let subAttr = attributes[name];
      // if saveUnknown is activated the input has an unknown attribute, let's create one on the fly.
      if (!subAttr && schema.options.errorUnknown) {
        throw new errors.ParseError(`Unknown nested attribute ${name} with value: ${JSON.stringify(value)}`);
      }
      if (!subAttr && (schema.options.saveUnknown || Array.isArray(schema.options.saveUnknown) && schema.options.saveUnknown.indexOf(name) >= 0)) {
        subAttr = createUnknownAttributeFromDynamo(schema, name, value);
        attr.attributes[name] = subAttr;
      }
      if (subAttr) {
        const attrVal = await subAttr.parseDynamo(value);
        if (attrVal !== undefined && attrVal !== null) {
          val[name] = attrVal;
        }
      }
    }
    return val;
  }

  async function listify (v, attr) {
    if (!v) { return; }
    const val = [];
    debug('parsing list');

    if (Array.isArray(v)) {
      for (let i = 0; i < v.length; i += 1) {
        // TODO assume only one attribute type allowed for a list
        /* eslint-disable-next-line prefer-destructuring */
        const attrType = attr.attributes[0];
        const attrVal = await attrType.parseDynamo(v[i]);
        if (attrVal !== undefined && attrVal !== null) {
          val.push(attrVal);
        }
      }
    }
    return val;
  }

  function datify (v) {
    debug('parsing date from %s', v);
    return new Date(parseInt(v, 10));
  }
  function bufferify (v) {
    return Buffer.from(v);
  }
  function stringify (v) {
    if (typeof v !== 'string') {
      debug('******', v);
      return JSON.stringify(v);
    }
    return v;
  }

  let val;

  switch (this.type.name) {
  case 'string':
    val = await dedynamofy('S', this.isSet, stringify, this);
    break;
  case 'number':
    val = await dedynamofy('N', this.isSet, JSON.parse, this);
    break;
  case 'boolean':
    // 'S' is backwards compatible however 'BOOL' is a new valid argument
    val = await dedynamofy(this.type.dynamo, this.isSet, JSON.parse, this);
    break;
  case 'date':
    val = await dedynamofy('N', this.isSet, datify, this);
    break;
  case 'object':
    val = await dedynamofy('S', this.isSet, JSON.parse, this);
    break;
  case 'array':
    val = await dedynamofy('S', this.isSet, JSON.parse, this);
    break;
  case 'map':
    val = await dedynamofy('M', this.isSet, mapify, this);
    break;
  case 'list':
    val = await dedynamofy('L', this.isSet, listify, this);
    break;
  case 'buffer':
    val = await dedynamofy('B', this.isSet, bufferify, this);
    break;
  default:
    throw new errors.SchemaError(`Invalid attribute type: ${this.type}`);
  }

  if (this.get) {
    val = await this.get(val);
  }

  debug('parseDynamo: %s : "%s" : %j', this.name, this.type.name, val);

  return val;
};

module.exports.create = function (schema, name, obj) {
  debug('create: %j : "%s" : %j', schema, name, obj);

  const value = obj;
  let options = {};
  if (typeof obj === 'object' && obj.type) {
    options = obj;
  }

  const attr = new Attribute(schema, name, value);

  if (options.hashKey && options.rangeKey) {
    throw new errors.SchemaError(`Cannot be both hashKey and rangeKey: ${name}`);
  }

  if (options.hashKey || !schema.hashKey && !options.rangeKey) {
    schema.hashKey = attr;
  }

  if (options.rangeKey) {
    schema.rangeKey = attr;
  }

  // check for global attributes in the tree..
  if (attr.indexes) {
    for (const indexName in attr.indexes) {
      const index = attr.indexes[indexName];
      if (schema.indexes.global[indexName] || schema.indexes.local[indexName]) {
        throw new errors.SchemaError(`Duplicate index name: ${indexName}`);
      }
      if (index.global) {
        schema.indexes.global[indexName] = attr;
      } else {
        schema.indexes.local[indexName] = attr;
      }
    }
  }

  return attr;
};

module.exports.lookupType = function (dynamoObj) {
  if (dynamoObj.S !== null && dynamoObj.S !== undefined) {
    // try {
    //   JSON.parse(dynamoObj.S);
    //   return Object;
    // } catch (err) {
    return String;
    // }
  }
  if (dynamoObj.L !== null && dynamoObj.L !== undefined) {
    return Array;
  }
  if (dynamoObj.M !== null && dynamoObj.M !== undefined) {
    return Object;
  }
  if (dynamoObj.N !== null && dynamoObj.N !== undefined) {
    return Number;
  }
  if (dynamoObj.BOOL !== null && dynamoObj.BOOL !== undefined) {
    return Boolean;
  }
  if (dynamoObj.B !== null && dynamoObj.B !== undefined) {
    return Buffer;
  }
  if (dynamoObj.NS !== null && dynamoObj.NS !== undefined) {
    return [Number];
  }
};


/**
 * Copyright (c) 2016 Hideki Shiro
 */

import * as opentype from 'opentype.js';

const DEFAULT_FONT = require('path').join(__dirname, '../fonts/ipag.ttf');

// Private method

function parseAnchorOption(anchor) {
  let horizontal = anchor.match(/left|center|right/gi) || [];
  horizontal = horizontal.length === 0 ? 'left' : horizontal[0];

  let vertical = anchor.match(/baseline|top|bottom|middle/gi) || [];
  vertical = vertical.length === 0 ? 'baseline' : vertical[0];

  return { horizontal, vertical };
}

export default class TextToSVG {
  constructor(font) {
    this.font = font;
  }

  static loadSync(file = DEFAULT_FONT) {
    return new TextToSVG(opentype.loadSync(file));
  }

  static load(url, cb) {
    opentype.load(url, (err, font) => {
      if (err !== null) {
        return cb(err, null);
      }

      return cb(null, new TextToSVG(font));
    });
  }

  getWidth(text, options) {
    const fontSize = options.fontSize || 72;
    const kerning = 'kerning' in options ? options.kerning : true;
    const fontScale = 1 / this.font.unitsPerEm * fontSize;

    let width = 0;
    const glyphs = this.font.stringToGlyphs(text);
    for (let i = 0; i < glyphs.length; i++) {
      const glyph = glyphs[i];

      if (glyph.advanceWidth) {
        width += glyph.advanceWidth * fontScale;
      }

      if (kerning && i < glyphs.length - 1) {
        const kerningValue = this.font.getKerningValue(glyph, glyphs[i + 1]);
        width += kerningValue * fontScale;
      }

      if (options.letterSpacing) {
        width += options.letterSpacing * fontSize;
      } else if (options.tracking) {
        width += (options.tracking / 1000) * fontSize;
      }
    }
    return width;
  }

  getHeight(fontSize) {
    const fontScale = 1 / this.font.unitsPerEm * fontSize;
    return (this.font.ascender - this.font.descender) * fontScale;
  }

  getMetrics(text, options = {}) {
    const fontSize = options.fontSize || 72;
    const anchor = parseAnchorOption(options.anchor || '');

    const width = this.getWidth(text, options);
    const height = this.getHeight(fontSize);

    const fontScale = 1 / this.font.unitsPerEm * fontSize;
    const ascender = this.font.ascender * fontScale;
    const descender = this.font.descender * fontScale;

    let x = options.x || 0;
    switch (anchor.horizontal) {
      case 'left':
        x -= 0;
        break;
      case 'center':
        x -= width / 2;
        break;
      case 'right':
        x -= width;
        break;
      default:
        throw new Error(`Unknown anchor option: ${anchor.horizontal}`);
    }

    let y = options.y || 0;
    switch (anchor.vertical) {
      case 'baseline':
        y -= ascender;
        break;
      case 'top':
        y -= 0;
        break;
      case 'middle':
        y -= height / 2;
        break;
      case 'bottom':
        y -= height;
        break;
      default:
        throw new Error(`Unknown anchor option: ${anchor.vertical}`);
    }

    const baseline = y + ascender;

    return {
      x,
      y,
      baseline,
      width,
      height,
      ascender,
      descender,
    };
  }

  getD(text, options = {}) {
    const fontSize = options.fontSize || 72;
    const kerning = 'kerning' in options ? options.kerning : true;
    const letterSpacing = 'letterSpacing' in options ? options.letterSpacing : false;
    const tracking = 'tracking' in options ? options.tracking : false;
    const metrics = this.getMetrics(text, options);
    const path = this.font.getPath(text, metrics.x, metrics.baseline, fontSize, { kerning, letterSpacing, tracking });

    return path.toPathData();
  }

  getPath(text, options = {}) {
    const attributes = Object.keys(options.attributes || {})
      .map(key => `${key}="${options.attributes[key]}"`)
      .join(' ');
    const d = this.getD(text, options);

    if (attributes) {
      return `<path ${attributes} d="${d}"/>`;
    }

    return `<path d="${d}"/>`;
  }

  getSVG(text, options = {}) {
    const metrics = this.getMetrics(text, options);
    let svg = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${metrics.width}" height="${metrics.height}">`;
    svg += this.getPath(text, options);
    svg += '</svg>';

    return svg;
  }

  getDebugSVG(text, options = {}) {
    options = JSON.parse(JSON.stringify(options));

    options.x = options.x || 0;
    options.y = options.y || 0;
    const metrics = this.getMetrics(text, options);
    const box = {
      width: Math.max(metrics.x + metrics.width, 0) - Math.min(metrics.x, 0),
      height: Math.max(metrics.y + metrics.height, 0) - Math.min(metrics.y, 0),
    };
    const origin = {
      x: box.width - Math.max(metrics.x + metrics.width, 0),
      y: box.height - Math.max(metrics.y + metrics.height, 0),
    };

    // Shift text based on origin
    options.x += origin.x;
    options.y += origin.y;

    let svg = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${box.width}" height="${box.height}">`;
    svg += `<path fill="none" stroke="red" stroke-width="1" d="M0,${origin.y}L${box.width},${origin.y}"/>`; // X Axis
    svg += `<path fill="none" stroke="red" stroke-width="1" d="M${origin.x},0L${origin.x},${box.height}"/>`; // Y Axis
    svg += this.getPath(text, options);
    svg += '</svg>';

    return svg;
  }
}

module.exports = exports.default;