export namespace VanillaTilt {
	/**
	 * Options which configures the tilting
	 */
	export interface TiltOptions {
		/**
		 * Reverse the tilt direction
		 */
		reverse?: boolean;
		/**
		 * Max tilt rotation (degrees)
		 */
		max?: number;
		/**
		 * Transform perspective, the lower the more extreme the tilt gets.
		 */
		perspective?: number;
		/**
		 * 2 = 200%, 1.5 = 150%, etc..
		 */
		scale?: number;
		/**
		 * Speed of the enter/exit transition
		 */
		speed?: number;
		/**
		 * Set a transition on enter/exit.
		 */
		transition?: boolean;
		/**
		 * What axis should be disabled. Can be X or Y.
		 */
		axis?: null | "x" | "y";
		/**
		 * If the tilt effect has to be reset on exit.
		 */
		reset?: boolean;
		/**
		 * Easing on enter/exit.
		 */
		easing?: string;
		/**
		 * Added (@julien-c)
		 */
		glare?: boolean;
		'max-glare'?: number;
	}
	
	export interface TiltValues {
		/**
		 * The current tilt on the X axis
		 */
		tiltX: number;
		/**
		 * The current tilt on the Y axis
		 */
		tiltY: number;
		/**
		 * The current percentage on the X axis
		 */
		percentageX: number;
		/**
		 * The current percentage on the Y axis
		 */
		percentageY: number;
	}
	
	export interface HTMLVanillaTiltElement extends HTMLElement {
		vanillaTilt: VanillaTilt
	}
}


export class VanillaTilt {
	width:  number | null;
	height: number | null;
	left:   number | null;
	top:    number | null;
	element:  VanillaTilt.HTMLVanillaTiltElement;
	settings: VanillaTilt.TiltOptions;
	reverse : -1 | 1;
	glare: boolean;
	glarePrerender: boolean;
	transitionTimeout: number | null;
	updateCall:        number | null;
	glareElementWrapper: HTMLElement;
	glareElement: HTMLElement;
	updateBind: () => void;
	resetBind:  () => void;
	onMouseEnterBind: (e: Event) => void;
	onMouseMoveBind:  (e: Event) => void;
	onMouseLeaveBind: (e: Event) => void;
	event: MouseEvent;
	
	constructor(element, settings: VanillaTilt.TiltOptions = {}) {
		if (!(element instanceof Node)) {
			throw ("Can't initialize VanillaTilt because " + element + " is not a Node.");
		}
		
		this.width = null;
		this.height = null;
		this.left = null;
		this.top = null;
		this.transitionTimeout = null;
		this.updateCall = null;
		
		this.updateBind = this.update.bind(this);
		this.resetBind  = this.reset.bind(this);
		
		this.element  = element as VanillaTilt.HTMLVanillaTiltElement;
		this.settings = this.extendSettings(settings);
		
		this.reverse = this.settings.reverse ? -1 : 1;
		
		this.glare = this.isSettingTrue(this.settings.glare);
		this.glarePrerender = this.isSettingTrue(this.settings["glare-prerender"]);
		
		if (this.glare) {
			this.prepareGlare();
		}
		
		this.addEventListeners();
	}
	
	isSettingTrue(setting) {
		return setting === "" || setting === true || setting === 1;
	}
	
	addEventListeners() {
		this.onMouseEnterBind   = this.onMouseEnter.bind(this);
		this.onMouseMoveBind    = this.onMouseMove.bind(this);
		this.onMouseLeaveBind   = this.onMouseLeave.bind(this);
		this.onWindowResizeBind = this.onWindowResizeBind.bind(this);
	
		this.element.addEventListener("mouseenter", this.onMouseEnterBind);
		this.element.addEventListener("mousemove",  this.onMouseMoveBind);
		this.element.addEventListener("mouseleave", this.onMouseLeaveBind);
		if (this.glare) {
			window.addEventListener("resize", this.onWindowResizeBind);
		}
	}
	
	
	onMouseEnter(event) {
		this.updateElementPosition();
		(<any>this.element.style).willChange = "transform";
		this.setTransition();
	}
	
	onMouseMove(event) {
		if (this.updateCall !== null) {
			cancelAnimationFrame(this.updateCall);
		}
		
		this.event = event;
		this.updateCall = requestAnimationFrame(this.updateBind);
	}
	
	onMouseLeave(event) {
		this.setTransition();
		
		if (this.settings.reset) {
			requestAnimationFrame(this.resetBind);
		}
	}
	
	reset() {
		this.event = {
			pageX: this.left! + this.width!  / 2,
			pageY: this.top!  + this.height! / 2
		} as MouseEvent;
		
		this.element.style.transform = "perspective(" + this.settings.perspective + "px) " +
			"rotateX(0deg) " +
			"rotateY(0deg) " +
			"scale3d(1, 1, 1)"
		;
		
		if (this.glare) {
			this.glareElement.style.transform = 'rotate(180deg) translate(-50%, -50%)';
			this.glareElement.style.opacity = '0';
		}
	}
	
	getValues() {
		let x = (this.event.clientX - this.left!) / this.width!;
		let y = (this.event.clientY - this.top!) / this.height!;
	
		x = Math.min(Math.max(x, 0), 1);
		y = Math.min(Math.max(y, 0), 1);
	
		let tiltX = (this.reverse * (this.settings.max! / 2 - x * this.settings.max!)).toFixed(2);
		let tiltY = (this.reverse * (y * this.settings.max! - this.settings.max! / 2)).toFixed(2);
		let angle = Math.atan2(this.event.clientX - (this.left! + this.width! / 2), -(this.event.clientY - (this.top! + this.height! / 2))) * (180 / Math.PI);
		
		return {
			tiltX: tiltX,
			tiltY: tiltY,
			percentageX: x * 100,
			percentageY: y * 100,
			angle: angle
		};
	}
	
	updateElementPosition() {
		let rect = this.element.getBoundingClientRect();
		
		this.width = this.element.offsetWidth;
		this.height = this.element.offsetHeight;
		this.left = rect.left;
		this.top = rect.top;
	}
	
	update() {
		const values = this.getValues();
		
		this.element.style.transform = [
			"perspective(" + this.settings.perspective + "px) ",
			"rotateX(" + (this.settings.axis === "x" ? 0 : values.tiltY) + "deg) ",
			"rotateY(" + (this.settings.axis === "y" ? 0 : values.tiltX) + "deg) ",
			"scale3d(" + this.settings.scale + ", " + this.settings.scale + ", " + this.settings.scale + ")",
		].join(" ");
		
		if (this.glare) {
			this.glareElement.style.transform = `rotate(${values.angle}deg) translate(-50%, -50%)`;
			this.glareElement.style.opacity   = `${values.percentageY * this.settings["max-glare"]! / 100}`;
		}
		
		this.element.dispatchEvent(new CustomEvent("tiltChange", {
			"detail": values
		}));
		
		this.updateCall = null;
	}
	
	/**
	 * Appends the glare element (if glarePrerender equals false)
	 * and sets the default style
	 */
	prepareGlare() {
		// If option pre-render is enabled we assume all html/css is present for an optimal glare effect.
		if (!this.glarePrerender) {
			// Create glare element
			const jsTiltGlare = document.createElement("div");
			jsTiltGlare.classList.add("js-tilt-glare");
			
			const jsTiltGlareInner = document.createElement("div");
			jsTiltGlareInner.classList.add("js-tilt-glare-inner");
			
			jsTiltGlare.appendChild(jsTiltGlareInner);
			this.element.appendChild(jsTiltGlare);
		}
		
		this.glareElementWrapper = this.element.querySelector(".js-tilt-glare") as HTMLElement;
		this.glareElement = this.element.querySelector(".js-tilt-glare-inner") as HTMLElement;
		
		if (this.glarePrerender) {
			return ;
		}
		
		Object.assign(this.glareElementWrapper.style, {
			"position": "absolute",
			"top": "0",
			"left": "0",
			"width": "100%",
			"height": "100%",
			"overflow": "hidden",
			'pointer-events': 'none',
		});
	
		Object.assign(this.glareElement.style, {
			'position': 'absolute',
			'top': '50%',
			'left': '50%',
			'pointer-events': 'none',
			'background-image': `linear-gradient(0deg, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 100%)`,
			'width': `${this.element.offsetWidth * 2}px`,
			'height': `${this.element.offsetWidth * 2}px`,
			'transform': 'rotate(180deg) translate(-50%, -50%)',
			'transform-origin': '0% 0%',
			'opacity': '0',
		});
	}
	
	updateGlareSize() {
		Object.assign(this.glareElement.style, {
			'width': `${this.element.offsetWidth * 2}`,
			'height': `${this.element.offsetWidth * 2}`,
		});
	}
	
	onWindowResizeBind() {
		this.updateGlareSize();
	}
	
	setTransition() {
		if (this.transitionTimeout) {
			clearTimeout(this.transitionTimeout);
		}
		// this.element.style.transition = `${this.settings.speed}ms ${this.settings.easing}`;
		/// From openai:
		this.element.style.transition = `transform .4s cubic-bezier(0,0,.2,1)`;
		if (this.glare) {
			this.glareElement.style.transition = `opacity ${this.settings.speed}ms ${this.settings.easing}`;
		}
		
		this.transitionTimeout = setTimeout(() => {
			this.element.style.transition = "";
			if (this.glare) {
				this.glareElement.style.transition = "";
			}
		}, this.settings.speed);
		
	}
	
	extendSettings(settings) {
		let defaultSettings = {
			reverse: false,
			max: 35,
			perspective: 1000,
			easing: "cubic-bezier(.03,.98,.52,.99)",
			scale: "1",
			speed: "300",
			transition: true,
			axis: null,
			glare: false,
			"max-glare": 1,
			"glare-prerender": false,
			reset: true,
		};
		
		let newSettings = {};
		for (var property in defaultSettings) {
			if (property in settings) {
				newSettings[property] = settings[property];
			} else if (this.element.hasAttribute("data-tilt-" + property)) {
				let attribute = this.element.getAttribute("data-tilt-" + property);
				try {
					newSettings[property] = JSON.parse(<any>attribute);
				} catch (e) {
					newSettings[property] = attribute;
				}
			} else {
				newSettings[property] = defaultSettings[property];
			}
		}
		
		return newSettings;
	}
	
	static init(elements, settings: VanillaTilt.TiltOptions = {}) {
		if (elements instanceof Node) {
			elements = [elements];
		}
		
		if (elements instanceof NodeList) {
			elements = [].slice.call(elements);
		}
		
		if (!(elements instanceof Array)) {
			return ;
		}
		
		elements.forEach((element) => {
			if (!("vanillaTilt" in element)) {
				element.vanillaTilt = new VanillaTilt(element, settings);
			}
		});
	}
}