/*

Ruby.js by Florian Gross. Licensed under the Ruby license.
From: http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/117184

Usage:

		[1, 2, 3].each(function(item) {
		   alert(item * 2)
		})

		[1, 2, 3].map(function(item) {
		   // The return is needed. functions don't return their last expression
		   // by default.
		   return(item * 2)
		}).each(alert) // Functions can be supplied directly instead of blocks.

		1.succ()   // This doesn't work --
		1.0.succ() // use either this
		(1).succ() // or this

		Also note that you can't omit the parentheses of a method call in 
		JavaScript which is by design of course, but takes some getting used to.

		Some bigger differences that come to mind:

		Array#each always yields value, index. This is no problem in JavaScript, 
		because a function that takes one argument will not get an Array of 
		arguments when you give multiple arguments to it.

		There's no Enumerable#each_with_index because of the above.

		Hash#each yields value, key. This makes things much easier, because it 
		the key can be handled as an index. (So there's a symmetry between 
		Array#each and Hash#each)

		Ranges don't work as well as I'd wish them to right now. I wanted to 
		make it possible to also have reverse Ranges like 10 .. 4, but that 
		doesn't work too well with Strings.

		String#ljust doesn't work with filler Strings that are longer than one 
		character right now.

*/

Object.prototype.clone = function(deepClone) {
  var result = new this.constructor()
  for (var property in this) {
    if (deepClone && typeof(this[property]) == 'object') {
      result[property] = this[property].clone(deepClone)
    } else {
      result[property] = this[property]
    }
  }
  return(result)
}

/*
Object.prototype.extend = function(other) {
  if (!this.mixins) this.mixins = []
  this.mixins.push(other)
  for (var property in other)
    if (!this.hasOwnProperty(property))
      this[property] = other[property]
}
*/

Object.prototype.cmp = function(other) {
  if (this < other) return(-1)
  if (this > other) return(+1)
  return(0)
}

Object.prototype.valuesAt = function() {
  var obj = this
  return(arguments.toArray().map(function(index) {
    return(obj[index])
  }))
}

/*
Object.prototype.toArray = function() {
  if (!this.length) throw("Can't convert")
  var result = []
  for (var i = 0; i < this.length; i++)
    result.push(this[i])
  return(result)
}
*/

Object.prototype.hash = function() {
  return(this.toSource().hash())
}

Object.prototype.instanceOf = function(klass) {
  return(this.constructor == klass)
}

Object.prototype.isA = Object.prototype.kindOf = function(klass) {
  if (this.instanceOf(klass)) return(true)
  if (this["mixins"] != undefined && this.mixins.includes(klass))
    return(true)
  return(false)
}

Object.prototype.methods = function() {
  var result = []
  for (var property in this)
    if (typeof(this[property]) == "function")
      result.push(property)
  return(result)
}

Object.prototype.respondTo = function(method) {
  return(this.methods().includes(method))
}

/*
Object.prototype.send = function(method) {
  var rest = arguments.toArray().last(-1)
  if (!this.respondTo(method)) throw("undefined method")
  return(this[method].apply(this, rest))
}
*/

Object.prototype.instanceEval = function(code) {
  if (code.isA(Function))
    return(code.apply(this))
  else
    return(eval(code.toString()))
}

Number.prototype.times = function(block) {
  for (var i = 0; i < this; i++) block(i)
}

Number.prototype.upto = function(other, block) {
  for (var i = this; i <= other; i++) block(i)
}

Number.prototype.downto = function(other, block) {
  for (var i = this; i >= other; i--) block(i)
}

Number.prototype.towards = function(other, block) {
  var step = this.cmp(other)
  for (var i = this; i !== other - step; i -= step)
    block(i) 
}

Number.prototype.succ = function() { return(this + 1) }
Number.prototype.pred = function() { return(this - 1) }

Number.prototype.chr = function() { return(String.fromCharCode(this)) }

enumerable = new Object()
enumerable.eachWindow = function(window, block) {
  if (!window.isA(Range)) window = range(0, window)
  elements = [], pushed = 0
  this.each(function(item, index) {
    elements.push(item)
    pushed += 1
    if (pushed % window.rend == 0) {
      start = [0, window.start - window.rend + pushed].max()
      end = [0, window.rend + pushed].max()
      block(elements.fetch(xrange(start, end)), index)
    }
  })
}

enumerable.collect = enumerable.map = function(block) {
  var result = []
  this.each(function(item, index) {
    result.push(block(item, index))
  })
  return(result)
}

enumerable.toArray = enumerable.entries = function() {
  return(this.map(function(item) { return(item) }))
}

enumerable.inject = function(firstArg) {
  var state, block, first = true
  if (arguments.length == 1) {
    block = firstArg
  } else {
    state = firstArg
    block = arguments[1]
  }
  this.each(function(item, index) {
    if (first && typeof(state) == "undefined")
      state = item, first = false
    else
      state = block(state, item, index)
  })
  return(state)
}

enumerable.find = enumerable.detect = function(block) {
  var result, done
  this.each(function(item, index) {
    if (!done && block(item, index)) {
      result = item
      done = true
    }
  })
  return(result)
}

enumerable.findAll = enumerable.select = function(block) {
  return(this.inject([], function(result, item, index) {
    return(block(item, index) ? result.add(item) : result)
  }))
}

enumerable.grep = function(obj) {
  return(this.findAll(function(item) {
    return(obj.test(item))
  }))
}

enumerable.reject = function(block) {
  return(this.select(function(item, index) {
    return(!block(item, index))
  }))
}

enumerable.compact = function() {
  return(this.select(function(item) {
    return(typeof(item) != "undefined")
  }))
}

enumerable.nitems = function() { return(this.compact().length) }

enumerable.sortBy = function(block) {
  return(this.map(function(item, index) {
    return([block(item, index), item])
  }).sort(function(a, b) {
    return(a[0].cmp(b[0]))
  }).map(function(item) {
    return(item[1])
  }))
}

enumerable.all = function(block) {
  return(this.findAll(block).length == this.length)
}

enumerable.any = function(block) {
  return(typeof(this.find(block)) != "undefined")
}

enumerable.includes = function(obj) {
  return(this.any(function(item) {
    return(item === obj)
  }))
}

enumerable.index = function(obj) {
  var result
  this.find(function(item, index) {
    if (obj == item) {
      result = index
      return(true)
    } else {
      return(false)
    }
  })
  return(result)
}

enumerable.uniq = function() {
  return(this.inject([], function(result, item) {
    return(result.includes(item) ? result : result.add(item))
  }))
}

enumerable.max = function(block) {
  if (!block) block = function(a, b) { return(a.cmp(b)) }
  return(this.sort(block).last())
}

enumerable.min = function(block) {
  if (!block) block = function(a, b) { return(a.cmp(b)) }
  return(this.sort(block).first())
}

enumerable.partition = function(block) {
  var positives = [], negatives = []
  this.each(function(item, index) {
    if (block(item, index))
      positives.push(item)
    else
      negatives.push(item)
  })
  return([positives, negatives])
}

enumerable.zip = function() {
  var ary = arguments.toArray()
  ary.unshift(this)
  return(ary.transpose())
}

enumerable.flatten = function(depth) {
  if (depth == undefined) depth = -1
  if (!depth) return(this)
  return(this.inject([], function(result, item) {
    var flatItem = item.respondTo("flatten") ? item.flatten(depth - 1) : [item]
    return(result.merge(flatItem))
  }))
}

/*
Array.fromObject = function(obj) {
  if (!obj.length) throw("Can't convert")
  var result = []
  for (var i = 0; i < obj.length; i++)
    result.push(obj[i])
  return(result)
}
*/

/*
Array.prototype.transpose = function() {
  var result, length = -1
  this.each(function(item, index) {
    if (length < 0) {
      length = item.length
      result = Array.withLength(length, function() {
        return(new Array(this.length))
      })
    } else if (length != item.length) {
      throw("Element sizes differ")
    }
    item.each(function(iitem, iindex) {
      result[iindex][index] = iitem
    })
  })
  return(result)
}
*/

Array.withLength = function(length, fallback) {
  var result = [null].mul(length)
  result.fill(fallback)
  return(result)
}

Array.prototype.each = function(block) {
  for (var index = 0; index < this.length; ++index) {
    var item = this[index]
    block(item, index)
  }
  return(this)
}
Array.prototype.extend(enumerable)

Array.prototype.isEmpty = function() { return(this.length == 0) }

Array.prototype.at = Array.prototype.fetch = function(index, length) {
  if (index.isA(Range)) {
    var end = index.rend + (index.rend < 0 ? this.length : 0)
    index = index.start
    length = end - index + 1
  }
  if (length == undefined) length = 1
  if (index < 0) index += this.length
  var result = this.slice(index, index + length)
  return(result.length == 1 ? result[0] : result)
}

Array.prototype.first = function(amount) {
  if (amount == undefined) amount = 1
  return(this.at(xrange(0, amount)))
}

Array.prototype.last = function(amount) {
  if (amount == undefined) amount = 1
  return(this.at(range(-amount, -1)))
}

Array.prototype.store = function(index) {
  var length = 1, obj
  arguments = arguments.toArray()
  arguments.shift()
  if (arguments.length == 2)
    length = arguments.shift()
  obj = arguments.shift()
  if (!obj.isA(Array)) obj = [obj]
  if (index.isA(Range)) {
    var end = index.rend + (index.rend < 0 ? this.length : 0)
    index = index.start
    length = end - index + 1
  }
  if (index < 0) index += this.length
  this.replace(this.slice(0, index).merge(obj).merge(this.slice(index + length)))
  return(this)
}

Array.prototype.insert = function(index) {
  var values = arguments.toArray().last(-1)
  if (index < 0) index += this.length + 1
  return(this.store(index, 0, values))
}

Array.prototype.update = function(other) {
  var obj = this
  other.each(function(item) { obj.push(item) })
  return(obj)
}

Array.prototype.merge = Array.prototype.concat
Array.prototype.add = function(item) { return(this.merge([item])) }

Array.prototype.clear = function() {
  var obj = this
  this.length.times(function(index) {
    delete obj[index]
  })
  this.length = 0
}

Array.prototype.replace = function(obj) {
  this.clear()
  this.update(obj)
}

Array.prototype.mul = function(count) {
  var result = []
  var obj = this
  count.times(function() { result = result.merge(obj) })
  return(result)
}

Array.prototype.fill = function(value) {
  var old_length = this.length
  var obj = this
  this.clear()
  var block
  if (typeof(value) != "function")
    block = function() { return(value) }
  else
    block = value

  old_length.times(function(i) {
    obj.push(block(i))
  })
}

Array.prototype.removeAt = function(targetIndex) {
  var result = this[targetIndex]
  var newArray = this.reject(function(item, index) {
    return(index == targetIndex)
  })
  this.replace(newArray)
  return(result)
}

Array.prototype.remove = function(obj) {
  this.removeAt(this.index(obj))
}

Array.prototype.removeIf = function(block) {
  this.replace(this.reject(block))
}

function Range(start, end, excludeEnd) {
  this.begin = this.start = start
  this.end = end
  this.excludeEnd = excludeEnd
  this.rend = excludeEnd ? end.pred() : end
  this.length = this.toArray().length
}

function range(start, end) { return(new Range(start, end)) }
function xrange(start, end) { return(new Range(start, end, true)) }

Range.prototype.toString = function() {
  return("" + this.start + (this.excludeEnd ? "..." : "..") + this.end)
}

Range.prototype.each = function(block) {
  var index = 0
  this.start.towards(this.rend, function(i) {return(block(i, index++))})
}
Range.prototype.extend(enumerable)

Range.prototype.includes = function(item) {
  return(this.start.cmp(item) == -1 && this.rend.cmp(item) == +1)
}

function Hash(defaultBlock) {
  this.defaultBlock = defaultBlock
  this.keys = []
  this.values = []
  this.length = 0
}

Hash.fromArray = function(array) {
  var result = new Hash()
  array.each(function(item) {
    var key = item[0], value = item[1]
    result.store(key, value)
  })
  return(result)
}

Hash.prototype.at = Hash.prototype.fetch = function(key, block) {
  var result
  if (this.hasKey(key))
    result = this["item_" + key.hash()]
  else {
    if (block) 
      result = block(key)
    else
      result = defaultBlock(key)
  }
  return(result)
}

Hash.prototype.store = function(key, value) {
  this.keys.push(key)
  this.values.push(value)
  this.length++
  return(this["item_" + key.hash()] = value)
}

Hash.prototype.toA = function() {
  return(this.keys.zip(this.values))
}

Hash.prototype.isEmpty = function() {
  return(this.length == 0)
}

Hash.prototype.has = Hash.prototype.includes = Hash.prototype.hasKey = function(key) {
  return(hasOwnProperty("item_" + key.hash()))
}

Hash.prototype.hasValue = function(value) {
  return(this.values.includes(value))
}

Hash.prototype.each = function(block) {
  this.toA().each(function (pair) {
    return(block(pair[1], pair[0]))
  })
}

Hash.prototype.extend(enumerable)

Hash.prototype.merge = function(other) {
  other.each(function(value, key) {
    this.store(key, value)
  })
}

Hash.prototype.remove = function(key) {
  var valueIndex = this.keys.index(key)
  var value = this.values[valueIndex]
  this.keys.remove(key)
  this.values.removeAt(valueIndex)
  delete(this["item_" + key.hash()])
  this.length--
  return([key, value])
}

Hash.prototype.removeIf = function(block) {
  this.each(function(value, key) {
    if (block(value, key))
      this.remove(key)
  })
}

Hash.prototype.shift = function() {
  return(this.remove(this.keys[0]))
}

Hash.prototype.clear = function() {
  var obj = this
  this.length.times(function() {obj.shift()})
}

Hash.prototype.replace = function(obj) {
  this.clear()
  this.merge(obj)
}

Hash.prototype.invert = function() {
  return(Hash.fromArray(this.map(function(value, key) {
    return([value, key])
  })))
}

Hash.prototype.rehash = function() {
  var result = new Hash(this.defaultBlock)
  this.each(function(value, key) {
    result.store(key, value)
  })
  this.replace(result)
}

function MatchData(matches, str, pos) {
  this.matches = matches, this.string = str
  this.begin = this.position = pos
  this.match = matches[0]
  this.captures = matches.slice(1)
  this.end = pos + this.match.length
  this.length = matches.length
  this.preMatch = str.substr(0, pos)
  this.postMatch = str.substr(this.end)
}

MatchData.prototype.toString = function() { return(this.match) }
MatchData.prototype.at = function(index) {
  return(this.matches.at(index))
}
MatchData.prototype.toArray = function() { return(this.matches) }

RegExp.prototype.match = function(str) {
  var matches
  if (matches = this.exec(str)) {
    var pos = str.search(this)
    return(new MatchData(matches, str, pos))
  }
}

String.prototype.clone = function() { return(new String(this)) }

String.prototype.each = function(block) {
  this.split("\n").each(block)
}

String.prototype.extend(enumerable)

String.prototype.toArray = function() { return(this.split("\n")) }

String.prototype.towards = function(other, block) {
  var item = this
  while (item.cmp(other) <= 0) {
    block(item)
    item = item.succ()
  }
}

String.prototype.hash = function() {
  var result = 0
  this.split("").each(function(item) {
    result += item.charCodeAt(0)
    result += (result << 10)
    result ^= (result >> 6)
  })
  result += (result << 3)
  result ^= (result >> 11)
  result += (result << 15)
  return(result)
}

String.prototype.chars = function() { return(this.split("")) }

String.prototype.at = String.prototype.fetch = function(index, length) {
  if (index.isA(Range)) {
    var end = index.rend + (index.rend < 0 ? this.length : 0)
    index = index.start
    length = end - index + 1
  }
  if (length == undefined) length = 1
  if (index < 0) index += this.length
  return(this.substr(index, length))
}

String.prototype.store = String.prototype.change = function(index) {
  var length = 1, obj
  arguments = arguments.toArray()
  arguments.shift()
  if (arguments.length == 2)
    length = arguments.shift()
  obj = arguments.shift()
  if (index.isA(Range)) {
    var end = index.rend + (index.rend < 0 ? this.length : 0)
    index = index.start
    length = end - index + 1
  }
  if (index < 0) index += this.length
  return(this.substr(0, index) + obj + this.substr(index + length))
}

String.prototype.reverse = function() {
  return(this.split("").reverse().join(""))
}

/*
String.prototype.scan = function(pattern) {
  var str = this, result = [], oldPos = -1, match, offset = 0
  while (match = pattern.match(str)) {
    if (match.end == match.begin)
      throw("Can't have null length matches with scan()")
    var newMatch = new MatchData(match.matches, match.string, match.position + offset)
    result.push(newMatch)
    str = match.postMatch
    offset += match.toString().length
  }
  return(result)
}
*/

String.prototype.sub = function(what, by, global) {
  var block = typeof(by) == "function" ? by : function() { return(by) }
  var matches = this.scan(what), result = this, offset = 0
  if (!global && !by.global) matches = matches.slice(0, 1)
  matches.each (function(match) {
    var replacement = block(match)
    offset += replacement.length - match.toString().length
    result = result.change(match.begin + offset, match.toString().length, replacement)
  })
  return(result)
}
String.prototype.gsub = function(what, by) { return(this.sub(what, by, true)) }

String.prototype.tr = function(from, to) {
  var map = Hash.fromArray(from.chars().zip(to.chars()))
  return(this.chars().map(function(chr) {
    return(map.includes(chr) ? map.fetch(chr) : chr)
  }).join(""))
}

String.prototype.mul = function(other) {
  var result = "", str = this
  other.times(function() { result += str })
  return(result)
}

String.prototype.isUpcase = function() { return(this == this.upcase()) }
String.prototype.isDowncase = function() { return(this == this.downcase()) }
String.prototype.isCapitalized = function() {
  return(this.fetch(0).isUpcase() && this.fetch(range(1, -1)).isDowncase())
}
String.prototype.upcase = String.prototype.toUpperCase
String.prototype.downcase = String.prototype.toLowerCase
String.prototype.capitalize = function() {
  return(this.fetch(0).upcase() + this.fetch(range(1, -1)).downcase())
}
String.prototype.swapcase = function() {
  return(this.chars().map(function(chr) {
    if (chr.isUpcase()) return(chr.downcase())
    if (chr.isDowncase()) return(chr.upcase())
    return(chr)
  }).join(""))
}
String.prototype.ord = function() { return(this.charCodeAt(0)) }

String.prototype.isEmpty = function() { return(this.length == 0) }

String.prototype.succ = function() {
  if (this.isEmpty()) return(this)
  /* numerics */
  if (/^\d+$/.test(this))
    return((Number(this) + 1).toString())
  /* just one character */
  if (this.length == 1) {
    /* letters */
    if (/[A-Za-z]/.test(this)) {
      var lastLetter = this.isUpcase() ? 'Z' : 'z'
      var firstLetter = this.isUpcase() ? 'A' : 'a'
      return((this == lastLetter) ? firstLetter.mul(2) : (this.ord() + 1).chr())
    } else {
      return(this == (-1).chr() ? 0.0.chr().mul(2) : (this.ord() + 1).chr())
    }
  /* multiple characters */
  } else {
    var result = this
    for (var index = this.length; index >= 0; index--) {
      var chr = this.at(index)
      if (chr.succ().length == 1 || index == 0)
        return(result.change(index, chr.succ()))
      else
        result = result.change(index, chr.succ().at(-1))
    }
  }
}

/*
String.prototype.ljust = function(length, fill) {
  if (!fill) fill = " "
  if (fill.length > 1) throw("TODO: Make fills with length > 1 work.")
  return(this + fill.mul(length / fill.length - this.length))
}
*/