(function() {
	// Public sightglass interface.
	function sightglass(obj, keypath, callback, options) {
		return new Observer(obj, keypath, callback, options)
	}

	// Batteries not included.
	sightglass.adapters = {}

	// Constructs a new keypath observer and kicks things off.
	function Observer(obj, keypath, callback, options) {
		this.options = options || {}
		this.options.adapters = this.options.adapters || {}
		this.obj = obj
		this.keypath = keypath
		this.callback = callback
		this.objectPath = []
		this.parse()

		if (isObject(this.target = this.realize())) {
			this.set(true, this.key, this.target, this.callback)
		}
	}

	// Tokenizes the provided keypath string into interface + path tokens for the
	// observer to work with.
	Observer.tokenize = function(keypath, interfaces, root) {
		var tokens = []
		var current = {i: root, path: ''}
		var index, chr

		for (index = 0; index < keypath.length; index++) {
			chr = keypath.charAt(index)

			if (!!~interfaces.indexOf(chr)) {
				tokens.push(current)
				current = {i: chr, path: ''}
			} else {
				current.path += chr
			}
		}

		tokens.push(current)
		return tokens
	}

	// Parses the keypath using the interfaces defined on the view. Sets variables
	// for the tokenized keypath as well as the end key.
	Observer.prototype.parse = function() {
		var interfaces = this.interfaces()
		var root, path

		if (!interfaces.length) {
			error('Must define at least one adapter interface.')
		}

		if (!!~interfaces.indexOf(this.keypath[0])) {
			root = this.keypath[0]
			path = this.keypath.substr(1)
		} else {
			if (typeof (root = this.options.root || sightglass.root) === 'undefined') {
				error('Must define a default root adapter.')
			}

			path = this.keypath
		}

		this.tokens = Observer.tokenize(path, interfaces, root)
		this.key = this.tokens.pop()
	}

	// Realizes the full keypath, attaching observers for every key and correcting
	// old observers to any changed objects in the keypath.
	Observer.prototype.realize = function() {
		var current = this.obj
		var unreached = false
		var prev

		this.tokens.forEach(function(token, index) {
			if (isObject(current)) {
				if (typeof this.objectPath[index] !== 'undefined') {
					if (current !== (prev = this.objectPath[index])) {
						this.set(false, token, prev, this.update.bind(this))
						this.set(true, token, current, this.update.bind(this))
						this.objectPath[index] = current
					}
				} else {
					this.set(true, token, current, this.update.bind(this))
					this.objectPath[index] = current
				}

				current = this.get(token, current)
			} else {
				if (unreached === false) {
					unreached = index
				}

				if (prev = this.objectPath[index]) {
					this.set(false, token, prev, this.update.bind(this))
				}
			}
		}, this)

		if (unreached !== false) {
			this.objectPath.splice(unreached)
		}

		return current
	}

	// Updates the keypath. This is called when any intermediary key is changed.
	Observer.prototype.update = function() {
		var next, oldValue

		if ((next = this.realize()) !== this.target) {
			if (isObject(this.target)) {
				this.set(false, this.key, this.target, this.callback)
			}

			if (isObject(next)) {
				this.set(true, this.key, next, this.callback)
			}

			oldValue = this.value()
			this.target = next

			if (this.value() !== oldValue) this.callback()
		}
	}

	// Reads the current end value of the observed keypath. Returns undefined if
	// the full keypath is unreachable.
	Observer.prototype.value = function() {
		if (isObject(this.target)) {
			return this.get(this.key, this.target)
		}
	}

	// Sets the current end value of the observed keypath. Calling setValue when
	// the full keypath is unreachable is a no-op.
	Observer.prototype.setValue = function(value) {
		if (isObject(this.target)) {
			this.adapter(this.key).set(this.target, this.key.path, value)
		}
	}

	// Gets the provided key on an object.
	Observer.prototype.get = function(key, obj) {
		return this.adapter(key).get(obj, key.path)
	}

	// Observes or unobserves a callback on the object using the provided key.
	Observer.prototype.set = function(active, key, obj, callback) {
		var action = active ? 'observe' : 'unobserve'
		this.adapter(key)[action](obj, key.path, callback)
	}

	// Returns an array of all unique adapter interfaces available.
	Observer.prototype.interfaces = function() {
		var interfaces = Object.keys(this.options.adapters)

		Object.keys(sightglass.adapters).forEach(function(i) {
			if (!~interfaces.indexOf(i)) {
				interfaces.push(i)
			}
		})

		return interfaces
	}

	// Convenience function to grab the adapter for a specific key.
	Observer.prototype.adapter = function(key) {
		return this.options.adapters[key.i] ||
			sightglass.adapters[key.i]
	}

	// Unobserves the entire keypath.
	Observer.prototype.unobserve = function() {
		var obj

		this.tokens.forEach(function(token, index) {
			if (obj = this.objectPath[index]) {
				this.set(false, token, obj, this.update.bind(this))
			}
		}, this)

		if (isObject(this.target)) {
			this.set(false, this.key, this.target, this.callback)
		}
	}

	// Check if a value is an object than can be observed.
	function isObject(obj) {
		return typeof obj === 'object' && obj !== null
	}

	// Error thrower.
	function error(message) {
		throw new Error('[sightglass] ' + message)
	}

	// Export module for Node and the browser.
	if (typeof module !== 'undefined' && module.exports) {
		module.exports = sightglass
	} else if (typeof define === 'function' && define.amd) {
		define([], function() {
			return this.sightglass = sightglass
		})
	} else {
		this.sightglass = sightglass
	}
}).call(this);
