# Require
fs = require 'fs'
now = require 'now'
path = require 'path'
buildr = require 'buildr'
nowpadCommon = require(__dirname+'/public/common.coffee').nowpadCommon
List = nowpadCommon.List

# -------------------------------------
# Classes

class Client
	# Required
	id: null
	nowpad: null

	# Optional
	documentIds: []

	# Constructor
	constructor: (id,nowpad) ->
		# Reset
		@documentIds = []

		# Apply
		@id = id
		@nowpad = nowpad
	
	# Destroy
	destroy: ->
		# Prepare
		clientId = @id

		# Remove
		@nowpad.clients.remove clientId

		# Clear Documents
		documentIds = @documentIds
		@documentIds = []

		# Destroy Documents
		for documentId in documentIds
			document = @nowpad.documents.get documentId
			if document and typeof document.clientIds[clientId] is not 'undefined'
				delete document.clientIds[clientId]
				if document.clientIds.length is 0
					document.destroy()
	
		# Log
		console.log 'Destroyed Client', clientId

class Document
	# Required
	id: null
	nowpad: null

	# Optional
	value: ''
	state: false
	delay: 200
	states: []
	offset: 0
	locked: false
	clientIds: []

	# Constructor
	constructor: (id,value,nowpad) ->
		# Reset
		@states = []
		@clientIds = []

		# Apply
		@id = id
		@value = value if value
		@nowpad = nowpad

	# Lock
	lock: (clientId) ->
		success = false

		if !@locked
			@locked = clientId
			success = true
			console.log 'Client '+clientId+' locked the document '+@id
		else
			console.log 'Client '+clientId+' failed to lock the document '+@id
			console.log 'it\'s currently locked by client '+@locked
		
		return success
	
	# Unlock
	unlock: (clientId) ->
		success = false

		if @locked is clientId
			@locked = false
			success = true
			console.log 'Client '+clientId+' unlocked the document '+@id
		else
			console.log 'Client '+clientId+' failed to unlock the document '+@id
			console.log 'it\'s currently locked by client '+@locked
		
		return success
	
	# Destroy
	destroy: ->
		# Prepare
		documentId = @id

		# Remvoe
		@nowpad.clients.remove documentId

		# Clear Clients
		clientIds = @clientIds
		@clientIds = []

		# Destroy Clients
		for clientId in clientIds
			client = @nowpad.clients.get clientId
			if client and typeof document.documentIds[documentId] is not 'undefined'
				delete document.documentIds[documentId]
				if client.documentIds.length is 0
					client.destroy()
		
		# Log
		console.log 'Destroyed Document', documentId

class DocumentList extends List
	# Init
	constructor: (nowpad) ->
		@nowpad = nowpad
	
	# Get a document, or create it if it doesn't exist
	# next(err,document)
	fetch: (id,next) ->
		document = @get id
		if document
			next false, document
		else if @nowpad.requestHandler
			@nowpad.requestHandler id, =>
				document = @get id
				if document
					next false, document
				else
					next Error 'Could not fetch the document '+id
		else
			document = new Document id, false, @nowpad
			@nowpad.documents.add document
			next false, document

# -------------------------------------
# Server

class Nowpad

	# Server
	server: null
	everyone: null
	port: null
	buildrConfig: 
			# Paths
			srcPath: __dirname+'/public'
			outPath: __dirname+'/public'

			# Checking
			checkScripts: false

			# Compression (requires outPath)
			compressScripts: false # Array or true or false

			# Order
			scriptsOrder: [
				'diff_match_patch.js'
				'common.coffee'
				'client.coffee'
			]

			# Bundling
			bundleScriptPath: __dirname+'/public/out.js'

	# Nowpad
	documents: null
	clients: null
	requestHandler: null
	
	# Events
	events:
		sync: []
		disconnected: []
	
	# Initialise
	constructor: ({server,everyone}={}) ->
		# Prepare
		@server = server
		@everyone = everyone || now.initialize @server, {clientWrite: false}

		# Clean
		@documents = new DocumentList(@)
		@clients = new List()

		# Now
		@nowBind()

		# Cache
		@cacheClientScript()

		# Routes
		@server.get '/nowpad/nowpad.js', (req,res) =>
			@serveClientScript(req,res)

		# Clean
		@documents = new DocumentList(@)
		@clients = new List()

	# Cache the client script
	cacheClientScript: ->
		# Build
		mercuryBuildr = buildr.createInstance @buildrConfig
		mercuryBuildr.process (err) =>
			throw err  if err
			console.log 'Building completed'
			fs.readFile @buildrConfig.bundleScriptPath, (err,data) =>
				throw err  if err
				@fileString = data.toString()
	
	# Server the client script
	serveClientScript: (req,res) ->
		console.log @fileString
		res.writeHead 200, 'content-type': 'text/javascript'
		res.write @fileString
		res.end()
	
	# Log
	log: ->
		console.log(
			clients: @clients
			documents: @documents
		)
	
	# Add document
	addDocument: (documentId,value) ->
		unless @documents.has documentId
			document = new Document documentId, value, @
			@documents.add document

	# Delete document
	delDocument: (documentId) ->
		document = @documents.get documentId
		if document
			document.destroy()

	# Request document
	requestDocument: (requestHandler) ->
		if @requestHandler
			throw new Error 'Request handler already defined'
		@requestHandler = requestHandler
	
	# Initialise Now.js
	nowBind: ->
		nowpad = @
		everyone = @everyone

		# A client has connected
		everyone.on 'join', ->
			# Create the new client
			clientId = nowpad.clients.generateId()
			client = new Client(clientId,nowpad)
			nowpad.clients.add(client)

			# Associate it with now
			@now.clientId = clientId

			# Log
			console.log 'New Client:', clientId

		# A client has disconnected
		everyone.on 'leave', ->
			# Fetch
			clientId = @now.clientId
			nowpad.clients.destroy @now.clientId

			# Log
			console.log 'Bye Client:', clientId
		
		# A client is shaking hands with the server
		everyone.now.nowpad_handshake = (notifySync,notifyDelay,callback) ->
			# Check the user isn't evil
			if (typeof notifySync isnt 'function') or (typeof notifyDelay isnt 'function')
				console.log 'Evil client'
				return false
			
			# Apply the client-side functions used to notify the client to the now session
			@now.nowpad_notifySync = notifySync
			@now.nowpad_notifyDelay = notifyDelay

			# Trigger the callback
			if callback then callback(@now.clientId)
	
		# Create a timer to ensure locks don't last forever
		lockTimer = false
		lockTimerDelay = 1500

		# Lock a document
		everyone.now.nowpad_lockDocument = (documentId, callback) ->
			# Fetch Document
			nowpad.documents.fetch documentId, (err,document) =>
				# Error
				if err
					console.log err
					return

				# Attempt document lock
				result = document.lock @now.clientId

				# If success, set a timeout
				if result
					lockTimerCallback = =>
						clearTimeout lockTimer
						lockTimer = false
						console.log '\n!!! A lock has lasted too long... !!!\n'
						document.unlock @now.clientId
					lockTimer = setTimeout lockTimerCallback, lockTimerDelay
				
				# Send result back to client
				if callback then callback result
			
		# Unlock
		everyone.now.nowpad_unlockDocument = (documentId, callback) ->
			# Fetch Document
			nowpad.documents.fetch documentId, (err,document) =>
				# Error
				if err
					console.log err
					return

				# Attempt document unlock
				result = document.unlock @now.clientId

				# Clear timeout
				clearTimeout lockTimer
				lockTimer = false

				# Send result back to client
				if callback then callback result
			
		# Log
		everyone.now.nowpad_log = ->
			nowpad.log()
		
		# A document is preparing for sync
		everyone.now.nowpad_valueSyncDocument = (documentId, callback) ->
			# Fetch document
			nowpad.documents.fetch documentId, (err,document) =>
				# Error
				if err
					console.log err
					return
				
				# Fetch values
				state = document.state
				value = document.value
				delay = document.delay

				# Send back
				callback state, value, delay

				# Log
				console.log 'Valuing', @now.clientId, 'for document', documentId
			
		# Sync
		everyone.now.nowpad_patchSyncDocument = (documentId,clientState,patch,callback) ->
			# Fetch document
			nowpad.documents.fetch documentId, (err,document) =>
				# Error
				if err
					console.log err
					return
				
				# Prepare
				stateQueue = []
				document.state = document.state || 0

				# Log
				console.log '\nSyncing ['+@now.clientId+'/'+documentId+']'
				#console.log document
				console.log ''

				# Update Client
				if clientState isnt document.state
					# Requires Updates

					# Add patches
					stateQueue = document.states.slice clientState
					
					# Log
					console.log 'Syncing from', clientState, 'to', document.state
					console.log stateQueue
					console.log ''

				# Update Server
				if patch
					# Update
					document.state++

					# Log
					console.log 'Received patch:', @now.clientId, 'from', clientState, 'to', document.state
					console.log patch
					console.log ''

					# State
					State =
						id: document.state
						patch: patch
						clientId: @now.clientId
					
					# Add
					stateQueue.push State
					document.states.push State

					# Apply
					result = nowpadCommon.applyPatch patch, document.value
					document.value = result.value

				# Return updates to client
				callback(stateQueue,document.state)
				
				# Notify other clients
				if patch
					# Notify nowpad clients
					everyone.now.nowpad_notifySync document.id, document.state

					# Notify application
					nowpad.trigger 'sync', [document.id, document.value, document.state]
		
	# Bind
	bind: (event,callback) ->
		if typeof @events[event] is 'undefined'
			throw new Error 'Unauthorised event: '+event
		else
			@events[event].push callback
	
	# Trigger
	trigger: (event,args) ->
		for callback in @events[event]
			callback.apply(callback,args)

# API
nowpad =
	createInstance: (config) ->
		return new Nowpad(config)

# Export
module.exports = nowpad
