import { Utils } from '../lib/Utils';
import db from '../models/Database';
import * as mongodb from 'mongodb';
import { ObjectID, Cursor } from 'mongodb';


export class ObjectFactory {
	static create(__type: any, o: any): any {
		if (__type.fromObject) {
			return __type.fromObject(o);
		}
		const newObject = new __type();
		return Object.assign(newObject, o);
	}
}


export class MongoObject {
	protected static __type;							// The current class
	protected static __collectionName: string;			// The Mongo collection name
	protected static __idField: string = "_id";			// Default id used to findOne
	protected static __wlistJsonAttrs: string[] = [];	// Whitelist of attributes to serialize.
	
	// Json serialization.
	
	private toJsonRepr(): object {
		return Utils.pick(this, (<any>this.constructor).__wlistJsonAttrs);
	}
	
	toJson(): string {
		return JSON.stringify(this.toJsonRepr());
	}
	
	
	/// Find family of methods
	
	static async findOne<T>(id: string | ObjectID | mongodb.FilterQuery<T>, options?: mongodb.FindOneOptions): Promise<T | null> {
		const q = (typeof id === 'string' || id instanceof ObjectID)
			? { [this.__idField]: id }
			: id;
		
		const o = await db.collection(this.__collectionName).findOne(q, options);
		if (o) {
			return ObjectFactory.create(this.__type, o);
		}
		return null;
	}
	
	static async findOneAndUpdate<T>(filter: mongodb.FilterQuery<T>, update: Object, options?: mongodb.FindOneAndReplaceOption): Promise<T | null> {
		const o = await db.collection(this.__collectionName).findOneAndUpdate(filter, update, options);
		if (o && o.value) {
			return ObjectFactory.create(this.__type, o.value);
		}
		return null;
	}
	
	static find<T>(query: mongodb.FilterQuery<T> = {}, options?: mongodb.FindOneOptions): HfCursor<T> {
		const cursor = db.collection(this.__collectionName).find(query, options);
		return HfCursor.cast<T>(cursor, this.__type);
	}
}



export class HfCursor<T> extends Cursor<T> {
	protected __type;
	
	static cast<T>(cursor: Cursor<T>, type: any): HfCursor<T> {
		// “The use of __proto__ is controversial, and has been discouraged.”
		// see stackoverflow.com/a/32186367
		// see developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/proto
		(<any>cursor).__proto__ = HfCursor.prototype;
		(<any>cursor).__type = type;
		return cursor as HfCursor<T>;
	}
	
	toArray(): Promise<T[]> {
		return super.toArray().then((objs) => {
			return objs.map((o) => {
				return ObjectFactory.create(this.__type, o);
			});
		});
	}
	
	forEach(__iterator: mongodb.IteratorCallback<T>, __callback: mongodb.EndCallback = () => {}) {
		super.forEach((o) => {
			const newObject = ObjectFactory.create(this.__type, o);
			__iterator(newObject);
		}, __callback);
	}
	
	
	
	
	on(event: string, listener: (...args) => void): this {
		if (event === 'data') {
			super.on('data', (o) => {
				const newObject = ObjectFactory.create(this.__type, o);
				listener(newObject);
			});
		}
		else {
			super.on(event, listener);
		}
		return this;
	}
	
	once(event: string, listener: (...args) => void): this {
		if (event === 'data') {
			super.once('data', (o) => {
				const newObject = ObjectFactory.create(this.__type, o);
				listener(newObject);
			});
		}
		else {
			super.once(event, listener);
		}
		return this;
	}
	
	//
	// Below: cursor methods are only here to make Typescript
	// know that they return the HfCursor object itself.
	// (We have checked that the mongo driver does the right thing underneath)
	//
	
	limit(value: number): HfCursor<T> {
		return super.limit(value) as HfCursor<T>;
	}
	
	skip(value: number): HfCursor<T> {
		return super.skip(value) as HfCursor<T>;
	}
	
	sort(keyOrList: string | Object[] | Object, direction?: number): HfCursor<T> {
		return super.sort(keyOrList, direction) as HfCursor<T>;
	}
	
	stream(options?: { transform?: Function }): HfCursor<T> {
		return super.stream(options) as HfCursor<T>;
	}
}