| // | 
| //                              list | 
| //                            ┌──────┐ | 
| //             ┌──────────────┼─head │ | 
| //             │              │ tail─┼──────────────┐ | 
| //             │              └──────┘              │ | 
| //             ▼                                    ▼ | 
| //            item        item        item        item | 
| //          ┌──────┐    ┌──────┐    ┌──────┐    ┌──────┐ | 
| //  null ◀──┼─prev │◀───┼─prev │◀───┼─prev │◀───┼─prev │ | 
| //          │ next─┼───▶│ next─┼───▶│ next─┼───▶│ next─┼──▶ null | 
| //          ├──────┤    ├──────┤    ├──────┤    ├──────┤ | 
| //          │ data │    │ data │    │ data │    │ data │ | 
| //          └──────┘    └──────┘    └──────┘    └──────┘ | 
| // | 
|   | 
| function createItem(data) { | 
|     return { | 
|         prev: null, | 
|         next: null, | 
|         data: data | 
|     }; | 
| } | 
|   | 
| function allocateCursor(node, prev, next) { | 
|     var cursor; | 
|   | 
|     if (cursors !== null) { | 
|         cursor = cursors; | 
|         cursors = cursors.cursor; | 
|         cursor.prev = prev; | 
|         cursor.next = next; | 
|         cursor.cursor = node.cursor; | 
|     } else { | 
|         cursor = { | 
|             prev: prev, | 
|             next: next, | 
|             cursor: node.cursor | 
|         }; | 
|     } | 
|   | 
|     node.cursor = cursor; | 
|   | 
|     return cursor; | 
| } | 
|   | 
| function releaseCursor(node) { | 
|     var cursor = node.cursor; | 
|   | 
|     node.cursor = cursor.cursor; | 
|     cursor.prev = null; | 
|     cursor.next = null; | 
|     cursor.cursor = cursors; | 
|     cursors = cursor; | 
| } | 
|   | 
| var cursors = null; | 
| var List = function() { | 
|     this.cursor = null; | 
|     this.head = null; | 
|     this.tail = null; | 
| }; | 
|   | 
| List.createItem = createItem; | 
| List.prototype.createItem = createItem; | 
|   | 
| List.prototype.updateCursors = function(prevOld, prevNew, nextOld, nextNew) { | 
|     var cursor = this.cursor; | 
|   | 
|     while (cursor !== null) { | 
|         if (cursor.prev === prevOld) { | 
|             cursor.prev = prevNew; | 
|         } | 
|   | 
|         if (cursor.next === nextOld) { | 
|             cursor.next = nextNew; | 
|         } | 
|   | 
|         cursor = cursor.cursor; | 
|     } | 
| }; | 
|   | 
| List.prototype.getSize = function() { | 
|     var size = 0; | 
|     var cursor = this.head; | 
|   | 
|     while (cursor) { | 
|         size++; | 
|         cursor = cursor.next; | 
|     } | 
|   | 
|     return size; | 
| }; | 
|   | 
| List.prototype.fromArray = function(array) { | 
|     var cursor = null; | 
|   | 
|     this.head = null; | 
|   | 
|     for (var i = 0; i < array.length; i++) { | 
|         var item = createItem(array[i]); | 
|   | 
|         if (cursor !== null) { | 
|             cursor.next = item; | 
|         } else { | 
|             this.head = item; | 
|         } | 
|   | 
|         item.prev = cursor; | 
|         cursor = item; | 
|     } | 
|   | 
|     this.tail = cursor; | 
|   | 
|     return this; | 
| }; | 
|   | 
| List.prototype.toArray = function() { | 
|     var cursor = this.head; | 
|     var result = []; | 
|   | 
|     while (cursor) { | 
|         result.push(cursor.data); | 
|         cursor = cursor.next; | 
|     } | 
|   | 
|     return result; | 
| }; | 
|   | 
| List.prototype.toJSON = List.prototype.toArray; | 
|   | 
| List.prototype.isEmpty = function() { | 
|     return this.head === null; | 
| }; | 
|   | 
| List.prototype.first = function() { | 
|     return this.head && this.head.data; | 
| }; | 
|   | 
| List.prototype.last = function() { | 
|     return this.tail && this.tail.data; | 
| }; | 
|   | 
| List.prototype.each = function(fn, context) { | 
|     var item; | 
|   | 
|     if (context === undefined) { | 
|         context = this; | 
|     } | 
|   | 
|     // push cursor | 
|     var cursor = allocateCursor(this, null, this.head); | 
|   | 
|     while (cursor.next !== null) { | 
|         item = cursor.next; | 
|         cursor.next = item.next; | 
|   | 
|         fn.call(context, item.data, item, this); | 
|     } | 
|   | 
|     // pop cursor | 
|     releaseCursor(this); | 
| }; | 
|   | 
| List.prototype.forEach = List.prototype.each; | 
|   | 
| List.prototype.eachRight = function(fn, context) { | 
|     var item; | 
|   | 
|     if (context === undefined) { | 
|         context = this; | 
|     } | 
|   | 
|     // push cursor | 
|     var cursor = allocateCursor(this, this.tail, null); | 
|   | 
|     while (cursor.prev !== null) { | 
|         item = cursor.prev; | 
|         cursor.prev = item.prev; | 
|   | 
|         fn.call(context, item.data, item, this); | 
|     } | 
|   | 
|     // pop cursor | 
|     releaseCursor(this); | 
| }; | 
|   | 
| List.prototype.forEachRight = List.prototype.eachRight; | 
|   | 
| List.prototype.reduce = function(fn, initialValue, context) { | 
|     var item; | 
|   | 
|     if (context === undefined) { | 
|         context = this; | 
|     } | 
|   | 
|     // push cursor | 
|     var cursor = allocateCursor(this, null, this.head); | 
|     var acc = initialValue; | 
|   | 
|     while (cursor.next !== null) { | 
|         item = cursor.next; | 
|         cursor.next = item.next; | 
|   | 
|         acc = fn.call(context, acc, item.data, item, this); | 
|     } | 
|   | 
|     // pop cursor | 
|     releaseCursor(this); | 
|   | 
|     return acc; | 
| }; | 
|   | 
| List.prototype.reduceRight = function(fn, initialValue, context) { | 
|     var item; | 
|   | 
|     if (context === undefined) { | 
|         context = this; | 
|     } | 
|   | 
|     // push cursor | 
|     var cursor = allocateCursor(this, this.tail, null); | 
|     var acc = initialValue; | 
|   | 
|     while (cursor.prev !== null) { | 
|         item = cursor.prev; | 
|         cursor.prev = item.prev; | 
|   | 
|         acc = fn.call(context, acc, item.data, item, this); | 
|     } | 
|   | 
|     // pop cursor | 
|     releaseCursor(this); | 
|   | 
|     return acc; | 
| }; | 
|   | 
| List.prototype.nextUntil = function(start, fn, context) { | 
|     if (start === null) { | 
|         return; | 
|     } | 
|   | 
|     var item; | 
|   | 
|     if (context === undefined) { | 
|         context = this; | 
|     } | 
|   | 
|     // push cursor | 
|     var cursor = allocateCursor(this, null, start); | 
|   | 
|     while (cursor.next !== null) { | 
|         item = cursor.next; | 
|         cursor.next = item.next; | 
|   | 
|         if (fn.call(context, item.data, item, this)) { | 
|             break; | 
|         } | 
|     } | 
|   | 
|     // pop cursor | 
|     releaseCursor(this); | 
| }; | 
|   | 
| List.prototype.prevUntil = function(start, fn, context) { | 
|     if (start === null) { | 
|         return; | 
|     } | 
|   | 
|     var item; | 
|   | 
|     if (context === undefined) { | 
|         context = this; | 
|     } | 
|   | 
|     // push cursor | 
|     var cursor = allocateCursor(this, start, null); | 
|   | 
|     while (cursor.prev !== null) { | 
|         item = cursor.prev; | 
|         cursor.prev = item.prev; | 
|   | 
|         if (fn.call(context, item.data, item, this)) { | 
|             break; | 
|         } | 
|     } | 
|   | 
|     // pop cursor | 
|     releaseCursor(this); | 
| }; | 
|   | 
| List.prototype.some = function(fn, context) { | 
|     var cursor = this.head; | 
|   | 
|     if (context === undefined) { | 
|         context = this; | 
|     } | 
|   | 
|     while (cursor !== null) { | 
|         if (fn.call(context, cursor.data, cursor, this)) { | 
|             return true; | 
|         } | 
|   | 
|         cursor = cursor.next; | 
|     } | 
|   | 
|     return false; | 
| }; | 
|   | 
| List.prototype.map = function(fn, context) { | 
|     var result = new List(); | 
|     var cursor = this.head; | 
|   | 
|     if (context === undefined) { | 
|         context = this; | 
|     } | 
|   | 
|     while (cursor !== null) { | 
|         result.appendData(fn.call(context, cursor.data, cursor, this)); | 
|         cursor = cursor.next; | 
|     } | 
|   | 
|     return result; | 
| }; | 
|   | 
| List.prototype.filter = function(fn, context) { | 
|     var result = new List(); | 
|     var cursor = this.head; | 
|   | 
|     if (context === undefined) { | 
|         context = this; | 
|     } | 
|   | 
|     while (cursor !== null) { | 
|         if (fn.call(context, cursor.data, cursor, this)) { | 
|             result.appendData(cursor.data); | 
|         } | 
|         cursor = cursor.next; | 
|     } | 
|   | 
|     return result; | 
| }; | 
|   | 
| List.prototype.clear = function() { | 
|     this.head = null; | 
|     this.tail = null; | 
| }; | 
|   | 
| List.prototype.copy = function() { | 
|     var result = new List(); | 
|     var cursor = this.head; | 
|   | 
|     while (cursor !== null) { | 
|         result.insert(createItem(cursor.data)); | 
|         cursor = cursor.next; | 
|     } | 
|   | 
|     return result; | 
| }; | 
|   | 
| List.prototype.prepend = function(item) { | 
|     //      head | 
|     //    ^ | 
|     // item | 
|     this.updateCursors(null, item, this.head, item); | 
|   | 
|     // insert to the beginning of the list | 
|     if (this.head !== null) { | 
|         // new item <- first item | 
|         this.head.prev = item; | 
|   | 
|         // new item -> first item | 
|         item.next = this.head; | 
|     } else { | 
|         // if list has no head, then it also has no tail | 
|         // in this case tail points to the new item | 
|         this.tail = item; | 
|     } | 
|   | 
|     // head always points to new item | 
|     this.head = item; | 
|   | 
|     return this; | 
| }; | 
|   | 
| List.prototype.prependData = function(data) { | 
|     return this.prepend(createItem(data)); | 
| }; | 
|   | 
| List.prototype.append = function(item) { | 
|     return this.insert(item); | 
| }; | 
|   | 
| List.prototype.appendData = function(data) { | 
|     return this.insert(createItem(data)); | 
| }; | 
|   | 
| List.prototype.insert = function(item, before) { | 
|     if (before !== undefined && before !== null) { | 
|         // prev   before | 
|         //      ^ | 
|         //     item | 
|         this.updateCursors(before.prev, item, before, item); | 
|   | 
|         if (before.prev === null) { | 
|             // insert to the beginning of list | 
|             if (this.head !== before) { | 
|                 throw new Error('before doesn\'t belong to list'); | 
|             } | 
|   | 
|             // since head points to before therefore list doesn't empty | 
|             // no need to check tail | 
|             this.head = item; | 
|             before.prev = item; | 
|             item.next = before; | 
|   | 
|             this.updateCursors(null, item); | 
|         } else { | 
|   | 
|             // insert between two items | 
|             before.prev.next = item; | 
|             item.prev = before.prev; | 
|   | 
|             before.prev = item; | 
|             item.next = before; | 
|         } | 
|     } else { | 
|         // tail | 
|         //      ^ | 
|         //      item | 
|         this.updateCursors(this.tail, item, null, item); | 
|   | 
|         // insert to the ending of the list | 
|         if (this.tail !== null) { | 
|             // last item -> new item | 
|             this.tail.next = item; | 
|   | 
|             // last item <- new item | 
|             item.prev = this.tail; | 
|         } else { | 
|             // if list has no tail, then it also has no head | 
|             // in this case head points to new item | 
|             this.head = item; | 
|         } | 
|   | 
|         // tail always points to new item | 
|         this.tail = item; | 
|     } | 
|   | 
|     return this; | 
| }; | 
|   | 
| List.prototype.insertData = function(data, before) { | 
|     return this.insert(createItem(data), before); | 
| }; | 
|   | 
| List.prototype.remove = function(item) { | 
|     //      item | 
|     //       ^ | 
|     // prev     next | 
|     this.updateCursors(item, item.prev, item, item.next); | 
|   | 
|     if (item.prev !== null) { | 
|         item.prev.next = item.next; | 
|     } else { | 
|         if (this.head !== item) { | 
|             throw new Error('item doesn\'t belong to list'); | 
|         } | 
|   | 
|         this.head = item.next; | 
|     } | 
|   | 
|     if (item.next !== null) { | 
|         item.next.prev = item.prev; | 
|     } else { | 
|         if (this.tail !== item) { | 
|             throw new Error('item doesn\'t belong to list'); | 
|         } | 
|   | 
|         this.tail = item.prev; | 
|     } | 
|   | 
|     item.prev = null; | 
|     item.next = null; | 
|   | 
|     return item; | 
| }; | 
|   | 
| List.prototype.push = function(data) { | 
|     this.insert(createItem(data)); | 
| }; | 
|   | 
| List.prototype.pop = function() { | 
|     if (this.tail !== null) { | 
|         return this.remove(this.tail); | 
|     } | 
| }; | 
|   | 
| List.prototype.unshift = function(data) { | 
|     this.prepend(createItem(data)); | 
| }; | 
|   | 
| List.prototype.shift = function() { | 
|     if (this.head !== null) { | 
|         return this.remove(this.head); | 
|     } | 
| }; | 
|   | 
| List.prototype.prependList = function(list) { | 
|     return this.insertList(list, this.head); | 
| }; | 
|   | 
| List.prototype.appendList = function(list) { | 
|     return this.insertList(list); | 
| }; | 
|   | 
| List.prototype.insertList = function(list, before) { | 
|     // ignore empty lists | 
|     if (list.head === null) { | 
|         return this; | 
|     } | 
|   | 
|     if (before !== undefined && before !== null) { | 
|         this.updateCursors(before.prev, list.tail, before, list.head); | 
|   | 
|         // insert in the middle of dist list | 
|         if (before.prev !== null) { | 
|             // before.prev <-> list.head | 
|             before.prev.next = list.head; | 
|             list.head.prev = before.prev; | 
|         } else { | 
|             this.head = list.head; | 
|         } | 
|   | 
|         before.prev = list.tail; | 
|         list.tail.next = before; | 
|     } else { | 
|         this.updateCursors(this.tail, list.tail, null, list.head); | 
|   | 
|         // insert to end of the list | 
|         if (this.tail !== null) { | 
|             // if destination list has a tail, then it also has a head, | 
|             // but head doesn't change | 
|   | 
|             // dest tail -> source head | 
|             this.tail.next = list.head; | 
|   | 
|             // dest tail <- source head | 
|             list.head.prev = this.tail; | 
|         } else { | 
|             // if list has no a tail, then it also has no a head | 
|             // in this case points head to new item | 
|             this.head = list.head; | 
|         } | 
|   | 
|         // tail always start point to new item | 
|         this.tail = list.tail; | 
|     } | 
|   | 
|     list.head = null; | 
|     list.tail = null; | 
|   | 
|     return this; | 
| }; | 
|   | 
| List.prototype.replace = function(oldItem, newItemOrList) { | 
|     if ('head' in newItemOrList) { | 
|         this.insertList(newItemOrList, oldItem); | 
|     } else { | 
|         this.insert(newItemOrList, oldItem); | 
|     } | 
|   | 
|     this.remove(oldItem); | 
| }; | 
|   | 
| module.exports = List; |