#!package export Assets

#!import fs
#!import path
#!import crypto
#!import MiddlewareHandler from arcane-middleware

#!import on-finished
#!import etag
#!import mime

#!import tools.wait

class Assets extends MiddlewareHandler

	middleware: (@config, @global) ->
		self = this

		components_map = {}

		controller_path = "#{@global.root}/controller"
		for i in fs.readdirSync(controller_path)
			component_path = "#{controller_path}/#{i}/components"
			try for x in fs.readdirSync(component_path)
				components_map["/#{i.replace(/Controller$/g , '').toLowerCase()}/components/#{x.replace(/.ts$/g, '')}"] = "#{component_path}/#{x}"

		@config.on 'components-delete', (path) ->
			delete components_map[path];

		@config.on 'components-added', (path_arr) ->
			components_map[path_arr[0].replace(/.ts$/g, '')] = path_arr[1]

		(req, res, app) ->
			request_file = "#{req.root}/assets#{req.url.split('?')[0]}"

			if not (fs.existsSync(request_file) and fs.lstatSync(request_file).isFile()) and components_map[req.url.split('?')[0]]?
				request_file = components_map[req.url.split('?')[0]]

			if request_file? and fs.existsSync(request_file) and fs.lstatSync(request_file).isFile()
				stats = fs.statSync(request_file)
				result = wait.for Assets.RequestFileStaticFile, req, res, request_file, stats
				throw 'assets'

	@ReadStream: fs.ReadStream

	@rangeParser: (size, str) ->
		valid = true
		i = str.indexOf('=')
		if -1 == i
			return -2
		arr = str.slice(i + 1).split(',').map((range) ->
			_range = range.split('-')
			start = parseInt(_range[0], 10)
			end = parseInt(_range[1], 10)
			# -nnn
			if isNaN(start)
				start = size - end
				end = size - 1
				# nnn-
			else if isNaN(end)
				end = size - 1
			# limit last-byte-pos to current length
			if end > size - 1
				end = size - 1
			# invalid
			if isNaN(start) or isNaN(end) or start > end or start < 0
				valid = false
			{
				start: start
				end: end
			}
		)
		arr.type = str.slice(0, i)
		if valid then arr else -1

	@RequestFileStaticFile: (req, res, path, stat, cb) ->
		len = stat.size
		options = {}
		opts = {}
		ranges = req.headers.range
		offset = options.start or 0
		#debug('pipe "%s"', path);
		# set header fields
		Assets._setHeader res, path, stat
		# set content-type
		if res.getHeader('Content-Type')
			cb null, true
			return
		type = mime.lookup(path)
		charset = mime.charsets.lookup(type)
		res.set 'Content-Type', type + (if charset then '; charset=' + charset else '')
		#------------------------------------------------------------------------------------------//
		# conditional GET support
		if Assets.isConditionalGET(req) and Assets.isCachable(res) and Assets.isFresh(req, res, stat)
			#console.log('ERROR: not modified ' + path);
			cb null, true
			return Assets.notModified(res)
		#-----------------------------------------------------------------------------------------//
		# adjust len to start/end options
		len = Math.max(0, len - offset)
		if options.end != undefined
			bytes = options.end - offset + 1
			if len > bytes
				len = bytes
		#-----------------------------------------------------------------------------------------//
		# Range support
		if ranges
			ranges = Assets.rangeParser(len, ranges)
			# If-Range support
			if !Assets.isRangeFresh(req, res)
				#debug('range stale');
				ranges = -2
			# unsatisfiable
			if -1 == ranges
				#debug('range unsatisfiable');
				res.set 'Content-Range', 'bytes */' + stat.size
				Assets.serverError 416
				cb null, true
				return
			# valid (syntactically invalid/multiple ranges are treated as a regular response)
			if -2 != ranges and ranges.length == 1
				#debug('range %j', ranges);
				# Content-Range
				res.status 206
				res.set 'Content-Range', 'bytes ' + ranges[0].start + '-' + ranges[0].end + '/' + len
				offset += ranges[0].start
				len = ranges[0].end - (ranges[0].start) + 1
		#---------------------------------------------------------------------------------------//
		# clone options
		for prop of options
			opts[prop] = options[prop]
		#--------------------------------------------------------------------------------------//
		# set read options
		opts.start = offset
		opts.end = Math.max(offset, offset + len - 1)
		#---------------------------------------------------------------------------------------//
		# content-length
		res.set 'Content-Length', len
		# HEAD support
		if 'HEAD' == req.method
			res.send res.statusCode
			cb null, true
			return
		# res.updateHeaders res.statusCode
		Assets.ServerStream req, res, path, opts, cb
		return

	@_setHeader: (res, path, stat) ->
		if !res.get('Accept-Ranges')
			res.set 'Accept-Ranges', 'bytes'
		if !res.get('Date')
			res.set 'Date', (new Date).toUTCString()
		if !res.get('Cache-Control')
			res.set 'Cache-Control', 'public, max-age=31536000'
		if !res.get('Last-Modified')
			modified = stat.mtime.toUTCString()
			res.set 'Last-Modified', modified
		if !res.get('ETag')
			val = etag(stat)
			res.set 'ETag', val
		return

	@isConditionalGET: (req) ->
		req.headers['if-none-match'] or req.headers['if-modified-since']

	@isCachable: (res) ->
		res.statusCode >= 200 and res.statusCode < 300 or 304 == res.statusCode

	@isFresh: (req, res, stat) ->
		Assets.fresh req.headers, res._headers, stat

	@fresh: (req, res, stat) ->
		mtime = Date.parse(stat.mtime)
		headers = {}
		clientETag = req['if-none-match']
		clientMTime = Date.parse(req['if-modified-since'])
		length = stat.size
		(clientMTime or clientETag) and (!clientETag or clientETag == etag(stat)) and (!clientMTime or clientMTime >= mtime)

	@notModified: (res) ->
		Assets.removeContentHeaderFields res
		res.sendStatus 304
		return

	@removeContentHeaderFields: (res) ->
		headers = [
			'Content-Encoding'
			'Content-Language'
			'Content-Length'
			'Content-Location'
			'Content-MD5'
			'Content-Range'
			'Content-Type'
			'Expires'
			'Last-Modified'
		]
		for i of headers
			res.removeHeader headers[i]
		return

	@isRangeFresh: (req, res) ->
		ifRange = req.headers['if-range']
		if !ifRange
			return true
		if ~ifRange.indexOf('"') then ~ifRange.indexOf(req.headers.etag) else Date.parse(req.headers['last-modified']) <= Date.parse(ifRange)

	@serverError: (res, status) ->
		msg = http.STATUS_CODES[status]
		res._headers = undefined
		res.send status, msg
		return

	@ServerStream: (req, res, path, options, cb) ->
		# TODO: this is all lame, refactor meeee
		finished = false
		# pipe
		_stream = fs.createReadStream(path, options)
		#this.emit('stream', _stream);
		_stream.pipe res
		# response finished, done with the fd
		onFinished res, ->
			finished = true
			Assets._destroy _stream
			cb null, 'assets:finish'
			return
		# error handling code-smell
		_stream.on 'error', (err) ->
			# request already finished
			if finished
				cb null, 'assets:already-finish'
				return
			# clean up stream
			finished = true
			Assets._destroy _stream
			# error
			#self.onStatError(err);
			notfound = [
				'ENOENT'
				'ENAMETOOLONG'
				'ENOTDIR'
			]
			if ~notfound.indexOf(err.code)
				cb null, true
				return Assets.serverError(404, err)
			Assets.serverError res, 500, err
			cb null, true
			return
		# end
		_stream.on 'end', ->
			#self.emit('end');
			#res.send(res.statusCode);
			return
		do req.__response
		return

	@_destroy: (stream) ->
		if stream instanceof Assets.ReadStream
			return Assets.destroyReadStream(stream)
		if !(stream instanceof Stream)
			return stream
		if typeof stream.destroy == 'function'
			stream.destroy()
		stream

	@destroyReadStream: (stream) ->
		stream.destroy()
		if typeof stream.close == 'function'
			# node.js core bug work-around
			stream.on 'open', Assets.onopenClose
		stream

	@onopenClose: ->
		if typeof @fd == 'number'
			# actually close down the fd
			@close()
		return
