const Skinnable@ WindowFrame;
const Skinnable@ WindowBorder;
const Skinnable@ LightArea;
const Skinnable@ DarkArea;
const Skinnable@ VSep;
const Skinnable@ VSepSmall;
const Skinnable@ HSep;
const Skinnable@ HFrame;
const Skinnable@ Progress;
const Skinnable@ FilledProgress;
const Skinnable@ Resizer;
const Skinnable@ CloseButtonSkin;
const Skinnable@ ButtonSkin;
const Skinnable@ TabBar;
const Skinnable@ TabSkin;
const Skinnable@ ToggleSkin;
const Skinnable@ DialogSkin;
Color white;

/* Initialize skin */
void initSkin() {
	@WindowFrame = getSkinnable("WindowFrame");
	@WindowBorder = getSkinnable("WindowBorder");
	@LightArea = getSkinnable("LightBackground");
	@DarkArea = getSkinnable("DarkBackground");
	@VSep = getSkinnable("VSep");
	@VSepSmall = getSkinnable("VSepSmall");
	@HSep = getSkinnable("HSep");
	@HFrame = getSkinnable("HFrame");
	@TabBar = getSkinnable("TabBar");
	@Progress = getSkinnable("Progress");
	@FilledProgress = getSkinnable("FilledProgress");
	@Resizer = getSkinnable("Resizer");
	@CloseButtonSkin = getSkinnable("CloseButton");
	@ButtonSkin = getSkinnable("Button");
	@TabSkin = getSkinnable("TabButton");
	@ToggleSkin = getSkinnable("ToggleButton");
	@DialogSkin = getSkinnable("Dialog");
	white = Color(0xffffffff);
}

/* Expand an element in a horizontal direction based on text size. */
void setTextExpand(GuiButton@ button, string@ text) {
	setTextExpand(button, text, EA_Left);
}

void setTextExpand(GuiButton@ button, string@ text, ElementAlignment align) {
	button.setText(text);

	dim2di textSize = getTextDimension(text);
	dim2di btnSize = button.getSize();
	dim2di newSize = dim2di(textSize.width + 8, btnSize.height);

	if (btnSize.width > newSize.width)
		return;

	button.setSize(newSize);

	if (align == EA_Right) {
		pos2di btnPos = button.getPosition();
		button.setPosition(pos2di(btnPos.x - (newSize.width - btnSize.width), btnPos.y));
	}
}

/* Create a skinned button */
GuiButton@ Button(recti rect, string@ text, GuiElement@ parent) {
	return GuiButton(ButtonSkin, rect, text, parent);
}

GuiButton@ Button(dim2di size, string@ text, GuiElement@ parent) {
	return Button(recti(pos2di(0, 0), size), text, parent);
}

/* Create a close button */
GuiButton@ CloseButton(recti rect, GuiElement@ parent) {
	return GuiButton(CloseButtonSkin, rect, null, parent);
}

/* Create a skinned tab button */
GuiButton@ TabButton(recti rect, string@ text, GuiElement@ parent) {
	GuiButton@ bt = GuiButton(TabSkin, rect, text, parent);
	bt.setToggleButton(true);
	return bt;
}

/* Create a skinned toggle button */
GuiButton@ ToggleButton(bool pressed, recti rect, string@ text, GuiElement@ parent) {
	GuiButton@ bt = GuiButton(ToggleSkin, rect, text, parent);
	bt.setToggleButton(true);
	bt.setPressed(pressed);
	return bt;
}

GuiButton@ ToggleButton(bool pressed, dim2di size, string@ text, GuiElement@ parent) {
	return ToggleButton(pressed, recti(pos2di(0, 0), size), text, parent);
}

/* Create a skinned dialog window */
GuiElement@ DialogWindow(recti pos) {
	return GuiDraggable(DialogSkin, pos, true, null);
}

/* Draw the essential elements for a window at once */
void drawWindowFrame(recti area) {
	drawSkinnable(WindowFrame, area);
}

/* Draw only the window border */
void drawWindowBorder(recti area) {
	drawSkinnable(WindowBorder, area);
}

/* Draw a bar that tabs can be placed in */
void drawTabBar(recti area) {
	drawSkinnable(TabBar, area);
}

/* Draw a light striped area */
void drawLightArea(recti area) {
	drawLightArea(area, false);
}

void drawLightArea(recti area, bool transparent) {
	if (transparent)
		drawSkinnable(LightArea, area, Color(230, 255, 255, 255));
	else
		drawSkinnable(LightArea, area);
}

/* Draw a dark dotted area */
void drawDarkArea(recti area) {
	drawDarkArea(area, false);
}

void drawDarkArea(recti area, Color col) {
	drawSkinnable(DarkArea, area, col);
}

void drawDarkArea(recti area, bool transparent) {
	if (transparent)
		drawSkinnable(DarkArea, area, Color(230, 255, 255, 255));
	else
		drawSkinnable(DarkArea, area);
}

/* Draw a vertical separator */
void drawVSep(recti area) {
	drawSkinnable(VSep, area);
}

/* Draw a thin vertical separator */
void drawVSepSmall(recti area) {
	drawSkinnable(VSepSmall, area);
}

/* Draw a horizontal separator */
void drawHSep(recti area) {
	drawSkinnable(HSep, area);
}

/* Draw the window resize handle */
void drawResizeHandle(recti area) {
	drawSkinnable(Resizer, area);
}

/* Draw a large progressbar */
const float ProgressPip = 12.f;
const float ProgressOff = 2.f;

void drawProgressBar(recti area, float progress) {
	drawProgressBar(area, progress, Color(0xffffffff));
}

void drawProgressBar(recti area, float progress, Color col) {
	const dim2di size = area.getSize();

	/* Progress bar background */
	// Make sure the width is round
	int totalPips = floor((size.width - ProgressOff*2) / ProgressPip);

	if (totalPips == 0)
		return;

	float width = totalPips * ProgressPip + ProgressOff*2;
	
	// Generate new area
	area = recti(area.LowerRightCorner - pos2di(width, size.height),
				 area.LowerRightCorner);

	drawSkinnable(Progress, area);

	/* Actual Progress */
	pos2di topLeft = area.UpperLeftCorner;
	int pos = ProgressOff;

	int pips = floor(totalPips * progress);
	float pipPerc = 1.f / totalPips;

	float visPerc = pipPerc * pips;
	float remPerc = progress - visPerc;
	float lastPipPerc = remPerc / pipPerc;

	Color lastPipCol = col;
	lastPipCol.A *= lastPipPerc;
	if (lastPipPerc > 0)
		pips += 1;

	for (int i = 0; i < pips; ++i) {
		if (pips-1 == i)
			drawSkinnable(FilledProgress, recti(
						pos2di(topLeft.x+pos, topLeft.y),
						dim2di(ProgressPip, size.height)),
						lastPipCol);
		else
			drawSkinnable(FilledProgress, recti(
						pos2di(topLeft.x+pos, topLeft.y),
						dim2di(ProgressPip, size.height)),
						col);
		pos += ProgressPip;
	}
}

/* Programmatically draw a grid with gradients */
void drawGrid(recti area, int unit, int width, Color outer, Color inner) {
	const pos2di topLeft = area.UpperLeftCorner;
	const dim2di size = area.getSize();

	int xres = floor(size.width / unit);
	int x = (size.width - (xres % 2 == 0 ? xres : xres-1) * unit) / 2;

	for (; x <= size.width; x += unit) {
		Color inCol = outer;
		inCol = inCol.interpolate(inner, abs(size.width/2 - x) / (size.width/2));

		if (inCol.A > 2) {
			drawRect(recti(topLeft + pos2di(x - floor(width / 2), 0),
						dim2di(width, size.height/2)),
					outer, outer, inCol, inCol);

			drawRect(recti(topLeft + pos2di(x - floor(width / 2), size.height/2),
						dim2di(width, size.height/2)),
					inCol, inCol, outer, outer);
		}
	}

	int yres = floor(size.width / unit);
	int y = (size.width - (yres % 2 == 0 ? yres : yres-1) * unit) / 2;

	for (; y <= size.width; y += unit) {
		Color inCol = outer;
		inCol = inCol.interpolate(inner, abs(size.height/2 - y) / (size.height/2));

		if (inCol.A > 2) {
			drawRect(recti(topLeft + pos2di(0, y - floor(width / 2)),
						dim2di(size.width/2, width)),
					outer, inCol, outer, inCol);

			drawRect(recti(topLeft + pos2di(size.width/2, y - floor(width / 2)),
						dim2di(size.width/2, width)),
					inCol, outer, inCol, outer);
		}
	}
}

/* Utility: Percentage-based rectangle */
recti recti_p(dim2di size, float x, float y, float xend, float yend) {
	return recti(size.width * x, size.height * y,
			size.width * xend, size.height * yend);
}

/* Utility: Parse an event for resize events */
enum DragResizeEvent {
	RE_None,
	RE_Resized,
	RE_Handled,
}

class DragResizeInfo {
	bool resizing;
	bool vertResize;
	bool horizResize;

	bool dragging;

	bool canMaximize;
	bool canResize;

	bool maximized;
	recti origPos;

	pos2di offset;

	DragResizeInfo() {
		canMaximize = true;
		canResize = true;
		maximized = false;
		resizing = false;
		dragging = false;
	}
};

dim2di makeEven(dim2di orig, bool vertEven, bool horizEven) {
	if ((orig.width % 2) != int(horizEven ? 0 : 1))
		orig.width -= 1;
	if ((orig.height % 2) != int(vertEven ? 0 : 1))
		orig.height -= 1;
	return orig;
}

DragResizeEvent handleDragResize(GuiElement@ ele, const MouseEvent& evt,
		DragResizeInfo& info, int minWidth, int minHeight) {
	if (ele is null)
		return RE_None;

	recti absPos = ele.getAbsolutePosition();
	bool vertEven = absPos.getSize().height % 2 == 0;
	bool horizEven = absPos.getSize().width % 2 == 0;
	pos2di mousePos(evt.x, evt.y);

	switch (evt.EventType) {
		case MET_DBL_CLICK: {
			if (evt.y < absPos.UpperLeftCorner.y + 13 && info.canMaximize) {
				info.dragging = false;

				if (info.maximized) {
					info.maximized = false;
					ele.setSize(info.origPos.getSize());
					ele.setPosition(info.origPos.UpperLeftCorner);
				}
				else {
					info.origPos = recti(ele.getPosition(), ele.getSize());
					ele.setPosition(pos2di(0, 0));

					ele.setSize(makeEven(dim2di(getScreenWidth(), getScreenHeight()), vertEven, horizEven));
					info.maximized = true;
				}
				return RE_Resized;
			}
		} break;
		case MET_LEFT_DOWN: {
			int rightDist = absPos.LowerRightCorner.x - mousePos.x;
			int bottomDist = absPos.LowerRightCorner.y - mousePos.y;

			if (info.canResize) {
				if ((rightDist < 8 && bottomDist < 19) || (rightDist < 19 && bottomDist < 8)) {
					info.horizResize = true;
					info.vertResize = true;
					info.resizing = true;
					info.offset = absPos.LowerRightCorner - mousePos;
					return RE_Handled;
				}
				else if (rightDist < 8) {
					info.horizResize = true;
					info.vertResize = false;
					info.resizing = true;
					info.offset = absPos.LowerRightCorner - mousePos;
					return RE_Handled;
				}
				else if (bottomDist < 8) {
					info.vertResize = true;
					info.horizResize = false;
					info.resizing = true;
					info.offset = absPos.LowerRightCorner - mousePos;
					return RE_Handled;
				}
			}
			if (!info.maximized) {
				info.dragging = true;
				info.offset = mousePos - absPos.UpperLeftCorner;
				GuiElement@ focus = getGuiFocus();
				if (focus is null || focus.isAncestor(ele))
					setGuiFocus(ele);
				return RE_Handled;
			}
			return RE_None;
		}
		case MET_MOVED:
			if (info.resizing) {
				dim2di reqSize = absPos.getSize();

				// Limit resize to the edge of the screen
				int screenWidth = getScreenWidth();
				if (mousePos.x > screenWidth)
					mousePos.x = screenWidth;

				int screenHeight = getScreenHeight();
				if (mousePos.y > screenHeight)
					mousePos.y = screenHeight;

				// Check which dimensions to resize
				if (info.horizResize)
					reqSize.width = mousePos.x - absPos.UpperLeftCorner.x + info.offset.x;
				if (info.vertResize)
					reqSize.height = mousePos.y -absPos.UpperLeftCorner.y + info.offset.y;

				if (reqSize.width < minWidth)
					reqSize.width = minWidth;
				if (reqSize.height < minHeight)
					reqSize.height = minHeight;

				reqSize = makeEven(reqSize, vertEven, horizEven);

				if (reqSize != absPos.getSize()) {
					ele.setSize(reqSize);
					return RE_Resized;
				}
				return RE_Handled;
			}
			else if (info.dragging) {
				ele.setPosition(mousePos - info.offset);
				return RE_Handled;
			}
			break;
		case MET_LEFT_UP:
			if (info.resizing) {
				info.resizing = false;
				return RE_Handled;
			}
			else if (info.dragging) {
				info.dragging = false;
				return RE_Handled;
			}
			break;
	}
	return RE_None;
}
