url = require 'url'
async = require 'async'
fs = require 'fs'
sanitizer = require 'sanitizer'
require 'juicy'

Function::clone = -> # function cloning
	clone = ->
	for property of @
		clone[property] = @[property] if @hasOwnProperty property
	
	clone.prototype = @prototype
	clone


class Lychee
	@drivers: {}
	@defaultDriver: ''
	
	@registerDriver: (name, driver) -> # register driver
		if driver.Extensions
			Lychee.Model.include driver.Extensions
		
		@drivers[name] = driver
	
	@connect: (hosts, done) -> # connect to all databases
		params = {}
		
		params[driver] = [] for driver of @drivers
		
		for host in hosts
			driver = url.parse(host).protocol.slice 0, -1
			params[driver].push host
		
		async.forEach Object.keys(params), (driver, nextDriver) =>
			@drivers[driver].connect params[driver], nextDriver
		, -> done() if done
	
	@disconnect: (done) -> # disconnect from all databases
		async.forEach Object.keys(@drivers), (driver, nextDriver) =>
			@drivers[driver].disconnect nextDriver
			@drivers[driver].disconnect nextDriver
		, -> done() if done
	
	@setDefaultDriver: (name) -> # setting default driver for all models
		@defaultDriver = name
	
	@setup: (model) -> # setting up model
		model::collectionName = model.collectionName ||= model.name.downcase.pluralized
		
		keys = []
		model::keys = model::keys.removeIf (item) ->
			if keys.indexOf(item.key) is -1
				keys.push item.key
				false
			else true
		
		model::scopes ||= []
		if model::scopes.length > 0
			model::scopes.forEach (scope) ->
				model[scope.name] = (callback) -> @find scope.query, callback
		
		model::validators ||= {}
		for key of model::validators
			keyExists = no
			for field in model::keys
				keyExists = yes if key is field.key
			
			delete model::validators[key] if not keyExists
		
		model::keys.forEach (field) ->
			model::primaryKey = field.key if field.primary
			
			model::__defineGetter__ field.key, ->
				@get field.key
			
			model::__defineSetter__ field.key, (value) ->
				@set field.key, value
		
		['driver', 'collection', 'scopes', 'scope', 'key', 'timestamps', 'validate', 'validates'].forEach (property) -> delete model[property]
		
		model::driver = model.driver = new @drivers[model.driverName or 'mongodb'] model
		
		model.model = model::model = model
		model

class Lychee.Validators
	@validators: []
	
	@register: (validator) -> @validators.push validator

Lychee.Validators.register require('./validators/string_length')
Lychee.Validators.register require('./validators/number_value')
Lychee.Validators.register require('./validators/presence')
Lychee.Validators.register require('./validators/acceptance')
Lychee.Validators.register require('./validators/confirmation')
Lychee.Validators.register require('./validators/with')
Lychee.Validators.register require('./validators/numericality')
Lychee.Validators.register require('./validators/condition')

class Lychee.Events
	@listeners: {}
	
	@on: (model, event, listener) ->
		@listeners[model] = {} if not @listeners[model]
		@listeners[model][event] = [] if not @listeners[model][event]
		@listeners[model][event].push listener
	
	@unbind: (model, event) ->
		@listeners[model][event] = []
	
	@emit: (model, event, context = @) ->
		return if not (@listeners[model] and @listeners[model][event])
		
		listener.call(context) for listener in @listeners[model][event]

class Lychee.Model
	constructor: (fields) ->
		@fields = {}
		@old = {}
		@errors = []
		@isValid = undefined
		@updateAttributes fields
		@
	
	get: (key) ->
		@fields[key]
	
	set: (key, value) ->
		if @fields[key] and (not @old[key] or @old[key] != value)
			@old[key] = @fields[key]
		
		@fields[key] = if value instanceof String then sanitizer.escape(value) else value
	
	setDefaults: -> # setting defaults, if they exist
		for field in @keys
			if (field instanceof Object) and field.default and not @fields[field.key]
				@fields[field.key] = if 'function' is typeof field.default then field.default() else field.default

	updateAttributes: (fields = {}) -> # mass-updating fields
		for key of fields
			if fields.hasOwnProperty key
				@set key, fields[key]
	
	update_attributes: -> @updateAttributes.apply @, arguments
	
	@driver: (@driverName = 'mongodb') ->
	
	@collection: (@collectionName) ->
	
	@scope: (name, query = {}) ->
		@::scopes = [] if not @::scopes
		@::scopes.push name: name, query: query
	
	@key: (key, params = {}) ->
		@::keys = [] if not @::keys
		@::keys.push Object.merge(key: key, params)
	
	key: (key, params = {}) ->
		@keys.push Object.merge(key: key, params)
	
	@timestamps: (keys = {}) ->
		keys.create ||= 'created_at'
		keys.update ||= 'updated_at'
		@::timestamps = keys
		@key keys.create, default: Date.now
		@key keys.update, default: Date.now
	
	@validates: ->
		@::validators = all: [] if not @::validators
		keys = []
		params = {}
		if arguments[0] instanceof Object and not arguments[1] # Custom validator
			keys.push 'all'
			params = with: arguments[0]
		else
			for item of arguments
				if 'string' is typeof arguments[item]
					keys.push arguments[item]
				else if 'object' is typeof arguments[item]
					params = arguments[item]
		
		validators = []
		
		for validator in Lychee.Validators.validators
			detected = no
			for key in validator.detectBy
				if params[key]
					detected = yes
					break
			
			validators.push validator if detected
		
		for key in keys
			@::validators[key] = [] if not @::validators[key]
			for validator in validators
				@::validators[key].push new validator(Object.merge(key: key, params))
		
		undefined
	
	@validate: -> @validates.apply @, arguments
	
	validate: (callback) ->
		@errors = all: []
		@isValid = yes
		async.forEach @keys, (field, nextKey) =>
			return nextKey() if not @validators[field.key]
			
			async.forEach @validators[field.key], (validator, nextValidator) =>
				validator.validate @fields[field.key], (valid, message) =>
					if not valid
						@errors[field.key] = [] if not @errors[field.key]
						@errors[field.key].push if message then message else 'is not valid'
						@isValid = no
					
					nextValidator()
				, @
			, -> nextKey()
		, => callback @isValid
	
	@find: (options, done) ->
		if options instanceof Function
			done = options
			options = {}

		@driver.find options, (err, items) =>
			models = []

			for item in items
				model = new @model item
				models.push model

			done err, models if done
	
	@all: -> @find.apply @, arguments

	save: (params, done) ->
		@setDefaults()
		if 'function' is typeof params
			done = params
			params = validate: yes
		
		next = =>
			handler = =>
				@callHook 'aroundSave'
				@callHook 'afterSave'
				Lychee.Events.emit @collectionName, 'save', @
				done() if done
				@callHook 'beforeSave'
				@callHook 'aroundSave'
			
			if @fields[@primaryKey] then @update(handler) else @create(handler)
			
		if params.validate or not params.validate?
			@validate (valid) =>
				return done(true) if not valid
				
				next()
		else next()

	create: (done) ->
		@callHook 'beforeCreate'
		@callHook 'aroundCreate'

		@driver.create @fields, (err, fields) =>
			for key in @keys
				@set key.key, fields[key.key]
			
			@callHook 'aroundCreate'
			@callHook 'afterCreate'

			Lychee.Events.emit @collectionName, 'create', @
			done() if done

	update: (done) ->
		@callHook 'beforeUpdate'
		@callHook 'aroundUpdate'
		
		if @timestamps and @timestamps.update
			delete @fields[@timestamps.update]
			@setDefaults()
		
		@driver.update @fields, (err, fields) =>
			@callHook 'aroundUpdate'
			@callHook 'aroundUpdate'

			Lychee.Events.emit @collectionName, 'update', @
			done() if done

	remove: (done) ->
		@callHook 'beforeRemove'
		@callHook 'aroundRemove'

		@driver.remove @fields, (err) =>
			@callHook 'aroundRemove'
			@callHook 'afterRemove'

			Lychee.Events.emit @collectionName, 'remove', @
			Object.freeze @fields

			done() if done

	@remove: (done) ->
		@driver.removeAll (err) =>
			Lychee.Events.emit @collectionName, 'removeAll', @
			done() if done

	callHook: (name) ->
		hook = @[name] or @[name.underscored]
		hook.call @ if hook

	@on: (event, listener) ->
		Lychee.Events.on @collectionName, event, listener

	@bind: -> @on.apply @, arguments

	@unbind: (event) ->
		Lychee.Events.unbind @collectionName, event


Lychee.registerDriver 'mongodb', require('./drivers/mongodb')
Lychee.registerDriver 'rest', require('./drivers/rest')
Lychee.registerDriver 'pg', require('./drivers/pg')

module.exports = Lychee