/**********************************************************************
* 
*
*
**********************************************************************/
((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define)
(function(require){ var module={} // make module AMD/node compatible...
/*********************************************************************/

var fs = require('fs')
var glob = require('glob')
var cp = require('child_process')

var object = require('ig-object')
var types = require('ig-types')

var pwpath = require('../path')
var base = require('./base')



//---------------------------------------------------------------------

//
// XXX structure is not final...
// 		- need to split each adapter into modules...
// 		- should the media handler api be merged with store???
// 		- how do we handle config???

var FILESTORE_OPTIONS = {
	index: '.index',
	backup: '/.backup',

	clearEmptyDir: true,
	dirToFile: true,
	cleanBackup: true,

	verbose: true,
}


var getOpts = 
function(opts){
	return {
		...FILESTORE_OPTIONS,
		...(opts ?? {}),
	} }


var encode = 
module.encode =
function(str){
	return str.replace(/[^\w .\\\/_\-]/gi, 
		function(c){
			return `%${c
				.charCodeAt(0)
				.toString(16)
				.toUpperCase()}` }) }
var decode =
module.decode =
function(str){
	return decodeURIComponent(str) }


//	func(base[, options])
//		-> true/false
//
//	func(base, path[, options])
//		-> true/false
//
// XXX not yet sure how w should handle dot-files....
// XXX should these be store methods???
// XXX do we need error checking in these???
var exists =
module.exists =
async function(base, sub, options){
	if(typeof(sub) != 'string'){
		options = sub ?? options
		sub = base
		base = null }
	var {index} = getOpts(options)

	sub = pwpath.sanitize(sub)
	var target = encode(
		base ?
			pwpath.join(base, sub)
			: sub)
	if(!fs.existsSync(target)){
		return false }
	var stat = await fs.promises.stat(target)
	if(stat.isDirectory()){
		return fs.existsSync(pwpath.join(target, index)) }
	return true }
var read =
module.read =
async function(base, sub, options){
	if(typeof(sub) != 'string'){
		options = sub ?? options
		sub = base
		base = null }
	var {index} = getOpts(options)

	sub = pwpath.sanitize(sub)
	var target = encode(
		base ?
			pwpath.join(base, sub)
			: sub)
	if(!fs.existsSync(target)){
		return undefined }
	// handle dir text...
	var stat = await fs.promises.stat(target)
	if(stat.isDirectory()){
		var target = pwpath.join(target, index) 
		fs.existsSync(target)
			|| (target = false) }
	return target ?
		fs.promises.readFile(target, {encoding: 'utf-8'})
		: undefined }
var mkdir = 
module.mkdir =
async function(base, sub, options){
	if(typeof(sub) != 'string' 
			&& !(sub instanceof Array)){
		options = sub ?? options
		sub = base 
		base = null }
	var {index, verbose} = getOpts(options)

	sub = pwpath.sanitize(sub)
	var levels = pwpath.split(sub)
	for(var level of levels){
		base = encode(
			base == null ?
				level
				: pwpath.join(base, level))
		// nothing exists -- create dir and continue...
		if(!fs.existsSync(base)){
			verbose 
				&& console.log('mkdir(..): mkdir:', base)
			await fs.promises.mkdir(base, {recursive: true}) 
			continue }
		// directory -- continue...
		var stat = await fs.promises.stat(base)
		if(stat.isDirectory()){
			continue }
		// file -- convert to dir...
		verbose 
			&& console.log('mkdir(..): converting file to dir:', base)
		await fs.promises.rename(base, base+'.pwiki-bak') 
		await fs.promises.mkdir(base, {recursive: true}) 
		await fs.promises.rename(base +'.pwiki-bak', base +'/'+ index) }
	return base }
var update = 
module.update =
async function(base, sub, data, options){
	if(typeof(data) != 'string'){
		options = data ?? options
		data = sub
		sub = base
		base = null }
	var {index} = getOpts(options)

	sub = pwpath.sanitize(sub)
	var target = encode(
		base ?
			pwpath.join(base, sub)
			: sub)
	// path already exists...
	if(fs.existsSync(target)){
		var stat = await fs.promises.stat(target)
		if(stat.isDirectory()){
			target = pwpath.join(target, index) } 
	// create path / parts of path...
	} else {
		// NOTE: no need to encode stuff here as it will be taken care 
		// 		of by .mkdir(..)
		var levels = pwpath.split(sub)
		levels.pop()
		// ensure the parent dir exists...
		await module.mkdir(
			...(base ?
				// NOTE: we are keeping this separate here to avoid creating 
				// 		anything above it...
				[base]
				: []), 
			levels, 
			options) }
	// write the data...
	var f = await fs.promises.open(target, 'w')
	await f.writeFile(data)
	f.close()
	return target }
var clear = 
module.clear =
async function(base, sub, options){
	if(typeof(sub) != 'string'){
		options = sub ?? options
		sub = base
		base = '' }
	var {index} = getOpts(options)

	sub = pwpath.sanitize(sub)
	// remove leaf...
	var target = encode(
		base == '' ?
			sub
			: pwpath.join(base, sub))
	// dir...
	if(fs.existsSync(target)){
		var stat = await fs.promises.stat(target)
		if(stat.isDirectory()){
			var files = await fs.promises.readdir(target)
			// remove index...
			if(files.includes(index)){
				await fs.promises.rm(pwpath.join(target, index))
				// NOTE: we do not care what we pop as long as the .length 
				// 		is correct as we'll not be using the content after 
				// 		this point...
				files.pop() }
			// remove dir if empty...
			if(files.length == 0){
				fs.promises.rmdir(target) }
		// simple file...
		} else {
			await fs.promises.rm(target) } }
	// cleanup path -- remove empty dirs... (XXX ???)
	var levels = pwpath.split(encode(sub))
		.slice(0, -1)
	base = encode(base)
	while(levels.length > 0){
		var cur = pwpath.join(base, ...levels)
		if(fs.existsSync(cur)){
			var stat = await fs.promises.stat(base)
			if(stat.isDirectory()){
				// stop cleanup if non-empty dir...
				if((await fs.promises.readdir(cur)).length != 0){
					break }
				fs.promises.rmdir(cur) } }
		levels.pop() } 
	return target }
var cleanup =
module.cleanup =
async function(base, options){
	var {index, clearEmptyDir, dirToFile, verbose} = getOpts(options)

	glob(pwpath.join(encode(base), '**/*'))
		.on('end', async function(paths){
			paths
				.sort(function(a, b){
					return b.length - a.length })
			for(var path of paths){
				var stat = await fs.promises.stat(path)
				if(stat.isDirectory()){
					var children = await fs.promises.readdir(path)
					// empty -> remove...
					if(clearEmptyDir 
							&& children.length == 0){
						verbose 
							&& console.log('cleanup(..): removing dir:', path)
						fs.promises.rmdir(path)
						continue }
					// dir -> file...
					if(dirToFile
							&& children.length == 1 
							&& children[0] == index){
						verbose 
							&& console.log('cleanup(..): converting dir to file:', path)
						await fs.promises.rename(path +'/'+ index, path+'.pwiki-bak') 
						await fs.promises.rmdir(path) 
						await fs.promises.rename(path +'.pwiki-bak', path)
						continue } } } }) }

// XXX backup metadata...
// 		- date
// 		- reason
// 		- refs...
// XXX set hidden attribute on backup dir...
// XXX add backup packing...
var backup =
module.backup = {
	// XXX backup config???
	//index: '.index',
	//base: '/.backup',
	//cleanBackup: true,
	//verbose: true,

	//
	// 	.create(<base>[, <options>])
	// 	.create(<base>, '**'[, <options>])
	// 	.create(<base>, '**', Date.timeStamp()[, <options>])
	// 		-> <list>
	//
	// 	.create(<base>, <path>[, <version>][, <options>])
	// 		-> <list>
	//
	// 	.create(<base>, <path>, false[, <options>])
	// 		-> <list>
	//
	// .create(..) and .restore(..) are completely symmetrical.
	//
	// NOTE: backing up ** will include nested backups but will skip the 
	// 		root backup but will ignore the root backup dir...
	//
	// XXX since these are *almost* identical in structure, can we reuse one 
	// 		to implement the other???
	// 		..or can we implement these in a manner similar to "cp A B" vs. "cp B A"???
	create: async function(base, sub='**', version=Date.timeStamp(), options){
		var that = this
		if(typeof(sub) == 'object'){
			options = sub
			sub = '**' }
		if(typeof(version) == 'object'){
			options = version
			version = Date.timeStamp() }
		// options...
		var {index, backup, verbose, recursive, cleanBackup, __batch} = options = getOpts(options)
		recursive = recursive ?? false

		var _backup = backup = 
			version ?
				pwpath.join(backup, version)
				: backup
		backup = 
			pwpath.join(
				base,
				pwpath.relative(pwpath.dirname(sub), backup))

		// ** or * -- backup each file in path...
		if(/[\\\/]*\*\*?$/.test(sub)){
			if(sub.endsWith('**')){
				options.recursive = true }
			options.__batch = true

			if(cleanBackup 
					&& fs.existsSync(backup)){
				verbose
					&& console.log('.create(..): cleaning:', backup)
				await fs.promises.rm(backup, {recursive: true}) }
			sub = sub.replace(/[\\\/]*\*\*?$/, '')
			var b = pwpath.split(_backup)
				.filter(function(p){ 
					return p != '' })
				.shift()
			return fs.promises.readdir(base +'/'+ sub)
				.iter()
				// skip backups...
				.filter(function(file){
					return !file.includes(b) })
				.map(async function(file){
					return await that.create(base, sub +'/'+ file, version, options) })
				// keep only the paths we backed up...
				.filter(function(e){ 
					return !!e }) 

		// backup single page...
		} else {
			var target = pwpath.join(base, sub) 
			var full = _backup[0] == '/'

			// nothing to backup...
			if(!fs.existsSync(target)){
				verbose
					&& console.log('.create(..): target does not exist:', target)
				return }

			var to = full ?
				backup +'/'+ sub
				: backup +'/'+ pwpath.basename(sub)
			var todir = pwpath.dirname(to)

			if(!recursive){
				var stat = await fs.promises.stat(target)
				if(stat.isDirectory()){
					target += '/'+index 
					to += '/'+index 
					// nothing to backup...
					if(!fs.existsSync(target)){
						verbose
							&& !__batch
							&& console.log('.create(..): nothing to backup:', target)
						return } } }

			verbose
				&& console.log('.create(..):', sub, '->', to)
			await fs.promises.mkdir(todir, {recursive: true}) 
			await fs.promises.cp(target, to, {force: true, recursive})
			return to } },
	restore: async function(base, sub, version, options){
		var that = this
		// XXX
		var {index, backup, verbose, recursive, preBackup, __batch} = options = getOpts(options)
		recursive = recursive ?? false

		var _backup = backup = 
			version ?
				pwpath.join(backup, version)
				: backup
		backup = 
			pwpath.join(
				base,
				pwpath.relative(
					pwpath.dirname(sub), 
					backup))

		// check if we can restore...
		if(!fs.existsSync(backup)){
			verbose
				&& console.log('restore(..): no backup version:', version)
			return }

		// XXX should we use the same options...
		preBackup
			&& await this.create(base, sub, options ?? {})

		// ** or * -- backup each file in path...
		// NOTE: when restoring there is no difference between ** and *...
		if(/[\\\/]*\*\*?$/.test(sub)){
			if(sub.endsWith('**')){
				options.recursive = true }
			// restore...
			// NOTE: we have already made a full backup so no need to 
			// 		redo it down the line...
			options.preBackup = false
			options.__batch = true

			sub = sub.replace(/[\\\/]*\*\*?$/, '')
			var to = pwpath.join(base, sub)
			var b = pwpath.split(_backup)
				.filter(function(p){ 
					return p != '' })
				.shift()
			// cleanup...
			// NOTE: we need this stage as the file list we are backing up 
			// 		and the one in the target dir can differ, and a single-page
			// 		.restore(..) will only remove collisions...
			await fs.promises.readdir(base +'/'+ sub)
				.iter()
				// skip backups...
				.filter(function(file){
					return !file.includes(b) })
				.map(async function(file){
					var p = pwpath.join(base, sub, file)
					verbose
						&& console.log('restore(..): removing:', p)
					await fs.promises.rm(p, {recursive: true})
					return p })
			return fs.promises.readdir(backup)
				.iter()
				.map(async function(file){
					return await that.restore(base, sub+'/'+file, version, options) })
				// keep only the paths we backed up...
				.filter(function(e){ 
					return !!e }) 

		// single page...
		} else {
			var index_file = ''
			var full = _backup[0] == '/'
			var source = full ?
				pwpath.join(backup, sub)
				: pwpath.join(backup, pwpath.basename(sub))
			if(!fs.existsSync(source)){
				verbose
					&& console.log('restore(..): source not present in backup:', source)
				return }
			var to = pwpath.join(base, sub)
			if(fs.existsSync(to)){
				var stat = await fs.promises.stat(to)
				if(stat.isDirectory()){
					var f = pwpath.join(to, index)
					if(fs.existsSync(f)){
						verbose
							&& console.log('restore(..): removing:', f)
						await fs.promises.rm(f) }
				} else {
					verbose
						&& console.log('restore(..): removing:', to)
					await fs.promises.rm(to) } }

			if(!recursive){
				// handle dir text...
				var stat = await fs.promises.stat(source)
				if(stat.isDirectory()){
					source += '/'+index
					to += '/'+index 
					if(!fs.existsSync(source)){
						verbose
							&& !__batch
							&& console.log('restore(..): source not present in backup:', source)
						return } } }

			verbose
				&& console.log('restore(..): restoring:', to)
			await fs.promises.cp(source, to, {recursive: true})
			return source } },
	//
	//	Get backup versions...
	//	listbackups(<base>[, <options>])
	//	listbackups(<base>, '*'[, <options>])
	//		-> <list>
	//
	//	Get backup versions containing <path>...
	//	listbackups(<base>, <path>[, <options>])
	//		-> <list>
	//
	list: async function(base, sub, options){
		var that = this
		if(typeof(sub) == 'object'){
			options = sub
			sub = '*' }
		var {backup} = getOpts(options)

		// handle local/global backups...
		var full = backup[0] == '/'
		base = full ?
			pwpath.join(base, backup)
			: pwpath.join(base, pwpath.dirname(sub), backup)
		sub = full ?
			sub
			: pwpath.basename(sub)

		return fs.existsSync(base) ?
			fs.promises.readdir(base)
				.iter()
				.filter(function(version){
					return (sub == '*' || sub == '**')
						|| fs.existsSync(
							pwpath.join(base, version, sub)) }) 
			: [] },

	remove: async function(base, version, options){
		var {backup, verbose} = getOpts(options)
		var target = 
			(version == '*' || version == '**') ?
				pwpath.join(base, backup)
				: pwpath.join(base, backup, version)
		if(fs.existsSync(target)){
			verbose
				&& console.log(`.remove(..): removing:`, target)
			await fs.promises.rm(target, {recursive: true})
			return target } },
	clear: async function(base, options){
		return await this.remove(base, '*', options) }
}


// -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -

// XXX might be a good idea to support ro mode on top level explicitly...
// XXX add monitor API + cache + live mode (auto on when lock detected)...
var FileStoreRO =
module.FileStoreRO = {
	__proto__: base.Store,

	// XXX
	__path__: 'data/fs',
	__pwiki_path__: '/.pwiki/',
	__metadata_path__: '$PWIKI/metadata',

	// XXX should this be "index" or ".index"???
	__directory_text__: '.index',

	expandPWikiPath: function(...path){
		return pwpath.join(...path)
			.replace(/\$PWIKI/, this.__pwiki_path__) },

	// XXX INDEX...
	__paths__: async function(){
		var that = this
		return new Promise(function(resolve, reject){
			glob(pwpath.join(that.__path__, '**/*'))
				.on('end', function(paths){
					Promise.all(paths
							.map(async function(path){
								return await module.exists(path) ?
									decode(path)
										.slice(that.__path__.length)
									: [] }))
						.then(function(paths){
							resolve(paths.flat()) }) }) }) },
	__exists__: async function(path){
		return await module.exists(this.__path__, path, {index: this.__directory_text__}) 
			&& path },
	__get__: async function(path){
		var p = pwpath.join(this.__path__, path)
		var m = this.expandPWikiPath(this.__path__, this.__metadata_path__, path)
		var {atimeMs, mtimeMs, ctimeMs, birthtimeMs} = await fs.promises.stat(p)
		return {
			atime: atimeMs,
			mtime: mtimeMs,
			ctime: ctimeMs,
			text: await module.read(p, {index: this.__directory_text__}),
			...JSON.parse(await module.read(m, {index: this.__directory_text__}) || '{}'),
		} },

	__update__: function(){},
	__delete__: function(){},
}

// XXX add a lock file and prevent multiple adapters from controlling 
// 		one path...
// XXX backup files on write/delete...
var FileStore =
module.FileStore = {
	__proto__: FileStoreRO,

	// XXX
	__path__: 'data/fs',
	__pwiki_path__: '/.pwiki',
	__backup_path__: '$PWIKI/backup',
	__lock_path__: '$PWIKI/lock',

	__directory_text__: '.index',

	// prevent more than one handler to write to a store...
	//
	__clear_lock__: [
		`SIGINT`, 
		`SIGUSR1`, 
		`SIGUSR2`, 
		`SIGTERM`,
		`exit`, 
		// XXX should we handle this??
		// 		...this can be an indicator of inconsistent state...
		//`uncaughtException`, 
	],
	__exit_lock_handler: undefined,
	ensureLock: async function(){
		var that = this
		var lock = this.expandPWikiPath(this.__path__, this.__lock_path__)
		// check lock...
		if(fs.existsSync(lock)){
			if(await module.read(lock) != process.pid){
				throw new Error('attempting to write to a locked store:', this.__path__) }
		// set lock...
		} else {
			await module.update(lock, `${process.pid}`) 
			// keep the pwiki dir hidden on windows...
			if(process.platform == 'win32'){
				cp.execSync('attrib +h '+ 
					this.expandPWikiPath(this.__path__, this.__pwiki_path__)) }
			this.__exit_lock_handler = 
				this.__exit_lock_handler 
					// NOTE: this must be sync as deferred calls might 
					// 		not get a chance to execute...
					?? function(){
						fs.rmSync(lock) }
			this.__clear_lock__.forEach(function(evt){
				process.off(evt, that.__exit_lock_handler)
				process.on(evt, that.__exit_lock_handler) }) }
		return this },

	// XXX do we write all the data or only the .text???
	__update__: async function(path, data, mode='update'){
		this.ensureLock()
		// metadata...
		module.update(
			this.__path__,
			this.expandPWikiPath(this.__metadata_path__, path),
			JSON.stringify(data),
			{index: this.__directory_text__})
		// text...
		return module.update(
			this.__path__, path, 
			data.text, 
			{index: this.__directory_text__}) },
    __delete__: async function(path){
		this.ensureLock()
		// metadata...
		module.clear(
			this.__path__,
			this.expandPWikiPath(this.__metadata_path__, path),
			{index: this.__directory_text__})
		// text...
		return module.clear(
			this.__path__, path, 
			{index: this.__directory_text__}) },

	// specific API...
	cleanup: async function(options={}){
		return module.cleanup(this.__path__, {
			index: this.__directory_text__,
			...options, 
		}) },
	// XXX add explicit versioning???
	backup: async function(path='**', options={}){
		this.ensureLock()
		return backup.create(
			this.__path__, path, 
			{
				index: this.__directory_text__,
				backup: this.expandPWikiPath(this.__backup_path__),
				...options,
			}) },
	restore: async function(path='**', options={}){
		this.ensureLock()
		return backup.restore(
			this.__path__, path, 
			{
				index: this.__directory_text__,
				backup: this.expandPWikiPath(this.__backup_path__),
				...options,
			}) },
}




/**********************************************************************
* vim:set ts=4 sw=4 :                               */ return module })
