/***************************************************************************
                          SIMPLY POWERFUL TOOLKIT (SPTK)
                          CTabs.cpp  -  description
                             -------------------
    begin                : Tue Feb 26 2002
    version              : 0.98, Jun 1 2002
    copyright            : (C) 2002-2005 by Alexey S.Parshin
    email                : alexeyp@gmail.com
 ***************************************************************************/

/***************************************************************************
   This library is free software; you can redistribute it and/or modify it 
   under the terms of the GNU Library General Public License as published by
   the Free Software Foundation; either version 2 of the License, or (at 
   your option) any later version.

   This library is distributed in the hope that it will be useful, but 
   WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library
   General Public License for more details.

   You should have received a copy of the GNU Library General Public License
   along with this library; if not, write to the Free Software Foundation, 
   Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.

   Please report all bugs and problems to "alexeyp@gmail.com"
 ***************************************************************************/

#include <FL/Fl.H>
#include <FL/fl_draw.H>

#include <string>
#include <vector>
#include <algorithm>

#include <sptk3/CScroll.h>
#include <sptk3/CTabs.h>
#include <sptk3/CScroll.h>

#define BORDER 10
#define TABSLOPE 5
#define EXTRASPACE 2
#define TAB_ROW_HEIGHT 20

using namespace std;

class CTabInfo {
public:
   int       x, y, width, height;
   int       id;
   Fl_Group *group;
public:
   CTabInfo(int x,int y,int width,int height,int id,Fl_Group *group);
   CTabInfo(const CTabInfo& );
   bool includesCoord(int x,int y);
   void displayWidget(Fl_Widget *w);
};

typedef vector<CTabInfo *> CTabsVector;

class CTabsList : public CTabsVector {
   friend class CTabsMatrix;
   friend class CTabs;
private:
   int         m_active;
protected:
   void      sety(int ycoord);
   CTabInfo *tabAt(int x,int y);
   void      extend(int width);
public:
   CTabsList();
   ~CTabsList() { clear(); }
   void      clear();
   int       active()                         { return m_active; }
   void      active(int activeIndex)          { m_active = activeIndex; }
   CTabInfo *activeTab();
   //int       indexOf(CTabInfo *ti);
};

class CTabsMatrix : public vector<CTabsList *> {
   friend class CTabs;
protected:
   CTabInfo  *m_activeTab;

   CTabInfo *tabAt(int x,int y);
public:
   CTabsMatrix(CTabs * multiTabs) { m_activeTab = 0L; }
   ~CTabsMatrix() { clear(); }

   void     clear();
   void     activate(int rowIndex,int tabIndex);
   void     activate(CTabInfo *);
};

CTabInfo::CTabInfo(int tx,int ty,int twidth,int theight,int tid,Fl_Group *tgroup) {
   x = tx;
   y = ty;
   width = twidth;
   height = theight;
   id = tid;
   group = tgroup;
}

CTabInfo::CTabInfo(const CTabInfo& ti) {
   x = ti.x;
   y = ti.y;
   width = ti.width;
   height = ti.height;
   id = ti.id;
   group = ti.group;
}

bool CTabInfo::includesCoord(int xx,int yy) {
   return ( xx > x && xx < x + width && yy > y && yy < y + height );
}

void CTabInfo::displayWidget(Fl_Widget *w) {
   Fl_Widget *p = w;
   do {
      p = p->parent();
      if (p == NULL)
         return;
   } while (p != group);

   // check if the tab is Fl_Scroll descendent
   CScroll *scroll = dynamic_cast<CScroll *>(group);
   if (scroll) {
      // check the position of the control bottom
      int controlTop    = w->y();
      int controlBottom = controlTop+w->h();
      int scrollTop     = scroll->y();
      int scrollBottom  = scrollTop + scroll->h();
      int xposition     = scroll->xposition();
      int yposition     = scroll->yposition();

      int dy = 0;

      if (controlBottom > scrollBottom)
         dy = controlBottom - scrollBottom;

      if (controlTop - dy < scrollTop)
         dy = controlTop - scrollTop;

      yposition    += dy;

      scroll->position(xposition,yposition);
   }
}

CTabsList::CTabsList()  {
   m_active = -1;
}

void CTabsList::clear() {
   for (iterator itor = begin(); itor != end(); itor++)
      delete *itor;
   CTabsVector::clear();
}

void CTabsList::extend(int width) {
   if (size()) return;
   CTabInfo& ti = *(*this)[size()-1];
   int totalWidth = ti.x + ti.width;
   float extendX = (width - totalWidth) / float(size());
   for (unsigned t = 0; t < size(); t++) {
      CTabInfo& ti = *(*this)[t];
      ti.x += int( t * extendX + 0.5 );
      ti.width += int( extendX );
   }
}

CTabInfo *CTabsList::activeTab() {
   if (m_active == -1) return 0;
   return (*this)[m_active];
}

void CTabsList::sety(int ycoord) {
   for (unsigned i=0; i < size(); i++) 
      (*this)[i]->y = ycoord;
}

CTabInfo *CTabsList::tabAt(int x,int y) {
   for (unsigned i=0; i < size(); i++) {
      CTabInfo& tab = *(*this)[i];
      if (tab.includesCoord(x,y))
         return &tab;
   }
   return 0;
}
/*
int CTabsList::indexOf(CTabInfo *ti) {
   for (unsigned i=0; i < m_count; i++) {
      CTabInfo * tab = m_list[i];
      if (tab == ti)
         return i;
   }
   return -1;
}
*/

void CTabsMatrix::clear() {
   for (iterator itor = begin(); itor != end(); itor++)
      delete *itor;
   vector<CTabsList *>::clear();
}

void CTabsMatrix::activate(int rowIndex,int tabIndex) {
   // find currently active row
   if (!size()) return;
   unsigned activeRowIndex = size()-1;
   CTabsList& activeRow = *(*this)[activeRowIndex];

   CTabsList& row = *(*this)[rowIndex];
   int ycoord = TAB_ROW_HEIGHT * activeRowIndex;

   if (size() > 1)
      row.sety(ycoord);
   row.active(tabIndex);
   if ((int)activeRowIndex != rowIndex) {
        // move row coordinates for the currently active row
      ycoord = TAB_ROW_HEIGHT * rowIndex;
      if (size() > 1)
         activeRow.sety(ycoord);
      activeRow.active(-1);
      // swap the rows visually
      (*this)[rowIndex] = &activeRow;
      (*this)[activeRowIndex] = &row;
   }
   m_activeTab = row.activeTab();
}

void CTabsMatrix::activate(CTabInfo *ti) {
   // find tab's row
   for (unsigned i=0; i < size(); i++) {
      CTabsList& tabsList = *(*this)[i];
      CTabsVector::iterator itor = find(tabsList.begin(),tabsList.end(),ti);
      if (itor != tabsList.end()) {
         int tabIndex = itor - tabsList.begin();
         activate(i,tabIndex);
         break;
      }
   }
}

CTabInfo *CTabsMatrix::tabAt(int x,int y) {
   for (unsigned i=0; i < size(); i++) {
      CTabInfo *tab = (*this)[i]->tabAt(x,y);
      if (tab)
         return tab;
   }
   return 0;
}

void CTabs::ctor_init() {
   m_tabsMatrix = new CTabsMatrix(this);
   push_ = 0;
   m_showTabs = true;
   m_autoColorIndex = 0;
   m_tabsMode = CTM_TOP;
   m_tabsWidth = m_tabsHeight = 0;
   box(FL_THIN_UP_BOX);
}

CTabs::CTabs(const char * label,int layoutSize,CLayoutAlign layoutAlignment) 
: CGroup(label,layoutSize,layoutAlignment) {
   ctor_init();
}

#ifdef __COMPATIBILITY_MODE__    
CTabs::CTabs(int X,int Y,int W, int H,const char *l)
: CGroup(X,Y,W,H,l) {
   ctor_init();
}
#endif

CTabs::~CTabs() {
   delete m_tabsMatrix;
}

// this is used by fluid to pick tabs:
Fl_Widget *CTabs::which(int event_x, int event_y) {
   CTabInfo *tab = m_tabsMatrix->tabAt(event_x-x(),event_y-y());
   if (tab && m_showTabs) {
      //m_tabsMatrix->activate(tab);
      return tab->group;
   }
   return 0;
}

bool CTabs::mouseEventOnTabs() {
   if (!m_showTabs) return false;
   switch (m_tabsMode) {
      case CTM_TOP:  
         if (Fl::event_y() > y()+m_tabsHeight) 
            return false;
         break;
      case CTM_RIGHT: 
         if (Fl::event_x() < x()+w()-m_tabsWidth) 
            return false;
         break;
   }
   return true;
}

int CTabs::handle(int event) {

   Fl_Widget  *o;
   int         i;
   CTabInfo   *tab;

   switch (event) {

      case FL_PUSH:
         if (mouseEventOnTabs()) {
            if (Fl::visible_focus()) Fl::focus(this);
         } else {
            return Fl_Group::handle(event);
         }
         break;

      case FL_DRAG:
      case FL_RELEASE:
         if (mouseEventOnTabs()) {
            if (Fl::visible_focus()) Fl::focus(this);
            int eventX = Fl::event_x();
            int eventY = Fl::event_y();
            o = which(eventX,eventY);
            if (event == FL_RELEASE) {
               push(0);
               if (o && value(o))
                  do_callback();
            }
            else push(o);
            tab = m_tabsMatrix->tabAt(eventX-x(),eventY-y());
            if (tab) 
               m_tabsMatrix->activate(tab);
            return 1;
         } else {
            return Fl_Group::handle(event);
         }
         break;

      case FL_FOCUS:
      case FL_UNFOCUS:
         if (Fl::visible_focus()) {
            redraw();
            return 1;
         } else return 0;

      case FL_KEYBOARD:
         switch (Fl::event_key()) {
            case FL_Left:
               if (child(0)->visible()) return 0;
               for (i = 1; i < children(); i ++)
                  if (child(i)->visible()) break;
               value(child(i - 1));
               do_callback();
               return 1;
            case FL_Right:
               if (child(children() - 1)->visible()) return 0;
               for (i = 0; i < children(); i ++)
                  if (child(i)->visible()) break;
               value(child(i + 1));
               do_callback();
               return 1;
            case FL_Down:
               redraw();
               return Fl_Group::handle(FL_FOCUS);
            default:
               break;
         }

      case FL_KEYUP:
         tab = m_tabsMatrix->m_activeTab;
         if (!tab)
            return Fl_Group::handle(event);
         switch (Fl::event_key()) {
            case FL_Tab:
            case FL_Left:
            case FL_Right:
            case FL_Up:
            case FL_Down:
               tab->displayWidget(Fl::focus());
         }
         return Fl_Group::handle(event);

      default:
         return Fl_Group::handle(event);
   }
   return 0;
}

int CTabs::push(Fl_Widget *o) {
   //if (push_ == o) return 0;
   if (push_ && !push_->visible() || o && !o->visible())
      damage(FL_DAMAGE_EXPOSE);
   push_ = o;
   return 1;
}

// The value() is the first visible child (or the last child if none
// are visible) and this also hides any other children.
// This allows the tabs to be deleted, moved to other groups, and
// show()/hide() called without it screwing up.
Fl_Widget* CTabs::value() {
   Fl_Widget* v = 0;
   Fl_Widget*const* a = array();
   for (int i=children(); i--;) {
      Fl_Widget* o = *a++;
      if (v) o->hide();
      else if (o->visible()) v = o;
      else if (!i) {o->show(); v = o;}
   }
   return v;
}

int CTabs::pageNumber() const {
   unsigned cnt = children();
   for (unsigned i = 0; i < cnt; i++) {
      Fl_Widget* o = child(i);
      if (o->visible())
         return i;
   }
   return -1;
}

void CTabs::pageNumber(unsigned pageNo) {
   unsigned cnt = children();
   if (pageNo >= cnt) return;
   for (unsigned i = 0; i < cnt; i++) {
      Fl_Widget* o = child(i);
      if (i == pageNo)
         o->show();
      else o->hide();
   }
}

// Setting the value hides all other children, and makes this one
// visible, iff it is really a child:
int CTabs::value(Fl_Widget *newvalue) {
   Fl_Widget*const* a = array();
   int ret = 0;
   for (int i=children(); i--;) {
      Fl_Widget* o = *a++;
      if (o == newvalue) {
         if (!o->visible()) ret = 1;
         o->show();
      } else {
         o->hide();
      }
   }
   return ret;
}

enum {LEFT, RIGHT, SELECTED};

void CTabs::draw() {
   Fl_Widget *v = value();

   switch (m_tabsMode) {
      case CTM_TOP:
         fl_clip (x(), y()+m_tabsHeight, w(), h()-m_tabsHeight);
         break;
      case CTM_RIGHT:
         fl_clip (x(), y(), w()-m_tabsWidth, h());
         break;
   }

   if (damage() & FL_DAMAGE_ALL) {
      // redraw the entire thing
      fl_color(color());
      draw_box(box(), x(), y(), w(), h(), v ? v->color() : color());
      if (v) draw_child(*v);
   } else { 
      // redraw the child
      if (v) update_child(*v);
   }

   fl_pop_clip();

   if (damage() & (FL_DAMAGE_EXPOSE|FL_DAMAGE_ALL)) {
      unsigned rowCount = m_tabsMatrix->size();
      for (unsigned r = 0; r < rowCount; r++) {
         CTabsList& row = *(*m_tabsMatrix)[r];
         int rowTabCount = row.size();
         for (int t = 0; t < rowTabCount; t++) {
            CTabInfo *tab = row[t];
            if (row.active()==t)
               draw_tab(tab,true);
            else  draw_tab(tab,row.active()==t);
         }
      }
   }
}

void CTabs::draw_tab(CTabInfo *tab, int selected) {
   if (!m_showTabs) return;

   fl_color(!selected ? fl_color_average(tab->group->color(),FL_BLACK,0.85f) : tab->group->color() );

    // Draw the background of the tab
   int x1 = x() + tab->x;
   int x2 = x1  + tab->width;
   int y1 = y() + tab->y;
   int y2 = y() + m_tabsHeight;

   switch (m_tabsMode) {
      case CTM_TOP:
         fl_begin_polygon();
         fl_vertex(x1+2, y2+selected);
         fl_vertex(x1+2, y1+TABSLOPE);
         fl_vertex(x1+2+TABSLOPE, y1);
         fl_vertex(x2, y1);
         fl_vertex(x2, y2+selected);
         fl_end_polygon();
         break;

      case CTM_RIGHT:
         y2 = y1 + tab->height - 3;
         fl_rectf(x1,y1,tab->width,tab->height-3);
         break;
   }

    // Draw the front-top highlighted edge of the tab
    //fl_color(!selected && tab->group==push_ ? fl_darker(tab->group->color()) : fl_lighter(tab->group->color()));
   fl_color(fl_lighter(fl_lighter(tab->group->color())));

   Fl_Align labelAlign = FL_ALIGN_CENTER;
   int      labelLeft = 0;

   int wt = 0,ht = 0;
   tab->group->measure_label(wt,ht);

   switch (m_tabsMode) {
      case CTM_TOP:
         fl_line(x1+2, y2, x1+2, y1+TABSLOPE, x1+2+TABSLOPE, y1);
         fl_line(x1+2+TABSLOPE, y1, x2, y1);
         if (selected) {
            if (x1>x()) fl_xyline(x(), y2, x1);
            if (x2+TABSLOPE < x()+w()-1) 
               fl_xyline(x2, y2, x()+w()-1);
         }
         fl_color(fl_darker(tab->group->color()));
         fl_line(x2, y1, x2, y2);
         labelLeft = x1 + 6 + (tab->width - 6 - wt) / 2;
         break;

      case CTM_RIGHT:
         fl_line(x1,y1,x2,y1);
         fl_color(fl_darker(tab->group->color()));
         if (selected) {
            fl_line(x1-1, y(), x1-1, y1-1);
            fl_line(x1-1, y2, x1-1, y()+h());
         }
         fl_line(x2,y1,x2,y2,x1,y2);
         labelAlign = FL_ALIGN_LEFT;
         labelLeft = x1 + 4;
         break;
   }

   tab->group->draw_label(labelLeft,y1+2,wt,tab->height-2,labelAlign);

   if (Fl::focus() == this && selected)
      draw_focus(box(), labelLeft - 3, y1+2, wt + 6, ht + 4);
}

void CTabs::resizeTabs() {
   int wt = 0, ht = 0;
   Fl_Widget* const* a = array();

   CTabInfo       *activeTab = 0L;

   m_tabsMatrix->clear();

   CTabsList *row = new CTabsList;
   m_tabsMatrix->push_back(row);

   m_tabsWidth = 0;
   if (m_tabsMode == CTM_RIGHT) // find out the longest tab label, if we have the vertical stack
      for (int i=0; i<children(); i++) {
         Fl_Widget * group = *a++;
         wt = 0;
         if (group->label()) {
            group->measure_label(wt,ht);
            wt += 6;
            if (wt > m_tabsWidth) m_tabsWidth = wt;
         }
      }

   int xpos = 0;
   int ypos = 0;
   int ystep = 0;
   if (m_showTabs)
      ystep = TAB_ROW_HEIGHT;

   switch (m_tabsMode) {
      case CTM_TOP:
         m_tabsWidth = 0;
         m_tabsHeight = 0; // to be determined
         break;
      case CTM_RIGHT:
         m_tabsWidth += 6;
         m_tabsHeight = 0;
         ypos = 3;
         xpos = w() - m_tabsWidth;
         break;
   }

   a = array();
   for (int i=0; i<children(); i++) {
      Fl_Widget *group = *a++;
      wt = 0;

      CTabInfo *tab = 0;

      switch (m_tabsMode) {
         case CTM_TOP:
            if (group->label())
               group->measure_label(wt,ht);
            wt += 10;
            if (xpos + wt > w()) {
               if (row->size() > 0) {
                  xpos = 0;
                  ypos += ystep;
                  row = new CTabsList;
                  m_tabsMatrix->push_back(row);
               } else {
                  wt = w() - xpos;
               }
            }
            tab  = new CTabInfo(xpos,ypos,wt,TAB_ROW_HEIGHT,i,(Fl_Group *)group);
            row->push_back(tab);
            if (group->visible())
               m_tabsMatrix->m_activeTab = tab;
            xpos += wt;
            break;
         case CTM_RIGHT:
            if (ypos + ystep < h()) {
               tab  = new CTabInfo(xpos,ypos,m_tabsWidth,TAB_ROW_HEIGHT,i,(Fl_Group *)group);
               row->push_back(tab);
            }
            ypos += ystep;
            break;
      }

      if (!tab) break;

      if (group->visible()) {
         activeTab = tab;
      }
   }

   switch (m_tabsMode) {
      case CTM_TOP:
         m_tabsHeight = ypos + ystep;
         m_tabsMatrix->activate(activeTab);
         extendTabs();
         break;
      default:
         m_tabsMatrix->activate(activeTab);
         break;
   }
}

void CTabs::resize(int xx, int yy, int ww, int hh) {
   CGroup::resize(xx,yy,ww,hh);

   resizeTabs();

   int group_x = xx + Fl::box_dx(box());
   int group_y = yy + m_tabsHeight + Fl::box_dy(box());
   int group_w = ww - Fl::box_dw(box()) - m_tabsWidth;
   int group_h = hh - m_tabsHeight - Fl::box_dh(box());

   unsigned cnt = children();
   for (unsigned i = 0; i < cnt; i++) {
      Fl_Widget *group = child(i);
      CLayoutManager *layout = dynamic_cast<CLayoutManager *>(group);
      if (layout) layout->layoutGrowMode(LGM_NONE); 

      group->resize(group_x,group_y,group_w,group_h);
   }
}

void CTabs::extendTabs() {
   unsigned cnt = m_tabsMatrix->size();
   for (unsigned r = 0; r < cnt; r++) {
      (*m_tabsMatrix)[r]->extend(w()-2);
   }
}

const Fl_Color CTabs::AutoColorTable[16] = {
   fl_rgb_color(0xB0,0xD0,0xD0),
   fl_rgb_color(0xC0,0xC0,0xE0),
   fl_rgb_color(192,176,160),
   fl_rgb_color(0xD0,0xD0,0xB0),
   fl_rgb_color(240,190,190),
   fl_rgb_color(0xC0,0xB0,0xC0),
   fl_rgb_color(0xC0,0xA0,0x90),
   fl_rgb_color(0xD0,0xD0,0xE8),
   fl_rgb_color(0xE8,0xC0,0xC0),

   fl_rgb_color(0xC0,0xE8,0xC0),
   fl_rgb_color(0xE8,0xC0,0xE8),
   fl_rgb_color(0xE0,0xE0,0xC0),
   fl_rgb_color(0xC0,0xE0,0xE0),
   fl_rgb_color(0xE0,0xC0,0xE0),
   fl_rgb_color(0xA0,0xB8,0xA0),
   fl_rgb_color(0xB8,0xC0,0xE8)
};

Fl_Group* CTabs::newScroll(const char *label,bool autoColor) {

   Fl_Group* group = lastTab();
   if (group && !group->children()) {
      remove(group);
      delete group;
   }

   begin();
   CScroll *scroll = new CScroll(label,10,SP_ALIGN_NONE);
   scroll->scrollbar.visible_focus(false);
   scroll->hscrollbar.visible_focus(false);

   scroll->box(FL_FLAT_BOX);
   if (tabsCount() > 1)
      scroll->hide();
   if (autoColor) {
      Fl_Color clr = AutoColorTable[m_autoColorIndex&0xF];
      scroll->color(clr);
      m_autoColorIndex++;
   }
   return scroll;
}

Fl_Group* CTabs::newPage(const char *label,bool autoColor) {
   Fl_Group* group = lastTab();
   if (group && !group->children()) {
      remove(group);
      delete group;
   }

   begin();
   group = new CGroup(label,10,SP_ALIGN_NONE);

   group->box(FL_FLAT_BOX);
   if (tabsCount() > 1)
      group->hide();
   if (autoColor) {
      Fl_Color clr = AutoColorTable[m_autoColorIndex&0xF];
      group->color(clr);
      m_autoColorIndex++;
   }
   return group;
}

Fl_Group* CTabs::lastTab() {
   // find the last children
   int i = children() - 1;
   if (i < 0) return 0;
   return (Fl_Group*) child(i);
}

void CTabs::showTabs(bool st) {
   if (m_showTabs != st) {
      m_showTabs = st;
      if (parent())
         parent()->damage(FL_DAMAGE_ALL);
   }
}

void CTabs::tabsMode(CTabsMode tm) {
   if (m_tabsMode != tm) {
      m_tabsMode = tm;
      if (parent())
         parent()->damage(FL_DAMAGE_ALL);
   }
}

unsigned CTabs::tabsCount() const {
   unsigned cnt = children();
   unsigned tcount = 0;
   for (unsigned i = 0; i < cnt; i++) {
      if (dynamic_cast<Fl_Group *>(child(i)))
         tcount++;
   }
   return tcount;
}
