define([
	"dojo/_base/kernel",
	"dojo/_base/array",
	"dojo/_base/connect",
	"dojo/_base/event",
	"dojo/dom-class",
	"dojo/dom-construct",
	"dojo/dom-style",
	"./_css3"
], function(kernel, array, connect, event, domClass, domConstruct, domStyle, css3){
	// module:
	//		dojox/mobile/pageTurningUtils
	
	kernel.experimental("dojox.mobile.pageTurningUtils");

	return function(){
		// summary:
		//		Utilities to provide page turning effects just like turning a real book.
		// example:
		// |	require([
		// |		"dojo/ready",
		// |		"dojox/mobile/pageTurningUtils"
		// |	], function(ready, pageTurningUtils){
		// |		var utils = new pageTurningUtils();
		// |		ready(function(){
		// |			utils.init(300, 400); // Specify width and height by pixels
		// |			utils.initCatalog(document.getElementById("catalog"));
		// |		});
		// |	);
		// |	<div id="catalog">
		// |		<div id="page1">
		// |			<div id="front1"><img src="img1.png"></div>
		// |			<div id="back1"><img src="img2.png"></div>
		// |		</div>
		// |		<div id="page2">
		// |			<div id="front2"><img src="img3.png"></div>
		// |			<div id="back2"><img src="img4.png"></div>
		// |		</div>
		// |		<div id="page3">
		// |			<div id="front3"><img src="img5.png"></div>
		// |			<div id="back3"></div>
		// |		</div>
		// |	</div>

		this.w = 0;
		this.h = 0;
		this.turnfrom = "top";
		this.page = 1;
		this.dogear = 1.0;
		this.duration = 2;
		this.alwaysDogeared = false;

		/* Internal properties */
		this._styleParams = {};
		this._catalogNode = null;
		this._currentPageNode = null;
		this._transitionEndHandle = null;

		this.init = function(/*int*/w, /*int*/h, /*String?*/turnfrom, 
							/*int?*/page, /*Number?*/dogear, /*Number?*/duration,
							/*Boolean?*/alwaysDogeared){
			// summary:
			//		Sets property values necessary for calculating the style parameters
			//		for page positioning and page turning effects.
			// w: int
			//		The width of each page by pixels. You cannot specify it by percentage.
			// h: int
			//		The height of each page by pixels. You cannot specify it by percentage.
			// turnfrom: String?
			//		Specifies from which side/corner the page turning starts. 
			//		You can choose from "top", "bottom" or "left". Defaults to "top".
			//		If "top", each page is turned from top-right corner of the page.
			//		If "bottom", each page is turned from bottom-right corner of the page.
			//		And if "left", each page is turned from top-left corner of the page.
			//		The page is shown as dog-eared except the case of "bottom".
			// page: int?
			//		The number of pages shown in the screen at a time.
			//		This parameter should be either of 1 or 2. Defaults to 1.
			//		If 1, the only one side of two facing pages are shown. 
			//		If 2, the two facing pages are shown at a time.
			// dogear: Number?
			//		The ratio of actual dog-ear width to the maximum dog-ear width which is
			//		11 percent of the page width (= 0.11 * w).
			//		This parameter should be a float number between 0 and 1. Defaults to 1.
			//		The actual dog-ear width is calculated by the following formula:
			// |		0.11 * w * dogear.
			//		This parameter is ignored if "bottom" is specified to turnfrom parameter.
			// duration: Number?
			//		The duration of page turning animations by seconds. (ex. 1.5, 3, etc)
			//		Defaults to 2.
			// alwaysDogeared: Boolean?
			//		Specifies whether all pages are always dog-eared or not.
			//		If true, all pages are always dog-eared.
			//		If false, only the current page is dog-eared while the others are not.
			//		This parameter is ignored if "bottom" is specified to turnfrom parameter.
			
			// Set property values
			this.w = w;
			this.h = h;
			this.turnfrom = turnfrom ? turnfrom : this.turnfrom;
			this.page = page ? page : this.page;
			this.dogear = typeof dogear !== 'undefined' ? dogear : this.dogear;
			this.duration = typeof duration !== 'undefined' ? duration : this.duration;
			this.alwaysDogeared = typeof alwaysDogeared !== 'undefined' ? alwaysDogeared : this.alwaysDogeared;
			
			if(this.turnfrom === "bottom"){ // dog-ear is not supported if using "bottom"
				this.alwaysDogeared = true;
			}
			// Calculate style parameters for page positioning and page turning effects
			this._calcStyleParams();
		};

		this._calcStyleParams = function(){
			// tags:
			//		private
			var tan58 = Math.tan(58 * Math.PI/180),
				cos32 = Math.cos(32 * Math.PI/180),
				sin32 = Math.sin(32 * Math.PI/180),
				tan32 = Math.tan(32 * Math.PI/180),
				w = this.w,
				h = this.h,
				page = this.page,
				turnfrom = this.turnfrom,
				params = this._styleParams;
			
			// Calculate each div size and position based on the page turning algorithm
			//	 fw: frontWidth, fh: frontHeight, dw: dogear, cx: posX, cy: posY,
			//	 dx: dogearX, dy: dogearY, fy:actualPagePos
			var fold = w * tan58,
				Q = fold,
				fw = Q * sin32 + Q * cos32 * tan58,
				fh = fold + w + w/tan58,
				dw = w * 0.11 * this.dogear,
				pw = w - dw,
				base = pw * cos32,
				cx, cy, dx, dy, fy; // These params depend on the types of "turnfrom" parameter
			
			switch(this.turnfrom){ // TODO Should separate each case as a plugin?
			case "top":
				cx = fw - base;
				cy = base * tan58;
				dx = fw - dw;
				dy = cy + pw/tan58 - 7;
				fy = cy/cos32;
				
				params.init = {
					page: css3.add({
						top: -fy + "px",
						left: (-fw + (page === 2 ? w : 0)) + "px",
						width: fw + "px",
						height: fh + "px"
					}, {
						transformOrigin: "100% 0%"
					}),
					front: css3.add({
						width: w + "px",
						height: h + "px"
					}, {
						boxShadow: "0 0"
					}),
					back: css3.add({
						width: w + "px",
						height: h + "px"
					}, {
						boxShadow: "0 0"
					}),
					shadow: {
						display: "",
						left: fw + "px",
						height: h * 1.5 + "px"
					}
				};
				params.turnForward = {
					page: css3.add({}, {
						transform: "rotate(0deg)"
					}),
					front: css3.add({}, {
						transform: "translate("+ fw + "px," + fy +"px) rotate(0deg)",
						transformOrigin: "-110px -18px"
					}),
					back: css3.add({}, {
						transform: "translate("+ (fw - w) + "px," + fy +"px) rotate(0deg)",
						transformOrigin: "0px 0px"
					})
				};
				params.turnBackward = {
					page: css3.add({}, {
						transform: "rotate(-32deg)"
					}),
					front: css3.add({}, {
						transform: "translate("+ cx + "px," + cy +"px) rotate(32deg)",
						transformOrigin: "0px 0px"
					}),
					back: css3.add({}, {
						transform: "translate("+ dx + "px," + dy +"px) rotate(-32deg)",
						transformOrigin: "0px 0px"
					})
				};
				break;
				
			case "bottom":
				cx = fw - (h * sin32 + w * cos32) - 2;
				cy = fh - (h + w/tan32) * cos32;
				dx = fw;
				dy = fh - w/sin32 - h;
				fy = fh - w/tan32 - h;
				
				params.init = {
					page: css3.add({
						top: (-fy + 50) + "px",
						left: (-fw + (page === 2 ? w : 0)) + "px",
						width: fw + "px",
						height: fh + "px"
					}, {
						transformOrigin: "100% 100%"
					}),
					front: css3.add({
						width: w + "px",
						height: h + "px"
					}, {
						boxShadow: "0 0"
					}),
					back: css3.add({
						width: w + "px",
						height: h + "px"
					}, {
						boxShadow: "0 0"
					}),
					shadow: {
						display: "none"
					}
				};
				params.turnForward = {
					page: css3.add({}, {
						transform: "rotate(0deg)"
					}),
					front: css3.add({}, {
						transform: "translate("+ fw + "px," + fy +"px) rotate(0deg)",
						transformOrigin: "-220px 35px"
					}),
					back: css3.add({}, {
						transform: "translate("+ (w * 2) + "px," + fy +"px) rotate(0deg)",
						transformOrigin: "0px 0px"
					})
				};
				params.turnBackward = {
					page: css3.add({}, {
						transform: "rotate(32deg)"
					}),
					front: css3.add({}, {
						transform: "translate("+ cx + "px," + cy +"px) rotate(-32deg)",
						transformOrigin: "0px 0px"
					}),
					back: css3.add({}, {
						transform: "translate("+ dx + "px," + dy +"px) rotate(0deg)",
						transformOrigin: "0px 0px"
					})
				};
				break;
				
			case "left":
				cx = -w;
				cy = pw/tan32 - 2;
				dx = -pw;
				dy = fy = pw/sin32 + dw * sin32;
				
				params.init = {
					page: css3.add({
						top: -cy + "px",
						left: w + "px",
						width: fw + "px",
						height: fh + "px"
					}, {
						transformOrigin: "0% 0%"
					}),
					front: css3.add({
						width: w + "px",
						height: h + "px"
					}, {
						boxShadow: "0 0"
					}),
					back: css3.add({
						width: w + "px",
						height: h + "px"
					}, {
						boxShadow: "0 0"
					}),
					shadow: {
						display: "",
						left: "-4px",
						height: ((page === 2 ? h * 1.5 : h) + 50) + "px"
					}
				};
				params.turnForward = {
					page: css3.add({}, {
						transform: "rotate(0deg)"
					}),
					front: css3.add({}, {
						transform: "translate("+ cx + "px," + cy +"px) rotate(0deg)",
						transformOrigin: "160px 68px"
					}),
					back: css3.add({}, {
						transform: "translate(0px," + cy +"px) rotate(0deg)",
						transformOrigin: "0px 0px"
					})
				};
				params.turnBackward = {
					page: css3.add({}, {
						transform: "rotate(32deg)"
					}),
					front: css3.add({}, {
						transform: "translate("+ (-dw) + "px," + dy +"px) rotate(-32deg)",
						transformOrigin: "0px 0px"
					}),
					back: css3.add({}, {
						transform: "translate("+ dx + "px," + dy +"px) rotate(32deg)",
						transformOrigin: "top right"
					})
				};
				break;
			}
			
			params.init.catalog = { // catalogNode
				width: (page === 2 ? w * 2 : w) + "px",
				height: ((page === 2 ? h * 1.5 : h) + (turnfrom == "top" ? 0 : 50)) + "px"
			};
		};

		this.getChildren = function(/*DomNode*/node){
			return array.filter(node.childNodes, function(n){ return n.nodeType === 1; });
		};

		this.getPages = function(){
			// summary:
			//		Get the array of all page nodes.
			return this._catalogNode ? this.getChildren(this._catalogNode) : null;
		};

		this.getCurrentPage = function(){
			// summary:
			//		Get the current page node.
			return this._currentPageNode;
		};

		this.getIndexOfPage = function(/*DomNode*/pageNode, /*DomNode[]?*/pages){
			if(!pages){
				pages = this.getPages();
			}
			for(var i=0; i<pages.length; i++){
				if(pageNode === pages[i]){ return i; }
			}
			return -1;
		};

		this.getNextPage = function(/*DomNode*/pageNode){
			for(var n = pageNode.nextSibling; n; n = n.nextSibling){
				if(n.nodeType === 1){ return n; }
			}
			return null;
		};

		this.getPreviousPage = function(/*DomNode*/pageNode){
			for(var n = pageNode.previousSibling; n; n = n.previousSibling){
				if(n.nodeType === 1){ return n; }
			}
			return null;
		};

		this.isPageTurned = function(/*DomNode*/pageNode){
			return pageNode.style[css3.name("transform")] == "rotate(0deg)";
		};

		this._onPageTurned = function(e){
			event.stop(e);
			if(domClass.contains(e.target, "mblPageTurningPage")){
				this.onPageTurned(e.target);
			}
		};

		this.onPageTurned = function(/*DomNode*/ /*===== pageNode =====*/){
			// summary:
			//		Stub function to which your application connects 
			//		to handle the event when each page is tured 
			// description:
			//		Called just after each page is turned.
		};

		this.initCatalog = function(/*DomNode*/catalogNode){
			// summary:
			//		Initializes the specified catalog/book.
			// description:
			//		Apply style class and other various styles to the specified catalog node
			//		for initialization.
			//		This function must be called every time when property values
			//		are changed by calling this.init(...) function.
			// catalogNode: DOMNode
			//		The catalog node to be initialized.
			
			if(this._catalogNode != catalogNode){
				if(this._transitionEndHandle){
					connect.disconnect(this._transitionEndHandle);
				}
				this._transitionEndHandle = connect.connect(catalogNode, css3.name("transitionEnd"), this, "_onPageTurned");
				this._catalogNode = catalogNode;
			}
			
			// Initialize catalog node
			domClass.add(catalogNode, "mblPageTurningCatalog");
			domStyle.set(catalogNode, this._styleParams.init.catalog);
			
			// Initialize child pages
			var pages = this.getPages();
			array.forEach(pages, function(pageNode){
				this.initPage(pageNode);
			}, this);
			
			this.resetCatalog();
		};

		this._getBaseZIndex = function(){
			// Check z-index of catalogNode
			return this._catalogNode.style.zIndex || 0;
		};

		this.resetCatalog = function(){
			// summary:
			//		Resets the catalog by adjust the state of child pages.
			// description:
			//		Adjust z-index and dog-ear show/hide state of each child page.
			//		This function must be called when you add a new page 
			//		or remove some page after initialization.
			
			// Adjust z-index and dog-ear of child pages
			var pages = this.getPages(),
				len = pages.length,
				base = this._getBaseZIndex();
			for(var i = len - 1; i>=0; i--){
				var pageNode = pages[i];
				this.showDogear(pageNode);
				if(this.isPageTurned(pageNode)){
					pageNode.style.zIndex = base + len + 1;
				}else{
					pageNode.style.zIndex = base + len - i;
					!this.alwaysDogeared && this.hideDogear(pageNode);
					this._currentPageNode = pageNode;
				}
			}
			if(!this.alwaysDogeared && this._currentPageNode != pages[len - 1]){
				this.showDogear(this._currentPageNode);
			}
		};

		this.initPage = function(/*DomNode*/pageNode, /*int?*/dir){
			// summary:
			//		Initializes the specified page.
			// description:
			//		Apply style class and other various styles to the specified page node
			//		for initialization.
			//		This function must be called when you add a new page after initialization.
			// pageNode: DOMNode
			//		The page node to be initialized.
			// dir: int?
			//		Specified whether the page is turned or not at initialization.
			//		This parameter should be either of 1 or -1.
			//		If 1, the page is turned at initialization. If -1, it is not turned.
			
			// Create shadow node if not exist
			var childNodes = this.getChildren(pageNode);
			while(childNodes.length < 3){
				pageNode.appendChild(domConstruct.create("div", null));
				childNodes = this.getChildren(pageNode);
			}
			
			// Check if it is the first time to initialize this page node or not
			var isFirst = !domClass.contains(pageNode, "mblPageTurningPage");
			
			// Apply style class
			domClass.add(pageNode, "mblPageTurningPage");
			domClass.add(childNodes[0], "mblPageTurningFront"); // frontNode
			domClass.add(childNodes[1], "mblPageTurningBack"); // backNode
			domClass.add(childNodes[2], "mblPageTurningShadow"); // shadowNode
			
			// Apply styles
			var p = this._styleParams.init;
			domStyle.set(pageNode, p.page);
			domStyle.set(childNodes[0], p.front); // frontNode
			domStyle.set(childNodes[1], p.back); // backNode
			p.shadow && domStyle.set(childNodes[2], p.shadow); // shadowNode
			
			if(!dir){
				// Determine whether to turn this page or not
				if(isFirst && this._currentPageNode){
					var pages = this.getPages();
					dir = this.getIndexOfPage(pageNode) < this.getIndexOfPage(this._currentPageNode) ? 1 : -1;
				}else{
					dir = this.isPageTurned(pageNode) ? 1 : -1;
				}
			}
			this._turnPage(pageNode, dir, 0);
		};

		this.turnToNext = function(/*Number?*/duration){
			// summary:
			//		Turns a page forward.
			// description:
			//		The current page is turned forward if it is not the last page of the book.
			// duration: Number?
			//		The duration of page turning animations by seconds. (ex. 1.5, 3, etc)
			//		If this parameter is omitted, this.duration property value is used.
			var nextPage = this.getNextPage(this._currentPageNode);
			if(nextPage){
				this._turnPage(this._currentPageNode, 1, duration);
				this._currentPageNode = nextPage;
			}
		};

		this.turnToPrev = function(/*Number?*/duration){
			// summary:
			//		Turns a page backward.
			// description:
			//		The current page is turned backward if it is not the first page of the book.
			// duration: Number?
			//		The duration of page turning animations by seconds. (ex. 1.5, 3, etc)
			//		If this parameter is omitted, this.duration property value is used.
			var prevPage = this.getPreviousPage(this._currentPageNode);
			if(prevPage){
				this._turnPage(prevPage, -1, duration);
				this._currentPageNode = prevPage;
			}
		};

		this.goTo = function(/*int*/index){
			// summary:
			//		Jumps to the specified page without page turning animations.
			// index: int
			//		The index of the page you want to jump to.
			var pages = this.getPages();
			if(this._currentPageNode === pages[index] || pages.length <= index) { return; }
			
			var goBackward = index < this.getIndexOfPage(this._currentPageNode, pages);
			while(this._currentPageNode !== pages[index]) {
				goBackward ? this.turnToPrev(0) : this.turnToNext(0);
			}
		};

		this._turnPage = function(/*DomNode*/pageNode, /*int*/dir, /*Number?*/duration){
			// tags:
			//		private
			var childNodes = this.getChildren(pageNode),
				d = ((typeof duration !== 'undefined') ? duration : this.duration) + "s",
				p = (dir === 1) ? this._styleParams.turnForward : this._styleParams.turnBackward;
			
			// Apply styles for page turning animations
			p.page[css3.name("transitionDuration")] = d;
			domStyle.set(pageNode, p.page);
			
			p.front[css3.name("transitionDuration")] = d;
			domStyle.set(childNodes[0], p.front); // frontNode
			
			p.back[css3.name("transitionDuration")] = d;
			domStyle.set(childNodes[1], p.back); // backNode
			
			// Adjust z-index and dog-ear
			var pages = this.getPages(),
				nextPage = this.getNextPage(pageNode),
				len = pages.length,
				base = this._getBaseZIndex();
			if(dir === 1){
				pageNode.style.zIndex = base + len + 1;
				if(!this.alwaysDogeared && nextPage && this.getNextPage(nextPage)){
					this.showDogear(nextPage);
				}
			}else{
				if(nextPage){
					nextPage.style.zIndex = base + len - this.getIndexOfPage(nextPage, pages);
					!this.alwaysDogeared && this.hideDogear(nextPage);
				}
			}
		};

		this.showDogear = function(/*DomNode*/pageNode){
			// summary:
			//		Shows the dog-ear.
			var childNodes = this.getChildren(pageNode);;
			domStyle.set(pageNode, "overflow", "");
			childNodes[1] && domStyle.set(childNodes[1], "display", ""); // backNode
			childNodes[2] && domStyle.set(childNodes[2], "display", this.turnfrom === "bottom" ? "none" : ""); // shadowNode
		};

		this.hideDogear = function(/*DomNode*/pageNode){
			// summary:
			//		Hides the dog-ear.
			if(this.turnfrom === "bottom"){ return; }
			
			var childNodes = this.getChildren(pageNode);
			domStyle.set(pageNode, "overflow", "visible");
			childNodes[1] && domStyle.set(childNodes[1], "display", "none"); // backNode
			childNodes[2] && domStyle.set(childNodes[2], "display", "none"); // shadowNode
		};
	};
});
