
--===================================================
--										Class  (to be moved later)
--===================================================	
function Class(_className,_parent)
	local obj,className;
	local parent = _parent;
	obj = {};
	
	className = _className;
	
	obj.GetParent = function(self)
		return parent;
	end;
	obj.SetParent = function(self,_parent)
		parent = _parent;
	end;
	
	obj.GetType = function(self)
		return className;
	end
	
	obj.IsClass = function(self,str)
		assert(type(str)=="string","Useage: IsClass(str)");
		return self:GetType() == str;
	end;
	
	obj.IsSameClass = function(self,other)
		assert(type(other)=="table","IsSameClass. Other class is not a table");
		if other.GetType then
			return self:GetType() == other:GetType()
		end
	end
	
	return obj;
end

--===================================================
--										Helping functions
--===================================================	
local function TempBG(obj)
	if true then return end;
	local f = obj:GetGui()
	f:SetBackdrop( { 
		bgFile = "Interface\\DialogFrame\\UI-DialogBox-Gold-Background", 
		edgeFile = "edgeFile", tile = false, tileSize = 0, edgeSize = 32, 
		insets = { left = 0, right = 0, top = 0, bottom = 0 }
	});
	local color = {r=0.4,g=0.5,b=0.6}
	if GHI_EffectColors then
		color = GHI_EffectColors[GHI_ColorList[random(1,6)]];
	end
	f:SetBackdropColor(color.r, color.g, color.b,1);
end


function strsubutf8(str,a,b)  -- modified from http://wowprogramming.com/snippets/UTF-8_aware_stringsub_7
	assert(type(str)=="string" and type(a)=="number","incorrect input strsubutf8");
	assert(not(b) or (type(b)=="number" and b <= strlenutf8(str)),"End pos larger than string lenght");
	
	b = (b or strlenutf8(str));
	
	
	local start,_end = #str+1,#str+1;
  local currentIndex = 1
  local numChars = 0;
  if a <= 1 then
  	start = a;
  end
  if b <= 1 then
  	_end = b;
  end
  
  while currentIndex <= #str do
    local char = string.byte(str, currentIndex)
    if char > 240 then
      currentIndex = currentIndex + 4
    elseif char > 225 then
      currentIndex = currentIndex + 3
    elseif char > 192 then
      currentIndex = currentIndex + 2
    else
      currentIndex = currentIndex + 1
    end
    
    numChars = numChars +1;
    
    if numChars == a-1 then
    	start = currentIndex;
    end
    if numChars == b then
    	_end = currentIndex-1;
    end
  end
  return str:sub(start, _end)
end



-- class list
local Page,Area,Line,Text;

--===================================================
--										Document
--===================================================	
function GHU_Document(_parent,_width,_height,_xOff,_yOff)
	local className = "GHU_Document";
	local obj;
	local pages, altPages = {},{}; -- table with the page objects and alternative page objects
	local events={};
	local lastRef=0;
	local font,fontNotifiers = nil,{};
	
	local width,height,scale;
	local gui;
	
	
	local marked = {};
	local firstMarked;
	
	local currentPage = 1;
	local focus;
	
	local materialTex,backgroundTex;
	
	-- functions
	obj = {
	
	-- show
	Show = function(self)
		gui:Show();
		currentPage = 1;
		if pages[currentPage] then
			pages[currentPage]:Show();
		end
	end,
	Hide = function(self)
		gui:Hide();
		for _,page in pairs(pages) do
			page:Hide();		
		end
		--[[
		if pages[currentPage] then
			pages[currentPage]:Hide();
		end ]]
	end,
	
	-- Get functions
	GetType = function(self)
		return className;
	end,
	GetNext = function(self,page)
		for i =1,#(pages) do
			if page == pages[i] then -- the current page is found
				return pages[i+1]; -- is nil if it is the last page
			end
		end
	end,
	GetPrev = function(self,page)
		for i =1,#(pages) do
			if page == pages[i] then -- the current page is found
				return pages[i-1]; -- is nil if it is the first page
			end
		end
	end,
	GetFirst = function(self)
		return pages[1];
	end,
	GetLast = function(self)
		return pages[#(pages)];
	end,
	
	GetGui = function()
		return gui;
	end,
	
	-- Font
	SetFont = function(self,newFont)
		assert(newFont:GetObjectType()=="Font","The given font is not a font widget object. todo: change 'font'");
		font = newFont;
		
		-- notify the current focus
		
		-- notify registered functions
		for _,f in pairs(fontNotifiers) do
			f(obj);
		end
	end,
	GetFont = function(self)
		return font;	
	end,
	NotifyOnFontChange = function(self,func)
		assert(type(func)=="function","Usage: NotifyOnFontChange(func)");
		table.insert(fontNotifiers,func)
	end,
	
	-- marked
	RegMarked = function(self,object)
		obj:RemoveMarked(object);
		table.insert(marked,object);
	end,
	RemoveMarked = function(self,object)
		for i = #(marked),1,-1 do -- running backwards at it is often the latest registered 
			if marked[i] == object then
				table.remove(marked,i)
				return;			
			end
		end
	end,
	GetMarked = function(self)
		return marked;
	end,
	ClearAllMarkings = function(self) --print("Clear all marks");
		for i = 1,#(marked) do
			marked[i]:ClearMarked();	
		end
		firstMarked = nil;
		marked = {};
	end,
	SetFirstMarked = function(self,object)
		firstMarked = object;
		
	end,
	GetFirstMarked = function(self)
		return firstMarked;
	end,
	
	GetGui = function(self)
		return gui;
	end,
	GetParent = function(self)
		return parent;
	end,
	
	-- key handling
	ClaimFocus = function(self,object) --print("claim"); --called
		assert(object.GetType and object:GetType() == "Text","The focus can only be claimed by a text.");
		self:ClearFocus();				
		gui.key:EnableKeyboard(true);
		focus = object;
	end,
	ClearFocus = function(self) 
		if focus then --print("clear");
			gui.key:EnableKeyboard(nil);
			focus:SetCursorPos(nil);
			focus = nil;
		end
	end,
	GetFocus = function(self)
		return focus;
	end,
	
	-- serialize 
	-- recycleData is used to store data that is expected to possible reoccur in the object
	Serialize = function(keys,recycleData)
		local returnAll;
		if not(keys) and  not(recycleData) then 
			keys = {};
			recycleData = {};		
			returnAll = true;
		end
		
		local serialTable = {};
		keys[className] = {"pages"};
		
		for index,key in pairs(keys[className]) do
			local t = {};
			if key == "pages" then -- pack the pages
				
				for i,page in pairs(pages) do
					t[i] = page:Serialize(keys,recycleData);
				end
				
			end
			
			serialTable[index] = t;
		end
		
		if returnAll then
			return {serialTable,keys,recycleData};
		end
		return serialTable;
	end,
	Deserialize = function(t,keys,recycleData)
		if not(keys) and not(recycleData) then
			if type(t)=="table" then
				t_new,keys,recycleData = unpack(t);
				t = t_new;
			end
		end
	
		-- insert recycle data
		for index,data in pairs(t) do
			if type(data) == "string" then
				local recindex = gsub("_Rec_%s*")-- todo: check
				if recindex then
					t[index] = recycleData[recindex];
				end
			end		
		end
	
		for index,key in pairs(keys[className] or {}) do -- unpack the data one key at the time
					
			if key == "pages" then
				-- unpack the pages
				
			elseif key == "" then
			
			else
				--print(format("Warning: Missing index %s while unpacking %s. Data might be lost.",key,className));
			end
		end
	end,
	
	}
		
	-- setup
	width,height,scale = _width or 300,_height or 400,1;
		
	-- gui setup
	parent = _parent;
	gui = CreateFrame("Frame",parent or UIParent);
	gui.model = obj;
	gui:SetPoint("TOPLEFT",parent,"TOPLEFT",_xOff,_yOff);
	gui:SetWidth(width);
	gui:SetHeight(height);
	
	table.insert(pages,Page(obj,width,height,0,0))
	altPages = {};
	
	-- key handler
	local function OnKey(key)
		if key=="V" and IsControlKeyDown() then
			--print("disable")
			gui.key:EnableKeyboard(nil);
		elseif key=="S" and IsControlKeyDown() then
			-- save
			return true;		
		end
	end
	
	gui.key = CreateFrame("Button",gui);
	gui.key:SetFrameStrata("DIALOG");

	gui.key:Show();
	gui.key:SetScript("OnKeyUp",function(self,key)
		 self.activeKey = nil;
	end);
	gui.key:SetScript("OnKeyDown",function(self,key)
		
		self.activeKey = key;
		
		if focus then
			if not(OnKey(self.activeKey)) then -- try a document keybind first
				focus:EnterSpecial(self.activeKey);
			end
			self.nextKeyTime = GetTime()+0.5;
		end
	end);
	gui.key:SetScript("OnUpdate",function(self)
		if focus and self.activeKey and self.nextKeyTime and self.nextKeyTime <= GetTime() then
			if not(OnKey(self.activeKey)) then
				focus:EnterSpecial(self.activeKey);
			end
			self.nextKeyTime = GetTime()+0.05;
		end
	end);
	
	
	materialTex = DocTexture(obj:GetGui(),width+20,height+20,-10,10);
	materialTex:SetTextures(GHU_GetCollection("Parchment 2"));
	materialTex:Show();
	
	return obj;
end

--===================================================
--								Document Texture
--===================================================	
-- used for document material and document backgrounds
function DocTexture(_parent,_width,_height,_xOff,_yOff)
	local class = Class("DocTexture",_parent);
	
	local parent,width,height,xOff,yOff = _parent,_width,_height,_xOff,_yOff;
	local textures = {};
	
	local mainFrame;
	
	local texFrames = {};
	
	
	local getTextures = function(texType) -- get all available textures of a given type
	
	end
	
	class.SetTextures = function(self,...)
		for _,tex in pairs({...}) do
			local Type = tex:GetTexType();
			textures[Type] = textures[Type] or {};
			table.insert(textures[Type],tex);
		end
				
		self.Update();
	end
	
	
	
	class.Show = function(self)
		mainFrame:Show();
	end
	class.Hide = function(self)
		mainFrame:Hide();
	end
	
	
	
	class.Update = function(self)
		if #(textures["center"] or {}) == 0 then return end
				
		-- determine the smallest texture size
		local minX,minY,minXY;
		for _,cat in pairs(textures) do
			for _,tex in pairs(cat or {}) do
				minX = min(minX or tex:GetWidth(),tex:GetWidth());
				minY = min(minY or tex:GetHeight(),tex:GetHeight());
			end
		end
		minXY = min(minX,minY);
		
		-- calculate the used size of each texture and the amount of textures
		local cX = max(math.floor((width/minXY)+0.5),3);
		local cY = max(math.floor((height/minXY)+0.5),3);
		
		local xSize,ySize = math.floor(width/cX),math.floor(height/cY);
		
				
		-- calculate the needed frames
		local framesNeeded = cX*cY;
		
		-- create evt missing frames
		while (#(texFrames) < framesNeeded) do
			local f = CreateFrame("Frame",mainFrame);		
			f:SetFrameStrata("BACKGROUND");
			table.insert(texFrames,f);
		end
		
		-- place frames
		for iX = 1,cX do
			for iY = 1,cY do
				local f = texFrames[(iX-1)*cY + iY];
				f:SetWidth(xSize);
				f:SetHeight(ySize);
				-- evt clear all points?
				f:ClearAllPoints();
				f:SetPoint("TOPLEFT",mainFrame,"TOPLEFT",(iX-1)*xSize,-(iY-1)*ySize);
				
				-- decide and set texture
				local texType;
				if iX == 1 and iY == 1 then -- topleft
					texType = "topleft";
				elseif iX == cX and iY == 1 then -- topright		
					texType = "topright";		
				elseif iX == 1 and iY == cY then -- bottomleft
					texType = "bottomleft";
				elseif iX == cX and iY == cY then -- bottomright
					texType = "bottomright";
				elseif iX == 1 then -- left
					texType = "left";
				elseif iX == cX then -- right
					texType = "right";
				elseif iY == 1 then -- top
					texType = "top";
				elseif iY == cY then -- bottom
					texType = "bottom";
				end
				
				if not(texType) or not(textures[texType]) then
					texType = "center";				
				end
				
				local tex = textures[texType][random(#( textures[texType]))]; -- use a random of the fitting textures
				tex:ApplyTexture(f);				
				f:Show();
			end		
		end
		
		-- hide eventual unused frames
		for i=framesNeeded+1,#(texFrames) do
			if texFrames[i] then
				texFrames[i]:Hide();
			end	
		end
		
					
	end
	
	mainFrame = CreateFrame("Frame",parent);
	mainFrame:SetHeight(height);
	mainFrame:SetWidth(width);
	mainFrame:SetPoint("TOPLEFT",parent,"TOPLEFT",xOff,yOff)
	
	return class;
end

local textureTypes = {
	top=true,bottom=true,left=true,right=true,
	topleft=true,topright=true,buttomleft=true,buttomright=True,
	center=true,all=true,
};
function GHU_Texture(_path,_width,_height,_type)
	assert(type(_path)=="string" and type(_width)=="number" and type(_height)=="number","Useage Texture(parent,path,width,height)");
	if not(type(_types)=="table") then
		_types = {"center"}; --default
	end

	assert(textureTypes[_type]==true,"Unknown type "..(_type or "nil"));	
	
	
	local class = Class("Texture",nil);
	
	local path,width,height,Type = _path,_width,_height,_type;
	
	
	class.GetWidth = function(self) return width; end;
	class.GetHeight = function(self) return height; end;
	
	class.GetTexType = function(self) return Type; end;
	
	class.IsTexType = function(self,str)
		assert(type(textureTypes[str])==true,"Unknown texture type lookup");
		return str == Type;
	end;
		
	class.ApplyTexture = function(self,frame)
		if not(frame.texture) then
			frame.texture = frame:CreateTexture(nil,"BACKGROUND")
		end
		frame.texture:SetTexture(path)
		frame.texture:SetAllPoints(frame)

	end
	
	return class;
end

--===================================================
--										Page
--===================================================	

function Page(_parent,width,height,xOff,yOff) 
	local className,obj = "Page";
	
	local areas,header,footer = {};
	
	local parent = _parent;
	local gui;
	
	-- functions	
	obj = {
		
		-- show
		Show = function(self)
			gui:Show();
			
			for _,area in pairs(areas) do
				area:Show();			
			end
			header:Show();
			footer:Show();
		end,
		
		-- Get functions
		GetNext = function(self,area)
			for i =1,#(areas) do
				if area == areas[i] then -- the current page is found
					local t =  areas[i+1]; 
					if t then 
						return t;
					end
					-- the area is not there -> it is the last area on the page. Find first area on next page
					local nextPage = parent:GetNext(self);
					if not(nextPage) then
						return;					
					end
					return nextPage:GetFirst();
				end
			end
			
			-- check if it is the header
			if area == header then
				-- return header of next page
				local nextPage = parent:GetNext(self);
				if not(nextPage) then
					return;					
				end
				return nextPage:GetHeader();
			elseif area == footer then
				-- return footer of next page
				local nextPage = parent:GetNext(self);
				if not(nextPage) then
					return;					
				end
				return nextPage:GetFoter();
			end
		end,
		GetPrev = function(self,area)
			for i =1,#(areas) do
				if area == areas[i] then -- the current area is found
					if areas[i-1] then
						return areas[i-1];					
					end
					-- it is the first area on the page
					local prevPage = parent:GetNext(self);
					if not(prevPage) then
						return;					
					end
					return prevPage:GetLast();
				end
			end
			
			-- check if it is the header
			if area == header then
				-- return header of next page
				local prevPage = parent:GetPrev(self);
				if not(prevPage) then
					return;					
				end
				return prevPage:GetHeader();
			elseif area == footer then
				-- return footer of next page
				local prevPage = parent:GetPrev(self);
				if not(prevPage) then
					return;					
				end
				return prevPage:GetFoter();
			end
		end,
		GetFirst = function(self)
			return areas[1];
		end,
		GetLast = function(self)
			return areas[#(areas)];
		end,
		
		
		GetHeader = function(self)
			return header;
		end,
		GetFooter = function(self)
			return footer;
		end,
		
		GetGui = function(self)
			return gui;
		end,
		GetParent = function(self)
			return parent;
		end,
		
		-- Serialize
	
	}
	
	-- ============== setup ==============
	-- gui
	local STD_HeaderFooterHeight = 25;
	gui = CreateFrame("Frame",parent:GetGui());
	gui:SetPoint("TOPLEFT",parent:GetGui(),"TOPLEFT",xOff,yOff);
	gui:SetWidth(width);
	gui:SetHeight(height);
	
	--TempBG(obj);
	
	-- areas		
	table.insert(areas,Area(obj,width,height - 2* STD_HeaderFooterHeight,0,-STD_HeaderFooterHeight))
	header = Area(obj,width,STD_HeaderFooterHeight,0,0);
	footer = Area(obj,width,STD_HeaderFooterHeight,0,- (height - STD_HeaderFooterHeight));
	return obj;
end

--===================================================
--										Area
--===================================================	

function Area(_parent,width,height,xOff,yOff) 
	local className,obj = "Area";
	
	local lines = {};
	local unboundLine; -- "line" for all unbound objects on the page
	local parent,gui,doc;
	
	-- functions
	obj = {
		-- show
		Show = function(self)
			gui:Show();
			for _,line in pairs(lines) do
				line:Show();
			end
			unboundLine:Show();
		end,
		
		-- Get functions 
		GetNext = function(self,line)
			for i =1,#(lines) do
				if line == lines[i] then -- the current line is found
					local t =  lines[i+1]; 
					if t then 
						return t;
					end
					-- the line is not there -> it is the last line on the page. Find first line on next area
					local nextArea = parent:GetNext(self);
					if not(nextArea) then
						return;					
					end
					return nextArea:GetFirst();
				end
			end
		end,
		GetPrev = function(self,line)
			for i =1,#(lines) do
				if line == lines[i] then -- the current area is found
					if i == 1 then
						-- not there
						local prevArea = parent:GetPrev(self);
						if not(prevArea) then
							return;
						end
						return prevArea:GetLast();
					else
						return lines[i-1]; -- is nil if it is the first page
					end
					
				end
				
				
			end
		end,
		GetFirst = function(self)
			return lines[1];
		end,
		GetLast = function(self)
			return lines[#(lines)];
		end,
		
		GetGui = function(self)
			return gui;
		end,
		GetParent = function(self)
			return parent;
		end,
		
		InsertLine = function(self,asFirst)
			local newLine = Line(self,width,0) 
			if asFirst then
				table.insert(lines,1,newLine)
			else
				table.insert(lines,newLine)
			end
			self:Update();
			return newLine;
		end,
		DeleteLine = function(self,line)
			for i =1,#(lines) do
				if lines[i] == line then
					table.remove(lines,i);
					line:Dispose();
					
					-- if it is the last object in line
					if #(lines) == 0 then
						parent:DeleteArea(self);
					else
						self:Update();
					end
				end
			end
		end,
		Update = function(self)
			local y = 0;
			for i=1,#(lines) do
				local line = lines[i];
				line:SetOffset(-y);
				y = y + line:GetHeight();
				
				-- todo: Handle overflow			
			end
		end,
	}
	
	-- ============== setup ==============
	-- gui
	parent = _parent;
	gui = CreateFrame("Button",parent:GetGui());
	gui:SetPoint("TOPLEFT",parent:GetGui(),"TOPLEFT",xOff,yOff);
	gui:SetWidth(width);
	gui:SetHeight(height);
	
	table.insert(lines,Line(obj,width,0))
	--unboundLine = Line(obj,width,0);
	
	-- gui button for clicks
	gui:RegisterForDrag("LeftButton");
	gui:RegisterForClicks("AnyUp","AnyDown");
	
	
	local function markFromCurTo(object,position)
		
					
		local first = doc:GetFirstMarked();
		if not(first) then
			-- there is nothing marked. Insert object with current cursor as first
			first = doc:GetFocus();
			if not(first) then	-- no focus in document yet
				return
			end
			local cp = first:GetCursorPos();
			first:SetMarkFirstPos(cp);
			doc:SetFirstMarked(first);
			--print("Setting new first");				
		end
		
		-- save current start
		local startPos = first:GetMarkFirstPos() or first:GetCursorPos();
		doc:ClearAllMarkings();
		first:SetMarkFirstPos(startPos);
		
		-- Find relative connection (connection in chain)
		local found,backwards,o;
		o = object;
		-- from start to obj
		while (not(found) and o) do
			if o == first then
				found = true;
				backwards = true;
			else
				--print("Looking backwards");
				o = o:GetParent():GetPrev(o);
			end			
		end
		-- from obj to start
		o = object;
		while (not(found) and o) do
			if o == first then
				found = true;
			else
				
				o = o:GetParent():GetNext(o);
			end			
		end
		
		if not(found) then
			-- no connection
			--print("No connection");
			return;			
		end
		
		-- mark all between obj and start
		o = object;
		while (o) do
			
			if o == object and o == first then
				doc:SetFirstMarked(o);
				local p1 = position;
				local p2 = o:GetMarkFirstPos();
				if not(p2) then return end -- unknown reason
				o:SetMarked(min(p1,p2),max(p1,p2));
				break;
			elseif o == object then
				-- mark self
				
				local p = position;
				if not(p) then return end;						
				
				if backwards then
					o:SetMarked(0,p);
				else
					o:SetMarked(p);
				end
				o:SetCursorPos(p);
			elseif o == first then
				doc:SetFirstMarked(o);
				if backwards then
					o:SetMarked(o:GetMarkFirstPos());
				else
					o:SetMarked(0,o:GetMarkFirstPos());
				end
				break;
			else
				o:SetMarked(); -- mark all
			end 
			
			if backwards then
				o = o:GetParent():GetPrev(o);
			else
				o = o:GetParent():GetNext(o);
			end			
		end
		
			
			
	
	end
	
	local function GetObjectUnderMouse()
		local x, y = GetCursorPosition();
		local s = gui:GetEffectiveScale();
		local xpos,ypos = x/s - gui:GetLeft(), -(y/s - gui:GetTop());
		
		if #(lines) == 0 then return; end
		
		local line = lines[1];
		
		while(obj:GetNext(line) and ypos > line:GetHeight()) do
			ypos = ypos - line:GetHeight();
			line = obj:GetNext(line);
		end
		
		local object = line:GetFirst();
		while(line:GetNext(object) and xpos > object:GetWidth()) do
			xpos = xpos - object:GetWidth();
			object = line:GetNext(object);
		end
		
		return object,xpos,ypos;
	end
	
	gui.isTextDragFrame = true;
	gui:SetScript("OnClick",function(self,btn,down)
		if (down) then
			--drag = true;
			self = obj;
			
			if btn == "LeftButton" then
				-- determine what object the mouse is over
				local object,xpos,ypos = GetObjectUnderMouse();
			
				if IsShiftKeyDown() and doc:GetFocus() then -- handle marking
					
						markFromCurTo(object,object:GetWidthPos(xpos));
					
					
				else
					--obj:ClearMarked()
					doc:ClearAllMarkings();
					object:SetCursorPos(object:GetWidthPos(xpos));
				end
			end
		else
			textDrag = false;
			
		end
	end);
	
	-- dragging
	local textDrag;
	gui:SetScript("OnDragStart",function(self) textDrag = true; end);
	gui:SetScript("OnDragStop",function(self) textDrag = false; end);
	gui:SetScript("OnUpdate",function(self) 
		
		if textDrag and GetMouseFocus() == gui then
			local object,xpos,ypos = GetObjectUnderMouse();
			markFromCurTo(object,object:GetWidthPos(xpos));
		end
					
	end)
	
	-- find document class
	doc = parent; 
	while (not(doc.GetType) or not(doc:GetType()=="GHU_Document")) do
		doc = doc:GetParent();
	end --
	
	
	-- temp background
	--TempBG(obj);
	
	
	return obj;
end

--===================================================
--										Line
--===================================================	

function Line(_parent,width)
	local className,obj = "Line";
	local objects = {};
	local parent,gui,bgbtn;
	
	-- functions
	obj = {
		-- show
		Show = function(self)
			gui:Show();
			for _,object in pairs(objects) do
				object:Show();
			end
		end,
		Hide = function(self)
			gui:Hide();
			for _,object in pairs(objects) do
				object:Hide();
			end
		end,
		
		-- Get functions
		GetNext = function(self,object)
			for i =1,#(objects) do
				if object == objects[i] then -- the current object is found
					if objects[i+1] then 
						return objects[i+1];
					end
					-- the object is not there
					local nextLine = parent:GetNext(self);
					if not(nextLine) then
						return;					
					end
					return nextLine:GetFirst();
				end
			end
		end,
		GetPrev = function(self,object)
			for i =1,#(objects) do
				if object == objects[i] then -- the current area is found
					if i == 1 then
						-- first one
						local prevLine = parent:GetPrev(self);
						if not(prevLine) then
							return;
						end
						return prevLine:GetLast();
					else
						return objects[i-1]; -- is nil if it is the first page
					end
				end
				
				
			end
		end,
		GetFirst = function(self)
			return objects[1];
		end,
		GetLast = function(self)
			return objects[#(objects)];
		end,
		
		GetGui = function(self)
			return gui;
		end,
		GetParent = function(self)
			return parent;
		end,
		
		Dispose = function(self)
			assert(#(objects)==0,"Line can not be disposed before all objects in the line ar deleted");
			self:Hide();
			wipe(gui)
			wipe(self);
		end,
		
		GetHeight = function(self)
			return gui:GetHeight();
		end,
		
		SetOffset = function(self,yOff)
			gui:ClearAllPoints();
			gui:SetPoint("TOPLEFT",parent:GetGui(),"TOPLEFT",0,yOff);
		end,
		
		InsertObject = function(self,object,asFirst) 
			if asFirst then
				-- if possible merge with the first
				local obj2 = objects[1];
				if obj2 and  obj2:GetType() == object:GetType() and obj2:GetFont() == object:GetFont() then  
					--print("merged with first","Merged '",object:GetText(),"' + '",obj2:GetText(),"'");
					obj2:AppendText(object:GetText(),true);
					object:Dispose();
					return obj2;
				else
					table.insert(objects,1,object);
					
				end
			else
				-- if possible merge with the last
				local obj2 = objects[#(objects)];
				if obj2 and not(obj2:RequiresNewline()) and  obj2:GetType() == object:GetType() and obj2:GetFont() == object:GetFont() then  
					--print("merged with last","Merged '",obj2:GetText(),"' + '",object:GetText(),"'");
					
					obj2:AppendText(object:GetText());
					object:Dispose();
					
					return obj2;
				else
				table.insert(objects,object);
				end
			end
			
			object:SetParent(self);
			
			self:Update();
			return object;
		end,
		
		RemoveObject = function(self,object) 
			for i =1,#(objects) do
				if objects[i] == object then
					table.remove(objects,i);
				
					-- if it is the last object in line
					if #(objects) == 0 then
						parent:DeleteLine(self);
					else
						self:Update();
					end
				end
			end
		end,
		
		DeleteObject = function(self,object)
			for i =1,#(objects) do
				if objects[i] == object then
					table.remove(objects,i);
					object:Hide();
					object:Dispose();
					
					-- if it is the last object in line
					if #(objects) == 0 then
						parent:DeleteLine(self);
					else
						self:Update();
					end
				end
			end
		end,
		
		Update = function(self)
			--print(#(objects)," objects line ",self.name);
			--for i,_ in pairs(objects) do --print("   ",i); end
			
			local x = 0;
			local h = 0;
			for i=1,#(objects) do
				local object = objects[i];
				
				h = max(h,object:GetHeight());
				
				--print(x + object:GetWidth().." <= "..width)
				if x + object:GetWidth() <= width then
					object:SetOffset(x);
					x = x + object:GetWidth();				
				else
					object:SetOffset(x);
					
					-- move all next objects to next line
					local nextLine = parent:GetNext(self);
					if not(nextLine) then
						nextLine = parent:InsertLine();
					end
					while #(objects) > i do
						--print("removing ",j,"out of ",#(objects),"e nd",i+1);
						nextLine:InsertObject(objects[#(objects)],true)
						table.remove(objects,#(objects));
						
					end
					
					-- evt cut the current obj in two
					local s,off = object:Split(width - x);
					--print("cutting ",string.len(s or""),"off");
					if s then
						local newObject = Text(nextLine);
						newObject:SetFont(object:GetFont());
						newObject:SetText(s,true);
						local modObj = nextLine:InsertObject(newObject,true);
						
						if off then
							object:SetCursorPos(nil);
							modObj:SetCursorPos(off+1);
						end	
						
						return nextLine:Update();					
					else
						-- the whole object should be moved down
						nextLine:InsertObject(objects[i],true)
						table.remove(objects,i);
						return nextLine:Update();
					end
					
					--gui:SetHeight(h);
					--parent:Update();
					--return;
					
					-- unreachable
					x = x + object:GetWidth();
					break;
				end		
				
				if object:RequiresNewline() then --print("disallow more objects after ",i);
					x = width + 1; -- disallow more objects on this line				
				end
				
			end
			
			
			if x < width then
				-- see if it is possible to move the next object up			
				local object = self:GetNext(self:GetLast());
				--print("obj ",object);
				if object then
					local s,off = object:Split(width - x,true); -- split the front off untill first space
					if s and s == object:GetText() then -- the whole text
						-- move the whole object up
						object:GetParent():RemoveObject(object);
						
						self:InsertObject(object,false);
						if off then
							object:SetCursorPos(off+1);
						end	
						
						if parent:GetNext(self) then 
							parent:GetNext(self):Update();
						end
					elseif s then
						local newObject = Text(self);
						newObject:SetFont(object:GetFont());
						newObject:SetText(s,true);
						local modObj = self:InsertObject(newObject,false);
						
						if off then
							object:SetCursorPos(nil);
							modObj:SetCursorPos(modObj:GetCharLenght()-strlenutf8(s)+off);
						end	
						
						parent:GetNext(self):Update();
						--print("putting '"..self:GetPrev(newObject):GetText().."' th en '"..s.."'");
						
					end
				end
			end
			
			
			gui:SetHeight(h);
			parent:Update();			
			--GHI_Message("E nd "..self.name)
		end,
		
	}
	
	obj.name = GHI_GenerateID()
	-- ============== setup ==============
	-- gui
	parent = _parent;
	gui = CreateFrame("Frame",parent);
	gui.model = obj;
	gui:SetPoint("TOPLEFT",parent:GetGui(),"TOPLEFT",0,0);
	gui:SetHeight(20);
	gui:SetWidth(width);
	
	
	
	-- insert a basic text obj
	table.insert(objects,Text(obj))
	
	--TempBG(obj);
	
	
	
	obj:Update();
	
	
	return obj;
end

--===================================================
--										Text
--===================================================	
local textDrag;
function Text(_parent) 
	local className,obj = "Text";
	
	local gui,parent,doc;
	local font,text;
	
	local curPos;
	local markFirst;
	local markStart;
	local markEnd;
	local newLineAfter;
	
	-- local functions
	local function GetMouseTextPos()
		-- Get the position of the click relative to the left side
		local x, y = GetCursorPosition();
		local s = gui:GetEffectiveScale();
		xpos = x/s - gui:GetLeft();
		
		return self:GetWidthPos(xpos);
	end
	local function InvertColor(r,g,b,a)
		return {
			r = 1 - r,
			g = 1 - g,
			b = 1 - b,
		};
	end
	
	
	
	
	
	-- functions
	obj = {
		-- show
		Show = function(self)
			gui:Show();
		end,
		Hide = function(self)
			gui:Hide();
			gui.input:Hide();
			gui.marker:Hide();
		end,
		Dispose = function(self)
			self:Hide();
			wipe(gui.input);
			wipe(gui.label);
			wipe(gui)
			wipe(self);
		end,
		
		GetGui = function(self)
			return gui;
		end,
		GetParent = function(self)
			return parent;
		end,
		SetParent = function(self,par)
			parent = par;
		end,
			
		GetType = function(self)
			return className;
		end,
		
		SetOffset = function(self,xOff)
			gui:ClearAllPoints();
			gui:SetPoint("BOTTOMLEFT",parent:GetGui(),"BOTTOMLEFT",xOff,0);
		end,
		
		SetFont = function(self,_font,markedOnly)
			assert(type(_font)=="table" and _font.GetObjectType and _font:GetObjectType() == "Font","Useage: Text:SetFont(font)");
			
			if markedOnly and not(self:IsFullyMarked()) then
				-- depending on the marking, split in two or three
				if markStart == 0 then
					-- todo: remember to insert new objects in the line
				elseif markEnd == strlenutf8(text) then
				
				else
				
				end
			end
			
			font = _font;
			local _,h = font:GetFont();
			gui:SetHeight(h);
			gui.label:SetFontObject(font);
			gui.label:SetHeight(h);
			
			gui.input:SetFontObject(font);
			gui.input:SetHeight(h);
			
			--marker
			gui.marker:SetHeight(h);
			--local color = InvertColor(font:GetTextColor());
			--gui.marker:SetBackdropColor(color.r,color.g,color.b,0.5);
		end,
		GetFont = function(self)
			return font;
		end,
		GetHeight = function(self)
			local _,h = font:GetFont();
			return h;
		end,
		GetWidth = function(self)
			return gui.label:GetWidth();		
		end,
		SetCursorPos = function(self,num) 
			if num == 0 and not(parent:GetFirst(self)==self) then
				local o = parent:GetPrev(self);
				if o then
					return o:SetCursorPos(strlenutf8(o:GetText()));
				end			
			end
			
			
			--print(num)
			if not(num) then 
				gui.input:ClearFocus();	
				gui.input:Hide();
				return 
			end
			
			curPos = min(num,strlenutf8(text));
			
			-- find the coordinate for the string
			gui.label:SetText(strsubutf8(text,0,curPos));
			local x = gui.label:GetWidth();
			gui.label:SetText(text);
			
			gui.input:SetPoint("TOPLEFT",gui,"TOPLEFT",x,0);
			gui.input:SetText("");
			gui.input:Show();
			gui.input:SetFocus();	
			--gui.key:Show();
		end,
		GetCursorPos = function(self)
			return curPos;
		end,
		GotCursorFocus = function(self)
			return (doc:GetFocus() == self);
		end,
		GetCharLenght = function(self)
			return strlenutf8(text or "");
		end,
		SetText = function(self,_text,skipUpdate)
			assert(type(_text)=="string","Text must be a string.");
			text = _text;
			gui.label:SetText(text);
			gui:SetWidth(gui.label:GetWidth()+3);
			
			-- undo register
			
			-- call update _function on parent
			if not(skipUpdate) then
				parent:Update();
			end
		end,
		GetText = function(self)
			return text;		
		end,
		GetWidthPos = function(self,xpos)
			while (gui.label:GetWidth() > xpos and strlenutf8(gui.label:GetText() or "") > 0) do
				local t = gui.label:GetText();
				gui.label:SetText(strsubutf8(t,0,strlenutf8(t)-1) or "");
			end
			local n = strlenutf8(gui.label:GetText() or "");
			gui.label:SetText(text);	
			return n;
		end,
		
		RequiresNewline = function(self)
			return newLineAfter;		
		end,
		SetNewline = function(self,newLine)
			newLineAfter = newLine;
			parent:Update();
		end,
		
		AppendText = function(self,_text,first)
			if #_text == 0 then return end;
			local newText;
			if first then--print("first");
				newText = _text..(text or "");
			else
				newText = (text or "") .. _text;
			end	
			self:SetText(newText,true);
		end,
		EnterText = function(self,_text,first,ignoreCursor) -- Enter text into the area from the position of the cursor
			if #_text == 0 then return end;
			
			-- if there is objects marked, delete them.
			local mark = doc:GetMarked()
			--print(#(mark),"marked");
			while(mark[1]) do
				object = mark[1];
				object:DeleteMarked();		
			end
			
			
			local newText;
			if curPos then
				newText = format("%s%s%s",strsubutf8(text,0,curPos) or "",_text,(curPos<strlenutf8(text) and strsubutf8(text,curPos+1)) or "")
			else
				newText = (text or "") .. _text;
			end		
			
			
			
			self:SetText(newText);
			--print("After insert",curPos)
			if doc:GetFocus() == self then
				if not(curPos) then
					self:SetCursorPos(strlenutf8(text)); -- is triggered when not intented
				else
					self:SetCursorPos(curPos + strlenutf8(_text));			
				end
			end
			
		end,
		EnterSpecial = function(self,key)
			if not(curPos) then return end;
			
			local newCurPos,newCurObj; -- new cursor position
			local newText;
			
			-- marked cases
			if #(doc:GetMarked()) > 0 then
				if key == "BACKSPACE" or key == "DELETE" then
					local mark = doc:GetMarked()
					while(mark[1]) do
						object = mark[1];
						object:DeleteMarked();		
					end
					return; -- dont d o more
				end
			end
			
			if key == "ENTER" then
				local mark = doc:GetMarked()
				while(mark[1]) do
					object = mark[1];
					object:DeleteMarked();		
				end
				newLineAfter = true;
				
				local textAfter = "";
				local textBefore = text;
				if curPos < strlenutf8(text) then
					textBefore = strsubutf8(text,0,curPos)
					textAfter = strsubutf8(text,curPos+1)			
				end
				
				-- move to first of next
				local nextObj = Text(parent);
				nextObj:SetFont(self:GetFont());
				nextObj:SetText(textAfter,true);
				
				parent:InsertObject(nextObj,nil);
				
				self:SetText(textBefore);				
				parent:GetNext(self):SetCursorPos(0);					
				return;
			elseif key == "BACKSPACE" then
				-- delete prev
				if curPos == 0 then
					local prev = parent:GetPrev(self);
					if prev then
						if prev:RequiresNewline() then -- delete newline to prev object
							prev:SetNewline(nil);
						else
							--last char in prev object
							prev:SetCursorPos(prev:GetCharLenght());
							return prev:EnterSpecial(key);
						end
					end
				else
					newText = format("%s%s",strsubutf8(text,0,curPos-1) or "",strsubutf8(text,curPos+1) or "")						
					newCurPos = curPos-1;
				end
			elseif key == "DELETE" then
				-- delete next
				if curPos == strlenutf8(text) then
					if newLineAfter then --delete newline to next object
						newLineAfter = false;
						newText = text;
					else
						--  first char in prev object
						local nextObj = parent:GetNext(self);
						nextObj:SetCursorPos(0);
						return nextObj:EnterSpecial(key);
					end
				else
					 newText = format("%s%s",strsubutf8(text,0,curPos) or "",strsubutf8(text,curPos+2) or "")						
					 
				end
			elseif key == "ESCAPE" then
				gui.input:ClearFocus();
			elseif key == "LEFT" then
				if curPos > 0 then --  or (parent:GetFirst(self) == self and curpos == 1) th en
					newCurPos = curPos - 1;
				else
					-- move to last position on previous object
					local o = parent:GetPrev(self);
					if o then
						o:SetCursorPos(strlenutf8(o:GetText()));
					end
				end
			elseif key == "RIGHT" then
				if curPos == strlenutf8(text) then
					-- move to first position on next object
					local o = parent:GetNext(self);
					if o then
						if parent:GetLast(self) == self then
							o:SetCursorPos(0);
						else
							o:SetCursorPos(1);						
						end
					end
				else
					newCurPos = curPos + 1;
				end
			elseif key == "HOME" then
				newCurObj = parent:GetFirst();
				newCurPos = 0;	
			elseif key == "END" then
				newCurObj = parent:GetLast();
				newCurPos = parent:GetLast():GetCharLenght();				
			end	
			
			-- update
			if newText then
				self:SetText(newText);
			end
			
			if not(newText) and newCurPos and IsShiftKeyDown() then
				-- mark text
				--[[
				if not(markFirst) then 
					--markStart = curPos;
					markFirst = curPos;
				end
				if newCurPos > markFirst then
					self:SetMarked(markFirst,newCurPos);
				else
					self:SetMarked(newCurPos,markFirst);
				end]]
				markFromCurTo(newCurObj or self,newCurPos);
				(newCurObj or self):SetCursorPos(newCurPos);
				
			elseif newCurPos and curPos then
				doc:ClearAllMarkings();
				
				(newCurObj or self):SetCursorPos(newCurPos);
				
			end
			
			if newText then
				parent:Update()
			end
		end,
		
		Split = function(self,maxWidth,cutFront) 
			-- cuts the textfild down to a maxwidth of x. 
			-- returns the cut off text. 
			-- if the whole text is cut away _then return nil
			-- if the focus is in the string, return the position.
			
			
			local pos = self:GetWidthPos(maxWidth);
			--print("position at ",maxWidth,"=",pos);
			if pos >= strlenutf8(text) then
				return text,curPos;
			end
			while not(strsubutf8(text,pos+1,pos+1) == " " or pos == strlenutf8(text)) and pos > 0 do
				pos = pos-1;			
			end
			--print("cut to pos",pos);
			if pos > 0 and not(cutFront) then
				local s = strsubutf8(text,pos+2);
				self:SetText(strsubutf8(text,1,pos+1),true);
				local pos;
				-- note the position if it is within the cut area
				
				if curPos and curPos > strlenutf8(text) then
						pos = curPos;		
				end
				return s,pos;
			elseif pos > 0 and cutFront then
				local s = strsubutf8(text,0,pos+1);
				self:SetText(strsubutf8(text,pos+2),true);
				local pos;
				-- note the position if it is within the cut area
				--print( curPos," < ",strlenutf8(text))
				if curPos and curPos < strlenutf8(text) then
						pos = curPos;
				end
				return s,pos;
			else
				return nil, curPos;
			end			
		end,
		
		
		-- marking
		ClearMarked = function(self)
			markStart, markEnd, markFirst = nil,nil,nil;
			gui.marker:Hide();
		end,
		DeleteMarked = function(self) --print("Delete marked");
			if not(markStart and markEnd) then return end;
			
			doc:RemoveMarked(obj);
			if self:IsFullyMarked() then 
				if obj:GotCursorFocus() then
					-- this object will have the cursor position after the deletion
					obj:SetText("");
					obj:SetCursorPos(0);
				else
					-- delete
					
					parent:DeleteObject(obj);
					return; -- important to exit fast, as the object no longer exsists
				end
			else
				--print(markStart,markEnd);
				--print("A",strsubutf8(text,0,markStart))
				--print("B",strsubutf8(text,markEnd));
				self:SetText(strsubutf8(text,0,markStart)..strsubutf8(text,markEnd+1));
				curPos = markStart;
				self:ClearMarked();	
			end
			
		end,
		GetMarkFirstPos = function(self)
			return markFirst;
		end,
		SetMarkFirstPos = function(self,markF)
			markFirst = markF;		
		end,
		SetMarked = function(self,markS,markE)
			if not(markS) then markS = 0 end
			if not(markE) then markE = strlenutf8(text) end
			assert(markS >= 0 and markS <= self:GetCharLenght(),"MarkS out of bounds");
			assert(markE >= 0 and markE <= self:GetCharLenght(),"MarkS out of bounds");
			
			--if not(markStart) then
				--markFirst = markS;			
			--end
			if markS == markE then
				self:ClearMarked();
				return;
			end
			
			doc:RegMarked(obj);
			
			markStart, markEnd = markS,markE;
			
			--print("Marked ",markStart, markEnd)
			
			-- find the coordinate for the string
			gui.label:SetText(strsubutf8(text,0,markStart));
			local xS = gui.label:GetWidth();
			gui.label:SetText(strsubutf8(text,0,markEnd));
			local xE = gui.label:GetWidth()-xS;
			gui.label:SetText(text);
			
			--print(markStart," -> ",markEnd,"=>",xE,"vs",self:GetWidth());
			
			gui.marker:SetWidth(xE);
			gui.marker:SetPoint("TOPLEFT",gui,"TOPLEFT",xS,0);
			gui.marker:SetBackdropColor(1,1,1,1)
			
			gui.marker:Show();
			
			
		end,
		IsFullyMarked = function(self)
			return (markStart == 0 and markEnd == strlenutf8(text));
		end,
		
	}
	
	-- ============== setup ==============
	-- gui (Button)
	parent = _parent;
	gui = CreateFrame("Frame",parent); -- button for registering clicks for marking and positioning
	gui:SetPoint("BOTTOMLEFT",parent:GetGui(),"BOTTOMLEFT",0,0);
	
	
	-- label to display the string
	gui.label = gui:CreateFontString()
	gui.label:SetPoint("TOPLEFT",gui,"TOPLEFT",0,0);
	
	
	
	
	-- edit box for new input
	gui.input = CreateFrame("Editbox",gui);
	gui.input:SetAutoFocus(nil);
	gui.input:SetWidth(1);
	
	gui.input:ClearFocus();
	gui.input:Hide();
	gui.input:SetScript("OnEditFocusGained",function(self)
		doc:ClaimFocus(obj);		
	end);
	gui.input:SetScript("OnEditFocusLost",function(self)
		self:Hide();
		curPos = nil;
		doc:ClearFocus();
	end); 
	
	gui.input:SetScript("OnTextChanged",function(self)
		obj:EnterText(self:GetText());
	end);
	
	-- marker
	gui.marker = CreateFrame("Frame",gui);
	gui.marker:SetBackdrop( { 
		bgFile = "Interface\\CHATFRAME\\CHATFRAMEBACKGROUND", 
		edgeFile = "edgeFile", tile = false, tileSize = 0, edgeSize = 32, 
		insets = { left = 0, right = 0, top = 0, bottom = 0 }
	});
	
	gui.marker:SetBackdropColor(0,0,0,1);
	
	gui.marker:Hide();
	
	-- find document class
	doc = parent; 
	while (not(doc.GetType) or not(doc:GetType()=="GHU_Document")) do
		doc = doc:GetParent();
	end --
	
	obj:SetFont(GameFontNormal);
	obj:SetText("",true);
	
	
	
	--TempBG(obj);
	
	return obj;
end


