-- Call is as follows: Boxes may have children. When layout is called, boxes
-- have their reserved sizes in x,y,width,height. They can then layout their
-- children as best they can inside those. Elements report their desired sizes
-- via getWidth(), getHeight(). If nothing is desired, nil is returned, if
-- percentage of parent is desired, a percentage string is returned. When
-- layout() returns, it can be added to a guichan container by calling
-- "addWidgetTo(container)"

Load("scripts/lib/classes.lua")

function dbgPrint(x)
   -- print(x)
end

-- dark = Color(38, 38, 78)
-- clear = Color(200, 200, 120)
-- black = Color(0, 0, 0)

Element = class(function(instance)
      instance._id = nil
      instance.expands = false
      instance.x = nil
      instance.y = nil
      instance.width = nil
      instance.height = nil
end)

function Element:id(name)
   self._id = name
   return self
end

function Element:setId(container, widget)
   if self._id then
      field,idx = string.match(self._id, "(.+)%[(%d+)%]")
      if field and idx then
         local tbl = container[field] or {}
         tbl[tonumber(idx)] = widget
         container[field] = tbl
      else
         container[self._id] = widget
      end
   end
end

function Element:withWidth(value)
   self.width = value
   return self
end

function Element:withHeight(value)
   self.height = value
   return self
end

function Element:expanding()
   self.expands = true
   return self
end

function Element:getWidth()
   error("Element subclass did not define getWidth")
end

function Element:getHeight()
   error("Element subclass did not define getHeight")
end

function Element:addWidgetTo(container)
   error("Element subclass did not define addWidgetTo")
end

Box = class(Element,
            function(instance, children)
               Element.init(instance)
               instance.paddingX = 0
               instance.paddingY = 0
               instance.children = children
               for i,child in ipairs(children) do
                  -- convenience...
                  if type(child) == "string" then
                     children[i] = LLabel(child)
                  end
               end
            end
)

function Box:withPadding(p, recursive)
   if type(p) == "number" then
      self.paddingX = p
      self.paddingY = p
   else
      self.paddingX = p[1]
      self.paddingY = p[2]
   end
   if recursive then
      for i,child in ipairs(self.children) do
         if child.withPadding then
            if child.paddingX == 0 and child.paddingY == 0 then
               child:withPadding(p, recursive)
            end
         end
      end
   end
   return self
end

function Box:expanding()
   self.expands = true
   return self
end

function Box:getWidth()
   if self.width == nil then
      self:calculateMinExtent()
   end
   return self.width
end

function Box:getHeight()
   if self.height == nil then
      self:calculateMinExtent()
   end
   return self.height
end

Box.DIRECTION_HORIZONTAL = 1
Box.DIRECTION_VERTICAL = 2

function Box:calculateMinExtent()
   local w = 0
   local h = 0
   local horiz = self.direction == Box.DIRECTION_HORIZONTAL
   for i,child in ipairs(self.children) do
      local cw = child:getWidth()
      if type(cw) == "number" then
         if horiz then
            w = w + cw + self.paddingX
         else
            w = math.max(w, cw)
         end
      end
      local ch = child:getHeight()
      if type(ch) == "number" then
         if horiz then
            h = math.max(h, ch)
         else
            h = h + ch + self.paddingY
         end
      end
   end
   self.x = 0
   self.y = 0
   self.width = w + (self.paddingX * 2)
   self.height = h + (self.paddingY * 2)
   dbgPrint("Min: " .. self.width .. " x " .. self.height)
end

function Box:layout()
   dbgPrint("XY: " .. self.x .. " - " .. self.y)
   local horiz = self.direction == Box.DIRECTION_HORIZONTAL

   local padding
   local totalSpace
   if horiz then
      padding = self.paddingX
      totalSpace = self.width - padding * 2
   else
      padding = self.paddingY
      totalSpace = self.height - padding * 2
   end

   local availableSpace = totalSpace - (padding * #self.children)
   local expandingChildren = 0

   for i,child in ipairs(self.children) do
      child.parent = self
      local s
      if horiz then
         s = child:getWidth()
      else
         s = child:getHeight()
      end
      if child.expands then
         expandingChildren = expandingChildren + 1
      end
      if type(s) == "string" then
         local pct = string.match(s, "[0-9]+")
         availableSpace = math.max(availableSpace - (totalSpace * (pct / 100)), 0)
      elseif type(s) == "number" then
         availableSpace = math.max(availableSpace - s, 0)
      elseif s == nil then
         -- boxes with no preference expand to fill available space
         if not child.expands then
            child.expands = true
            expandingChildren = expandingChildren + 1
         end
      else
         error("Invalid child extent: need string, number, or nil")
      end
   end

   local childW = self.width - self.paddingX * 2
   local childH = self.height - self.paddingY * 2
   local expandingChildrenS = 0
   if expandingChildren > 0 then
      expandingChildrenS = availableSpace / expandingChildren
   end
   local xOff = self.x + self.paddingX
   local yOff = self.y + self.paddingY

   for i,child in ipairs(self.children) do
      local s
      if horiz then
         s = child:getWidth()
      else
         s = child:getHeight()
      end
      if type(s) == "string" then
         local pct = string.match(s, "[0-9]+")
         local newS = totalSpace * (pct / 100)
         if child.expands then
            newS = newS + expandingChildrenS
         end
         if horiz then
            childW = newS
         else
            childH = newS
         end
      elseif type(s) == "number" then
         if child.expands then
            s = s + expandingChildrenS
         end
         if horiz then
            childW = s
         else
            childH = s
         end
      elseif w == nil then
         if horiz then
            childW = expandingChildrenS
         else
            childH = expandingChildrenS
         end
      end

      dbgPrint(xOff, yOff, childW, childH)
      child.x = xOff
      child.y = yOff
      child.width = childW
      child.height = childH
      dbgPrint("child: " .. child.width .. "x" .. child.height .. "+" .. child.x .. "+" .. child.y)
      if horiz then
         xOff = xOff + childW + padding
      else
         yOff = yOff + childH + padding
      end
   end
end

function Box:addWidgetTo(container, sizeFromContainer)
   if sizeFromContainer then
      if sizeFromContainer == true then
         self.x = 0 -- containers are relative inside
         self.y = 0
      else
         self.x = sizeFromContainer[1]
         self.y = sizeFromContainer[2]
      end
      self.width = container:getWidth()
      self.height = container:getHeight()
      dbgPrint("startsize:" .. self.width .. "x" .. self.height .. "+" .. self.x .. "+" .. self.y)
   end
   self:layout()
   for i,child in ipairs(self.children) do
      child:addWidgetTo(container)
   end
end

HBox = class(Box,
             function(instance, children)
                Box.init(instance, children)
                instance.direction = Box.DIRECTION_HORIZONTAL
             end
)

VBox = class(Box,
             function(instance, children)
                Box.init(instance, children)
                instance.direction = Box.DIRECTION_VERTICAL
             end
)

LLabel = class(Element,
               function(instance, text, font, center, vCenter)
                  Element.init(instance)
                  instance.label = Label("" .. text)
                  if type(font) == "string" then
                     instance.label:setFont(Fonts[font])
                  else
                     instance.label:setFont(font or Fonts["large"])
                  end
                  instance.label:adjustSize()
                  instance.center = center
                  instance.vCenter = vCenter
               end
)

function LLabel:getWidth()
   return self.label:getWidth()
end

function LLabel:getHeight()
   return self.label:getHeight()
end

function LLabel:layout()
   if self.center or center == nil then -- center text by default
      self.x = self.x + (self.width - self.label:getWidth()) / 2
   end
   if self.vCenter then
      self.y = self.y + (self.height - self.label:getHeight()) / 2
   end
end

function LLabel:addWidgetTo(container)
   self:layout()
   self:setId(container, self.label)
   container:add(self.label, self.x, self.y)
end

LFiller = class(Element)

function LFiller:getWidth()
   return nil
end

function LFiller:getHeight()
   return nil
end

function LFiller:addWidgetTo(container)
   -- nothing
end

LText = class(LLabel,
              function(instance, text)
                 LLabel.init(instance, text, Fonts["game"])
              end
)

LLargeText = class(LLabel)

LImageButton = class(Element,
                     function(instance, caption, hotkey, callback)
                        Element.init(instance)
                        instance.b = ImageButton(caption)
                        instance.b:setHotKey(hotkey)
                        instance.b:setBorderSize(0)
                        instance.b.callback = callback
                        if callback then
                           instance.b:setMouseCallback(function(evtname)
                                 if evtname == "mousePress" then
                                    PlaySound("click")
                                 end
                           end)
                           instance.b:setActionCallback(function()
                                 PlaySound("click")
                                 callback()
                           end)
                        end
                     end
)

function LImageButton:getWidth()
   return self.b:getWidth()
end

function LImageButton:getHeight()
   return self.b:getHeight()
end

function LImageButton:addWidgetTo(container)
   self.b:setSize(self.width, self.height)
   self.setId(container, self.b)
   container:add(self.b, self.x, self.y)
end

LButton = class(LImageButton,
                function(instance, caption, hotkey, callback)
                  LImageButton.init(instance, caption, hotkey, callback)
                  if (GetPlayerData(GetThisPlayer(), "RaceName") == "human") then
                     instance.b:setNormalImage(g_hbln)
                     instance.b:setPressedImage(g_hblp)
                     instance.b:setDisabledImage(g_hblg)
                  else
                     instance.b:setNormalImage(g_obln)
                     instance.b:setPressedImage(g_oblp)
                     instance.b:setDisabledImage(g_oblg)
                  end
                end
)

function LButton:getWidth()
   return 224
end

function LButton:getHeight()
   return 28
end

function LButton:addWidgetTo(container)
   self.b:setSize(self.width, self.height)
   self:setId(container, self.b)
   container:add(self.b, self.x, self.y)
end

LHalfButton = class(LButton)

function LHalfButton:getWidth()
   return 106
end

function LHalfButton:getHeight()
   return 28
end

LSlider = class(Element,
                function(instance, min, max, callback)
                   Element.init(instance)
                   instance.s = Slider(min, max)
                   instance.s:setBaseColor(dark)
                   instance.s:setForegroundColor(clear)
                   instance.s:setBackgroundColor(clear)
                   instance.s.callback = callback
                   if callback then
                      instance.s:setActionCallback(function(s) callback(instance.s, s) end)
                   end
                end
)

function LSlider:getWidth()
   return nil
end

function LSlider:getHeight()
   return nil
end

function LSlider:setValue(val)
   self.s:setValue(val)
   return self
end

function LSlider:addWidgetTo(container)
   self.s:setSize(self.width, self.height)
   self:setId(container, self.s)
   container:add(self.s, self.x, self.y)
end

LListBox = class(Element,
                 function(instance, w, h, list, callback)
                    Element.init(instance)
                    instance.bq = ListBoxWidget(60, 60)
                    instance.bq:setList(list)
                    instance.bq:setBaseColor(black)
                    instance.bq:setForegroundColor(clear)
                    instance.bq:setBackgroundColor(dark)
                    instance.bq:setFont(Fonts["game"])
                    instance.bq.callback = callback
                    if callback then
                      instance.bq:setActionCallback(callback)
                    end
                    list = list or {}
                    instance.bq.itemslist = list
                    instance.width = w
                    instance.height = h
                 end
)

function LListBox:getWidth()
   return self.width
end

function LListBox:getHeight()
   return self.height
end

function LListBox:addWidgetTo(container)
   self.bq:setSize(self.width, self.height)
   self:setId(container, self.bq)
   container:add(self.bq, self.x, self.y)
end

LCheckBox = class(Element,
                  function(instance, caption, callback)
                     Element.init(instance)
                     instance.b = ImageCheckBox(caption)
                     instance.b:setForegroundColor(clear)
                     instance.b:setBackgroundColor(dark)
                     if (GetPlayerData(GetThisPlayer(), "RaceName") == "human") then
                        instance.b:setUncheckedNormalImage(g_hcheckbox_off)
                        instance.b:setUncheckedPressedImage(g_hcheckbox_off2)
                        instance.b:setCheckedNormalImage(g_hcheckbox_on)
                        instance.b:setCheckedPressedImage(g_hcheckbox_on2)
                     else
                        instance.b:setUncheckedNormalImage(g_ocheckbox_off)
                        instance.b:setUncheckedPressedImage(g_ocheckbox_off2)
                        instance.b:setCheckedNormalImage(g_ocheckbox_on)
                        instance.b:setCheckedPressedImage(g_ocheckbox_on2)
                     end
                     instance.b.callback = callback
                     if callback then
                        instance.b:setActionCallback(function(s) callback(instance.b, s) end)
                     end
                     instance.b:setFont(Fonts["game"])
                     instance.b:adjustSize()
                  end
)

function LCheckBox:getWidth()
   return self.b:getWidth()
end

function LCheckBox:getHeight()
   return self.b:getHeight()
end

function LCheckBox:addWidgetTo(container)
   self.b:setSize(self.width, self.height)
   self:setId(container, self.b)
   container:add(self.b, self.x, self.y)
end

function LCheckBox:setMarked(flag)
   self.b:setMarked(flag)
   return self
end

LTextInputField = class(Element,
                        function(instance, text, callback)
                           Element.init(instance)
                           instance.b = TextField(text)
                           instance.b.callback = callback
                           if callback then
                              instance.b:setActionCallback(callback)
                           end
                           instance.b:setFont(Fonts["game"])
                           instance.b:setBaseColor(clear)
                           instance.b:setForegroundColor(clear)
                           instance.b:setBackgroundColor(dark)
                        end
)

function LTextInputField:getWidth()
   return nil
end

function LTextInputField:getHeight()
   return 10
end

function LTextInputField:addWidgetTo(container)
   self.b:setSize(self.width, self.height)
   self:setId(container, self.b)
   container:add(self.b, self.x, self.y)
end

LDropDown = class(Element,
                  function(instance, list, callback)
                     Element.init(instance)
                     local dd = ImageDropDownWidget()
                     if (GetPlayerData(GetThisPlayer(), "RaceName") == "human") then
                        dd:setItemImage(g_hibar)
                        dd:setDownNormalImage(g_hdslider_n)
                        dd:setDownPressedImage(g_hdslider_p)
                     else
                        dd:setItemImage(g_oibar)
                        dd:setDownNormalImage(g_odslider_n)
                        dd:setDownPressedImage(g_odslider_p)
                     end
                     dd:setFont(Fonts["game"])
                     dd:setList(list)
                     dd.list = list
                     dd.callback = callback
                     dd:setActionCallback(function(s) callback(dd, s) end)
                     dd:setBaseColor(dark)
                     dd:setForegroundColor(clear)
                     dd:setBackgroundColor(dark)
                     instance.dd = dd
                  end
)

function LDropDown:getWidth()
   return self.width or 60
end

function LDropDown:getHeight()
   return 8
end

function LDropDown:addWidgetTo(container)
   self.dd:setSize(self.width, self.height)
   self:setId(container, self.dd)
   container:add(self.dd, self.x, self.y)
end

LTextBox = class(Element,
                 function(instance, text)
                    Element.init(instance)
                    instance.b = TextBox(text)
                    instance.b:setFont(Fonts["game"])
                    instance.b:setBaseColor(clear)
                    instance.b:setForegroundColor(clear)
                    instance.b:setBackgroundColor(dark)
                    instance.scroll = ScrollArea()
                    instance.scroll:setContent(instance.b)
                    instance.scroll:setBaseColor(clear)
                    instance.scroll:setForegroundColor(clear)
                    instance.scroll:setBackgroundColor(dark)
                 end
)

function LTextBox:getWidth()
   return nil
end

function LTextBox:getHeight()
   return nil
end

function LTextBox:addWidgetTo(container)
   self.scroll:setSize(self.width, self.height)
   self.b:setSize(self.width, self.height)
   self:setId(container, self.b)
   container:add(self.scroll, self.x, self.y)
end
