const mongoose = require('mongoose');	 
const GoogleMaps = require('@google/maps').createClient({
	key: 'AIzaSyCvpBGztvxtRUNigOW9f0GXVRWlukJZsps'
});

var logger = require('../modules/logger'); 

const Schema = mongoose.Schema;	 

const GeocacheSchema = new Schema({
	key: {
		type: String,
		required: true,
		unique: true
	},
	formatted: {
		type: String,
		required: true
	},
	loc: {
		type: {
			type: String,
			default: 'Point'
		},
		coordinates: [{
			type: Number,
			default: [0, 0]
		}]
	},
	georesult: { 
		type: Schema.Types.Mixed 
	}
});

const Conversion = {
	kilometersToMeters: (distance) => {
		return parseFloat(distance * 1000);
	},

	kilometersToMiles: (distance) => {
		return parseFloat(distance / 1.60934);
	},

	kilometersToNauticalMiles: (distance) => {
		return parseFloat(distance * 0.539957);
	},

	metersToKilometers: (distance) => {
		return parseFloat(distance / 1000);
	},
	
	metersToMiles: (distance) => {
		return parseFloat(distance / 1609.34);
	},
	
	milesToKilometers: (distance) => {
		return parseFloat(distance * 1.60934);
	},
	
	milesToMeters: (distance) => {
		return parseFloat(distance * 1609.34);
	},

	milesToNauticalMiles: (distance) => {
		return parseFloat(distance * 0.868976);
	},
	
	nauticalMilesToMeters: (distance) => {
		return parseFloat(distance / 0.868976);
	},
	
	nauticalMilesToMiles: (distance) => {
		return parseFloat(distance / 0.868976);
	},
	
	nauticalMilesToKilometers: (distance) => {
		return parseFloat(distance * 1.852000674128);
	}
};

GeocacheSchema.index({ name: 1, loc: '2dsphere' });

const GeocacheModel = mongoose.model('geocache', GeocacheSchema);

/*function distanceBetween (geoJSON1, geoJSON2, unit = 'mi') {
	var radlat1 = Math.PI * geoJSON1.coordinates[1]/180;
	var radlat2 = Math.PI * geoJSON2.coordinates[1]/180;
	var theta = geoJSON1.coordinates[0] - geoJSON2.coordinates[0];
	var radtheta = Math.PI * theta/180;
	var dist = Math.sin(radlat1) * Math.sin(radlat2) + Math.cos(radlat1) * Math.cos(radlat2) * Math.cos(radtheta);
	dist = Math.acos(dist);
	dist = dist * 180/Math.PI;
	dist = dist * 60 * 1.1515; // miles between
	if (unit == "km") { dist = Conversion.metersToKilometers(Conversion.milesToMeters(dist)); }
	if (unit == "m") { dist = dist * 1.609344; }
	if (unit == "n") { dist = dist * 0.8684; }
	return dist;
}*/

function queryGeodataApi (query, callback) {
	GoogleMaps.geocode({
		address: query
	}, function(err, response) {
		if (err) {
			console.error('[GeocacheModel<<getGeoData>>] Address Geocoding Error', { address: query, response: response });
			callback(null, err, null);
		}
		
		if (response.json && Array.isArray(response.json.results)) {
			var data = {
				key: sanitizeNameForKey(query),
				formatted: response.json.results[0].formatted_address,
				georesult: response.json.results[0],
				loc: {
					type: 'Point',
					coordinates: [ 
						response.json.results[0].geometry.location.lng, 
						response.json.results[0].geometry.location.lat 
					]
				}
			};			
			callback(null, null, data);
		}
	});
}

function sanitizeNameForKey (name) {
	var key = name.replace(/[.,/#!$%^&*;:{}=\-_`~()]/g,'');
	key = key.replace(/\s{2,}/g,' ');
	key = key.trim();
	key = key.toLowerCase();
	return key;
}

module.exports = {	

	create: (e, geodata) => {
		var cb = typeof e === 'object' && e.emit ? e.emit.bind(e) : e;
		const promise = new Promise((resolve, reject) => {	
			var geocacheInstance = new GeocacheModel(geodata);
			geocacheInstance.save((err, result) => {
				if (err) {
					reject(err);
				}

				if (result) {
					resolve(result);
				}
			});
		});

		promise.then((result) => {
			cb('create', null, result);
		})
		.catch((err) => {
			cb('create', err, null);
		});
	},
	
	conversion: Conversion,
	
	find: (e, searchText) => {
		var cb = typeof e === 'object' && e.emit ? e.emit.bind(e) : e;
		const promise = new Promise((resolve, reject) => {	
			GeocacheModel.findOne({ key: sanitizeNameForKey(searchText) }, (err, result) => {
				if (err) {
					reject(err);
				}

				if (result) {
					resolve(result);
				}
			});
		});

		promise.then((result) => {
			cb('find', null, result);
		})
		.catch((err) => {
			cb('find', err, null);
		});
	},
	
	findLike: (e, searchText) => {
		var cb = typeof e === 'object' && e.emit ? e.emit.bind(e) : e;
		const promise = new Promise((resolve, reject) => {	
			GeocacheModel.find({ key: new RegExp('.*' + sanitizeNameForKey(searchText) + '.*', "i") }, (err, result) => {
				if (err) {
					reject(err);
				}

				if (result) {
					resolve(result);
				}
			});
		});

		promise.then((result) => {
			cb('findLike', null, result);
		})
		.catch((err) => {
			cb('findLike', err, null);
		});
	},
	
	findNear: (e, lng, lat, distance) => {
		var cb = typeof e === 'object' && e.emit ? e.emit.bind(e) : e;
		var point = {
			type: 'Point',
			coordinates: [ lng, lat ]
		};
		
		var opts = {
			spherical: true,
			maxDistance: Conversion.milesToMeters(distance)
		};		

		const promise = new Promise((resolve, reject) => {	
			GeocacheModel.geoNear(point, opts, (err, results) => {
				if (err) {
					reject(err);
				}

				if (results) {
					resolve(results);
				}
			});
		});

		promise.then((result) => {
			cb('findNear', null, result);
		})
		.catch((err) => {
			cb('findNear', err, null);
		});	 
	},
	
	getGeo: (e, id) => {
		var cb = typeof e === 'object' && e.emit ? e.emit.bind(e) : e;
	
		const promise = new Promise((resolve, reject) => {	
			GeocacheModel.findById(id, (err, result) => {
				if (err) {
					reject(err);
				}
				
				if (result) {
					resolve(result);
				}
			});
		});

		promise.then((result) => {
			cb('getGeos', null, result);
		})
		.catch((err) => {
			cb('getGeos', err, null);
		});			 
	},
	
	getGeos: (e) => {
		var cb = typeof e === 'object' && e.emit ? e.emit.bind(e) : e;
	
		const promise = new Promise((resolve, reject) => {	
			GeocacheModel.find({}, (err, results) => {
				if (err) {
					reject(err);
				}
				
				if (results) {
					resolve(results);
				}
			});
		});

		promise.then((result) => {
			cb('getGeos', null, result);
		})
		.catch((err) => {
			cb('getGeos', err, null);
		});			 
	},
	
	getGeoJSON: (e, searchText) => {
		var cb = typeof e === 'object' && e.emit ? e.emit.bind(e) : e;
	
		const promise = new Promise((resolve, reject) => {	
			queryGeodataApi(searchText, (f, err, geodata) => {
				if (err) {
					reject(err);
				}
				
				if (geodata) {
					resolve(geodata.loc);
				}
			});
		});

		promise.then((result) => {
			cb('getGeoJSON', null, result);
		})
		.catch((err) => {
			cb('getGeoJSON', err, null);
		});			 
	},
	
	getGeoJSONFromCache: (e, searchText) => {
		var cb = typeof e === 'object' && e.emit ? e.emit.bind(e) : e;

		const promise = new Promise((resolve, reject) => {	
			GeocacheModel.findOne({ key: sanitizeNameForKey(searchText) }, (err, result) => {
				if (err || !result) {
					queryGeodataApi(searchText, (f, err, geodata) => {
						if (err) {
							reject(err);
						}
					
						if (geodata) {
							let geocacheInstance = new GeocacheModel(geodata);
							geocacheInstance.save((err, result) => {
								if (err) {
									logger.error('[Geocache::getGeoJSON] There was an error creating the GeoJSON entry.', { err: err });
								}
								
								logger.debug()
							});
							resolve(geodata.loc);
						}
					});
				} else if (result) {
					resolve(result.loc);
				}
			});
		});

		promise.then((result) => {
			cb('getGeoJSONFromCache', null, result);
		})
		.catch((err) => {
			cb('getGeoJSONFromCache', err, null);
		});			 
	},
	
	populateFormatted: (e) => {
		var cb = typeof e === 'object' && e.emit ? e.emit.bind(e) : e;
		const promise = new Promise((resolve, reject) => {	
			GeocacheModel.find({}, (err, results) => {
				if (err) {
					reject(err);
				}

				if (results) {
					for (let i = 0; i < results.length; i++) {
						if (!results[i].formatted) {
							results[i].formatted = results[i].georesult.formatted_address;
							GeocacheModel.findByIdAndUpdate(results[i]._id, { $set: results[i] }, (err, result) => {
								if (err) logger.error('There was an error populating the geocache formatted address.');
								if (result) logger.log('The geocache entry was updated');
							});						
						}
					}
				
					resolve({ status: 'OK', message: 'The geocache entries have been updated.'});
				}
			});
		});

		promise.then((result) => {
			cb('populateFormattedAddresses', null, result);
		})
		.catch((err) => {
			cb('populateFormattedAddresses', err, null);
		});
	},
	
	populateKeys: (e) => {
		var cb = typeof e === 'object' && e.emit ? e.emit.bind(e) : e;
		const promise = new Promise((resolve, reject) => {	
			GeocacheModel.find({}, (err, results) => {
				if (err) {
					reject(err);
				}

				if (results) {
					for (let i = 0; i < results.length; i++) {
						if (!results[i].key) {
							results[i].key = sanitizeNameForKey(results[i].name);
							GeocacheModel.findByIdAndUpdate(results[i]._id, { $set: results[i] }, (err, result) => {
								if (err) logger.error('There was an error populating the geocache entry key.');
								if (result) logger.log('The geocache entry was updated');
							});						
						}
					}
				
					resolve({ status: 'OK', message: 'The geocache entries have been updated.'});
				}
			});
		});

		promise.then((result) => {
			cb('populateKeys', null, result);
		})
		.catch((err) => {
			cb('populateKeys', err, null);
		});
	},
	
	update: (e, id, geodata) => {
		var cb = typeof e === 'object' && e.emit ? e.emit.bind(e) : e;
		const promise = new Promise((resolve, reject) => {	
			GeocacheModel.findByIdAndUpdate(id, { $set: geodata }, (err, result) => {
				if (err) {
					reject(err);
				}

				if (result) {
					resolve(result);
				}
			});
		});

		promise.then((result) => {
			cb('update', null, result);
		})
		.catch((err) => {
			cb('update', err, null);
		});
	}
};