
const fs = require('fs/promises')
const fsSync = require('fs')
const path = require('path')
const { exec } = require('child_process')

const { countries, continents } = require('countries-list')

const { setting, setSetting, getSettingCmd } = require('./setting.cjs')
const { num37ToStr, getSmallMemoryFile, getZeroFill, aton6Start, aton4 } = require('./utils.cjs')

const v4db = setting.v4
const v6db = setting.v6
const locFieldHash = setting.locFieldHash
const mainFieldHash = setting.mainFieldHash



/**
 * @param {string} ip - ipv4 or ipv6 formatted address
 * @returns {object|null|Promise} location information
 */
const lookup = (ip) => {



	var isIpv6
	if(ip.includes(':')){
		ip = aton6Start(ip)
		isIpv6 = ip.constructor === BigInt
	} else {
		ip = aton4(ip)
		isIpv6 = false
	}
	const db = isIpv6 ? v6db : v4db
	if(!(ip >= db.firstIp)) return null
	const list = db.startIps
	var fline = 0, cline = db.lastLine, line
	for(;;){
		line = fline + cline >> 1
		if(ip < list[line]){
			if(cline - fline < 2) return null
			cline = line - 1
		} else {
			if(fline === line) {
				if(cline !== line && ip >= list[cline]) {
					line = cline
				}
				break
			}
			fline = line
		}
	}

	if(setting.smallMemory){

		return lineToFile(line, db).then(buffer => {
			var endIp = isIpv6 ? buffer.readBigUInt64LE(0) : buffer.readUInt32LE(0)
			if(ip > endIp) return null
			if(setting.isCountry){
				return setCountryInfo({
					country: buffer.toString('latin1', isIpv6 ? 8 : 4, isIpv6 ? 10 : 6)
				})
			}
			return setCityRecord(buffer, {}, isIpv6 ? 8 : 4)
		})
	}
	if(ip > db.endIps[line]) return null
	if(setting.isCountry){
		return setCountryInfo({
			country: db.mainBuffer.toString('latin1', line * db.recordSize, line * db.recordSize + 2)
		})
	}
	return setCityRecord(db.mainBuffer, {}, line * db.recordSize)
}

/**
 * setup database without reload
 * @param {object} [_setting]
 * @returns {void}
 */
const setupWithoutReload = setSetting

/**
 * clear in-memory database
 */
const clear = () => {
	v4db.startIps = v6db.startIps = v4db.endIps = v6db.endIps = v4db.mainBuffer = v6db.mainBuffer = null
	Region1NameJson = Region2NameJson = TimezoneJson = LocBuffer = CityNameBuffer = EuJson = null
}

var Region1NameJson, Region2NameJson, TimezoneJson, LocBuffer, CityNameBuffer, AreaJson, EuJson
/**
 * reload in-memory database
 * @param {object} [_setting]
 * @param {boolean} [_sync] - sync mode
 * @returns {Promise<void>}
 */
const reload = async (_setting, _sync) => {
	var curSetting = setting
	if(_setting){
		var oldSetting = Object.assign({}, setting)
		setSetting(_setting)
		curSetting = Object.assign({}, setting)
		Object.assign(setting, oldSetting)
	}

	const dataDir = curSetting.dataDir
	const v4 = v4db, v6 = v6db
	var dataFiles = {
		v41:           path.join(dataDir, '4-1.dat'),
		v42:           path.join(dataDir, '4-2.dat'),
		v43:           path.join(dataDir, '4-3.dat'),
		v61:           path.join(dataDir, '6-1.dat'),
		v62:           path.join(dataDir, '6-2.dat'),
		v63:           path.join(dataDir, '6-3.dat'),
		cityLocation: path.join(dataDir, 'location.dat'),
		cityName:     path.join(dataDir, 'name.dat'),
		citySub: 		  path.join(dataDir, 'sub.json')
	}

	var locBuffer, cityNameBuffer, subBuffer
	var buffer41, buffer42, buffer43, buffer61, buffer62, buffer63

	if(_sync){
		buffer41 = fsSync.readFileSync(dataFiles.v41)
		buffer61 = fsSync.readFileSync(dataFiles.v61)
		if(!curSetting.smallMemory){
			buffer42 = fsSync.readFileSync(dataFiles.v42)
			buffer43 = fsSync.readFileSync(dataFiles.v43)
			buffer62 = fsSync.readFileSync(dataFiles.v62)
			buffer63 = fsSync.readFileSync(dataFiles.v63)
		}
		if(curSetting.locFile){
			locBuffer = fsSync.readFileSync(dataFiles.cityLocation)
			if(locFieldHash.city){
				cityNameBuffer = fsSync.readFileSync(dataFiles.cityName)
			}
			if(locFieldHash.region1_name || locFieldHash.region2_name || locFieldHash.timezone || mainFieldHash.area || locFieldHash.eu){
				subBuffer = fsSync.readFileSync(dataFiles.citySub)
			}
		}
	} else {
		var prs = [
			fs.readFile(dataFiles.v41).then(data => buffer41 = data),
			fs.readFile(dataFiles.v61).then(data => buffer61 = data),
		]
		if(!curSetting.smallMemory){
			prs.push(
				fs.readFile(dataFiles.v42).then(data => buffer42 = data),
				fs.readFile(dataFiles.v43).then(data => buffer43 = data),
				fs.readFile(dataFiles.v62).then(data => buffer62 = data),
				fs.readFile(dataFiles.v63).then(data => buffer63 = data)
			)
		}
		if(curSetting.locFile){
			prs.push(fs.readFile(dataFiles.cityLocation).then(data => locBuffer = data))
			if(locFieldHash.city){
				prs.push(fs.readFile(dataFiles.cityName).then(data => cityNameBuffer = data))
			}
			if(locFieldHash.region1_name || locFieldHash.region2_name || locFieldHash.timezone || mainFieldHash.area || locFieldHash.eu){
				prs.push(fs.readFile(dataFiles.citySub).then(data => subBuffer = data))
			}
		}
		await Promise.all(prs)
	}

	if(_setting){
		Object.assign(setting, curSetting)
	}

	v4.startIps = new Uint32Array(buffer41.buffer, 0, buffer41.byteLength >> 2)
	v6.startIps = new BigUint64Array(buffer61.buffer, 0, buffer61.byteLength >> 3)
	if(!curSetting.smallMemory){
		v4.endIps = new Uint32Array(buffer42.buffer, 0, buffer42.byteLength >> 2)
		v4.mainBuffer = buffer43
		v6.endIps = new BigUint64Array(buffer62.buffer, 0, buffer62.byteLength >> 3)
		v6.mainBuffer = buffer63
	}

	v4.lastLine = v4.startIps.length - 1
	v6.lastLine = v6.startIps.length - 1
	v4.firstIp = v4.startIps[0]
	v6.firstIp = v6.startIps[0]
	if(curSetting.isCity){
		LocBuffer = locBuffer
		CityNameBuffer = cityNameBuffer
		if(subBuffer){
			var tmpJson = JSON.parse(subBuffer)
			if(locFieldHash.region1_name) Region1NameJson = tmpJson.region1_name
			if(locFieldHash.region2_name) Region2NameJson = tmpJson.region2_name
			if(locFieldHash.timezone) TimezoneJson = tmpJson.timezone
			if(mainFieldHash.area) AreaJson = tmpJson.area
			if(locFieldHash.eu) EuJson = tmpJson.eu
		}
	}
}

const watchHash = {}
/**
 * Watch database directory.
 * When database file is updated, it reload the database automatically
 * @param {string} [name] - name of watch. If you want to watch multiple directories, you can set different name for each directory
 */
const watchDb = (name = 'ILA') => {
	var watchId = null
	watchHash[name] = fsSync.watch(setting.dataDir, (eventType, filename) => {
		if(!filename.endsWith('.dat')) return;
		if(fsSync.existsSync(path.join(setting.dataDir, filename))) {
			if(watchId) clearTimeout(watchId)
			watchId = setTimeout(reload, 60 * 1000)
		}
	})
}

/**
 * Stop watching database directory
 * @param {string} [name] 
 */
const stopWatchDb = (name = 'ILA') => {
	if(watchHash[name]){
		watchHash[name].close()
		delete watchHash[name]
	}
}

/**
 * Update database and auto reload database
 * @param {object} [_setting] - if you need to update the database with different setting
 * @returns {Promise<boolean>} - true if database is updated, false if no need to update
 */
const updateDb = (_setting) => {



	return new Promise((resolve, reject) => {
		var cmd = 'node ' + path.resolve(__dirname, '..', 'script', 'updatedb.js')
		var arg
		if(_setting){
			var oldSetting = Object.assign({}, setting)
			setSetting(_setting)
			arg = getSettingCmd()
			Object.assign(setting, oldSetting)
		} else {
			arg = getSettingCmd()
		}
		if(!_setting){
			arg += ' ' + 'ILA_SAME_DB_SETTING=true'
		}
		exec(cmd + ' ' + arg, (err, stdout, stderr) => {
			if(err) {
				console.warn(err)
			}
			if(stderr) {
				console.error(stderr)
			}
			if(stdout) {
				console.log(stdout)
			}
			if(err) {
				reject(err)
			} else if(stdout.includes('ERROR TO UPDATE')){
				reject(new Error('ERROR TO UPDATE'))
			} else if(stdout.includes('NO NEED TO UPDATE')){
				resolve(false)
			} else if(stdout.includes('SUCCESS TO UPDATE')){
				setTimeout(() => reload(_setting).then(() => {
					resolve(true)
				}).catch(reject), 1000)
			} else {
				console.log('UNKNOWN ERROR')
				reject(new Error('UNKNOWN ERROR'))
			}
		})
	})
}

/*
-- Remain this code for better performance check
const lineToFile = (line, db) => {
	const [ dir, file, offset ] = getSmallMemoryFile(line, db)
	return new Promise((resolve, reject) => {







		fs.open(path.join(dir, file), 'r').then(fd => {
			const buffer = Buffer.alloc(db.recordSize)
			fd.read(buffer, 0, db.recordSize, offset).then(() => {
				fd.close().catch(reject)
				resolve(buffer)
			}).catch(reject)
		}).catch(reject)
	})
}
*/
const lineToFile = async (line, db) => {
	const [ dir, file, offset ] = getSmallMemoryFile(line, db)
	const fd = await fs.open(path.join(setting.dataDir, dir, file), 'r')
	const buffer = Buffer.alloc(db.recordSize)
	await fd.read(buffer, 0, db.recordSize, offset)
	fd.close().catch(console.warn)
	return buffer
}

const setCityRecord = (buffer, geodata, offset) => {
	var locId
	if(setting.locFile){
		locId = buffer.readUInt32LE(offset)
		offset += 4
	}
	if(mainFieldHash.latitude){
		geodata.latitude = buffer.readInt32LE(offset) / 10000
		offset += 4
	}
	if(mainFieldHash.longitude){
		geodata.longitude = buffer.readInt32LE(offset) / 10000
		offset += 4
	}
	if(mainFieldHash.postcode){
		var postcode2 = buffer.readUInt32LE(offset)
		var postcode1 = buffer.readInt8(offset + 4)
		if (postcode2) {
			var postcode, tmp
			if(postcode1 < -9){
				tmp = (-postcode1).toString()
				postcode = postcode2.toString(36)
				postcode = getZeroFill(postcode.slice(0, -tmp[1]), tmp[0]-0) + '-' + getZeroFill(postcode.slice(-tmp[1]), tmp[1]-0)
			} else if(postcode1 < 0){
				postcode = getZeroFill(postcode2.toString(36), -postcode1)
			} else if(postcode1 < 10){
				postcode = getZeroFill(postcode2.toString(10), postcode1)
			} else if(postcode1 < 72){
				postcode1 = String(postcode1)
				postcode = getZeroFill(postcode2.toString(10), (postcode1[0]-0) + (postcode1[1]-0))
				postcode = postcode.slice(0, postcode1[0]-0) + '-' + postcode.slice(postcode1[0]-0)
			} else {
				postcode = postcode1.toString(36).slice(1) + postcode2.toString(36)
			}
			geodata.postcode = postcode.toUpperCase()
		}
		offset += 5
	}
	if(mainFieldHash.area){
		geodata.area = AreaJson[buffer.readUInt8(offset)]
		offset += 1
	}

	if(locId){
		var locOffset = (locId-1) * setting.locRecordSize
		if(locFieldHash.country){
			geodata.country = LocBuffer.toString('utf8', locOffset, locOffset += 2)
			if(locFieldHash.eu){
				geodata.eu = EuJson[geodata.country]
			}
		}
		if(locFieldHash.region1){
			var region1 = LocBuffer.readUInt16LE(locOffset)
			locOffset += 2
			if(region1 > 0) geodata.region1 = num37ToStr(region1)
		}
		if(locFieldHash.region1_name){
			var region1_name = LocBuffer.readUInt16LE(locOffset)
			locOffset += 2
			if(region1_name > 0) geodata.region1_name = Region1NameJson[region1_name]
		}
		if(locFieldHash.region2){
			var region2 = LocBuffer.readUInt16LE(locOffset)
			locOffset += 2
			if(region2 > 0) geodata.region2 = num37ToStr(region2)
		}
		if(locFieldHash.region2_name){
			var region2_name = LocBuffer.readUInt16LE(locOffset)
			locOffset += 2
			if(region2_name > 0) geodata.region2_name = Region2NameJson[region2_name]
		}
		if(locFieldHash.metro){
			var metro = LocBuffer.readUInt16LE(locOffset)
			locOffset += 2
			if(metro > 0) geodata.metro = metro
		}
		if(locFieldHash.timezone){
			var timezone = LocBuffer.readUInt16LE(locOffset)
			locOffset += 2
			if(timezone > 0) geodata.timezone = TimezoneJson[timezone]
		}
		if(locFieldHash.city){
			var city = LocBuffer.readUInt32LE(locOffset)
			locOffset += 4
			if(city > 0){
				var start = city >>> 8
				geodata.city = CityNameBuffer.toString('utf8', start, start + (city & 255))
			}
		}
	}
	return setCountryInfo(geodata)
}
const setCountryInfo = (geodata) => {
	if(setting.addCountryInfo){
		var h = countries[geodata.country]
		geodata.country_name = h.name
		geodata.country_native = h.native
		geodata.continent = h.continent
		geodata.continent_name = continents[h.continent]
		geodata.capital = h.capital
		geodata.phone = h.phone
		geodata.currency = h.currency
		geodata.languages = h.languages
	}
	return geodata
}

reload(undefined, true)

module.exports={lookup:lookup,setupWithoutReload:setupWithoutReload,clear:clear,reload:reload,watchDb:watchDb,stopWatchDb:stopWatchDb,updateDb:updateDb}