/*
 * $Id$
 *
 * EDE Browser class
 * Part of edelib.
 * Copyright (c) 2005-2007 EDE Authors.
 *
 * This program is licenced under terms of the 
 * GNU General Public Licence version 2 or newer.
 * See COPYING for details.
 */


#include "EDE_Browser.h"
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>

#include <Fl/Fl.H>
#include <Fl/Fl_Window.H>



// ---------------- SORT ----------------


// convert size string to float - used for sorting files by size
double sizetof(char *s) {
	double d=atof(s);
	if (strchr(s,'g') || strchr(s,'G'))
		d*=1024*1024*1024;
	else if (strchr(s,'m') || strchr(s,'M'))
		d*=1024*1024;
	else if (strchr(s,'k') || strchr(s,'K'))
		d*=1024;
	return d;
}

// Sort function - return true if first is larger then second, otherwise false
bool EDE_Browser::sortfn(char*s1,char*s2,SortType type) {
	switch(type) {
	  case ALPHA_SORT:
		return (strcmp(s1,s2)>0);
	  case ALPHA_CASE_SORT:
		return (strcasecmp(s1,s2)>0);
	  case NUMERIC_SORT:
		return (atof(s1)>atof(s2));
	  case DATE_SORT:
		// unimplemented
		// TODO: use edelib::Date
		return false;
	  case FILE_SIZE_SORT:
		return (sizetof(s1)>sizetof(s2));
	  default:
		return false; // NO_SORT
	}
}


// Optimized quick sort algorithm 
void EDE_Browser::mqsort(char* arr[], int beg, int end, SortType type) {
	bool k = sort_direction ? false : true;
	if (end > beg + 1)
	{
		char* piv = arr[beg]; int l = beg + 1, r = end;
		while (l < r)
		{
			if (k^sortfn(arr[l],piv,type)) // ^ is XOR
				l++;
			else if (l==r-1)  // avoid costly swap if they're the same
				r--;
			else {
				swap(--r,l); // Fl_Browser::swap()
				char *tmp=arr[l]; // update array
				arr[l]=arr[r];
				arr[r]=tmp;
			}
		}
		// avoid costly swap if they're the same
		if (beg==l-1) 
			l--;
		else {
			swap(--l,beg);
			char*tmp=arr[beg]; // update array
			arr[beg]=arr[l];
			arr[l]=tmp;
		}

		// recursion
		mqsort(arr, beg, l, type);
		mqsort(arr, r, end, type);
	}
}



// callback for header buttons
void header_callback(Fl_Widget*w, void*) {
	Fl_Group* heading = (Fl_Group*)w->parent();
	EDE_Browser* browser = (EDE_Browser*)w->parent()->parent();
	for (int i=heading->children(); i--; )
		if (w == heading->child(i)) { browser->sort(i); break; }
}

// Toggle-type method
void EDE_Browser::sort(int column) {
	SortType t = column_sort_types_[column];
	if (t==NO_SORT) t=ALPHA_SORT;
	if (column!=sort_column || sort_type==NO_SORT)
		sort(column, t, false);
	else
		sort(column, sort_type, !sort_direction);
}

// Real sorting method with three parameters
#define SYMLEN 6
void EDE_Browser::sort(int column, SortType type, bool reverse) {
	char *h=column_header_;
	int hlen=strlen(h);
	char colchar = Fl_Icon_Browser::column_char();

	// FIXME sort() shouldn't call column_header() because that calls show_header() and that
	// deletes all buttons from header (so they could be recreated). This cause valgrind errors
	// since sort is called in button callback - you can't delete a widget in its own callback

	// Remove old sort direction symbol (if any) from header
	char *delim = 0;
	int col=0;
	if (sort_type != NO_SORT) {
		bool found=false;
		while ((delim=strchr(h, colchar))) {
			if (col==sort_column) {
				for (uint i=0; i<=strlen(delim); i++) delim[i-SYMLEN+1]=delim[i];
				found=true;
				break;
			}
			h=delim+1;
			col++;
		}
		if (!found && col==sort_column)
			column_header_[hlen-SYMLEN+1]='\0';
		h=column_header_;
		delim = 0;
		col=0;
	}

	// Add new symbol
	char *newheader = new char[hlen+6];
	if (type != NO_SORT) {
		// Construct symbol
		char sym[SYMLEN];
		if (reverse) snprintf(sym,SYMLEN,"@-22<");
		else snprintf(sym,SYMLEN,"@-22>");

		// Find column
		bool found=false;
		while ((delim=strchr(h, colchar))) {
			if (col==column) {
				*delim='\0';
				snprintf(newheader,hlen+SYMLEN,"%s%s\t%s",column_header_,sym,delim+1);
				found=true;
				break;
			}
			h=delim+1;
			col++;
		}
		if (!found && col==column) // Just append symbol to string
			snprintf(newheader, hlen+SYMLEN,"%s%s",column_header_,sym);
	} else {
		strncpy (newheader, column_header_, hlen);
		newheader[hlen]='\0';
	}
	column_header(newheader);
	delete[] newheader;
	sort_column=column; sort_type=type; sort_direction=reverse;

	// Start actually sorting
	if (type != NO_SORT) {
		// First create an array of strings in a given column
		char** sorttext = (char**)malloc(sizeof(char*)*(size()+1)); // index starts from 1 for simplicity in mqsort
		for (int i=1;i<=size();i++) {
			char *tmp = strdup(text(i));
			char *l = tmp;
			int col=0;
			while ((delim=strchr(l, Fl_Icon_Browser::column_char()))) {
				*delim = '\0';
				if (col==column) break;
				l=delim+1;
				col++;
			}
			sorttext[i] = strdup(l);
			free(tmp);
		}

		mqsort(sorttext, 1, size()+1, type);
		redraw();

		// Free the allocated memory
		for (int i=1; i<=size(); i++) free (sorttext[i]);
		free(sorttext);
	}
}


// --------------------------------------


static void scroll_cb(Fl_Widget* w, void*) {
	Fl_Scrollbar *s = (Fl_Scrollbar*)w;
	EDE_Browser *b = (EDE_Browser*)w->parent();
	b->hposition(s->value());
}


// ctor
EDE_Browser::EDE_Browser(int X,int Y,int W,int H,const char *L) : Fl_Icon_Browser(X,Y,W,H),
	  totalwidth_(0), column_header_(0), sort_column(0), sort_type(NO_SORT), sort_direction(false) {

	thegroup = new Fl_Group(X,Y,W,H);
	thegroup->begin();
		heading = new Heading(0,0,W,buttonheight);
 		heading->box(FL_FLAT_BOX); // draw heading background
		heading->align(FL_ALIGN_CLIP);
		heading->end();
		heading->hide();
		heading->parent(this); // for callback
	
		hscrollbar = new Fl_Scrollbar(1, H-Fl::scrollbar_size(), W-Fl::scrollbar_size()-3, Fl::scrollbar_size()); // take account for edges
		hscrollbar->type(FL_HORIZONTAL);
		hscrollbar->hide();
		hscrollbar->parent(this); // for callback
		hscrollbar->callback(scroll_cb);
	thegroup->end();
	thegroup->add(this);

	has_scrollbar(VERTICAL);

	resizable(0);
	thegroup->resizable(this);
	thegroup->align(FL_ALIGN_CLIP);

	// EDE_Browser is always a multiple-selection browser 
	type(FL_MULTI_BROWSER);

	column_sort_types_ = new SortType[256]; // 256 columns should be enough for anyone (tm)
	for (int i=0; i<256; i++) column_sort_types_[i]=NO_SORT;
}


// Deallocate all memory used by header labels
void EDE_Browser::cleanup_header() {
	// Deallocate old button labels
	for (int i=0; i<heading->children(); i++) {
		char *l = (char*)heading->child(i)->label();
		if (l && l[0]!='\0') free(l);
	}
	heading->clear();
}

//make buttons invisible
void EDE_Browser::hide_header() {
	if (heading->visible()) resize(x(),y()-buttonheight,w(),h()+buttonheight);
	cleanup_header();
	heading->hide();
}

// Regenerate and display header
void EDE_Browser::show_header() {
	int button_x=0;
	char *hdr = column_header_;
	const int* l = Fl_Icon_Browser::column_widths();
	cleanup_header();
	for (int i=0; i==0||l[i-1]; i++) {
		// If the button is last, calculate size
		int button_w = l[i];
		if (button_w == 0) button_w = totalwidth_-button_x;

		// Get part of header until delimiter char
		char *delim = 0;
		Fl_Button *b;

		if (hdr) {
			delim = strchr(hdr, Fl_Icon_Browser::column_char());
			if (delim) *delim='\0'; // temporarily
			b=new Fl_Button(button_x,heading->y(),button_w,buttonheight,strdup(hdr));
		} else {
			b=new Fl_Button(button_x,heading->y(),button_w,buttonheight,"");
		}

		b->align(FL_ALIGN_INSIDE|FL_ALIGN_LEFT|FL_ALIGN_CLIP);
		b->callback(header_callback);
		b->labelsize(12); // FIXME: hack for label size
		//b->labelcolor(FL_DARK3);
		heading->add(b);
		button_x += l[i];

		if (delim) {
			*delim=Fl_Icon_Browser::column_char(); // put back delimiter
			hdr=delim+1; // next field
		}
	}
	if (!heading->visible()) resize(x(),y()+buttonheight,w(),h()-buttonheight);
	heading->resizable(0); // We will resize the heading and individual buttons manually
	heading->show();
	heading->redraw(); //in case it was already visible
}

// Subclassed to resize heading and browser if neccessary
void EDE_Browser::column_widths(const int* l) {
	totalwidth_=0;
	int i; 
	for (i=0; l[i]; i++) totalwidth_+=l[i];
//	if (total>=scroll->w()) {
//		Fl_Icon_Browser::size(total,h());
//	}
	// If there was heading before, regenerate
	if (heading->visible())
		heading->size(totalwidth_,buttonheight);
//		show_header();

	// Second array for the Fl_Browser
	static int *tmp = 0;
	if (tmp) delete[] tmp;
	tmp=new int[i]; // FIXME: Dtor should cleanup this memory
	for (int j=0; j<i-1; j++) tmp[j]=l[j];
	tmp[i-1]=0;
	Fl_Icon_Browser::column_widths(tmp);
	// delete[] tmp; -- can't do this, browser goes berserk

	// Redraw parent so we don't get ugly artifacts after shrinking last button
	// Doesn't work anymore!
//	parent()->redraw();
}

const int* EDE_Browser::column_widths() const {
	int i,total=0;
	const int *l=Fl_Icon_Browser::column_widths();
	for (i=0; l[i]; i++) total+=l[i];

	static int *tmp = 0;
	if (tmp) delete[] tmp;
	tmp=new int[i+2]; // FIXME: Someone should cleanup this memory sometimes...
	for (int j=0; l[j]; j++) tmp[j]=l[j];

	tmp[i]=(totalwidth_-total);
	tmp[i+1]=0;
	return tmp;
}


// Subclassed handle() for keyboard searching
int EDE_Browser::handle(int e) {
	if (e==FL_FOCUS) { fprintf(stderr, "EB::focus\n"); }
	if (e==FL_KEYBOARD && Fl::event_state()==0) {
		// when user presses a key, jump to row starting with that character
		int k=Fl::event_key();
		if ((k>='a'&&k<='z') || (k>='A'&&k<='Z') || (k>='0'&&k<='9')) {
			if (k>='A'&&k<='Z') k+=('a'-'A');
			int ku = k - ('a'-'A'); //upper case
			int p=lineno(selection());
			for (int i=1; i<=size(); i++) {
				int mi = (i+p-1)%size() + 1; // search from currently selected one
				if (text(mi)[0]==k || text(mi)[0]==ku) {
					// select(line,0) just moves focus to line without selecting
					// if line was already selected, it won't be anymore
					select(mi,selected(mi));
					middleline(mi);
					//break;
					return 1; // menu will get triggered on key press :(
				}
			}
		}
		// Attempt to fix erratic behavior on enter key
		// Fl_Browser seems to do the following on enter key:
		// - when item is both selected and focused, callback isn't called at all (even FL_WHEN_ENTER_KEY_ALWAYS)
		// - when no item is selected, callback is called 2 times on focused item
		// - when one item is selected and other is focused, callback is first called on selected then on
		//   focused item, then the focused becomes selected
		// This partial fix at least ensures that callback is always called. Callback function should
		// deal with being called many times repeatedly.
		if ((when() & FL_WHEN_ENTER_KEY_ALWAYS) && k == FL_Enter) {
//			if (changed()!=0) {
				//fprintf(stderr,"do_callback()\n"); 
				do_callback();
//			}
		}

		if (k == FL_Tab) {
fprintf (stderr, "TAB\n");
			
//			Fl_Icon_Browser::handle(FL_UNFOCUS); return 1;
		}
	}
	return Fl_Icon_Browser::handle(e);
}

// Overload resize for show/hide horizontal scrollbar
void EDE_Browser::resize(int X, int Y, int W, int H) {
	int fsbs = Fl::scrollbar_size();
	if (W >= totalwidth_) {
		// hide scrollbar
		if (hscrollbar->visible())
			hscrollbar->hide();
		H += fsbs;
	} else {
		// show scrollbar
		hscrollbar->value(hscrollbar->value(), W, 0, totalwidth_);
		if (!hscrollbar->visible()) {
			hscrollbar->resize(X+1, Y+H-fsbs, W-fsbs-3, fsbs);
			hscrollbar->show();
			H -= fsbs;
		} else {
			hscrollbar->resize(X+1, Y+H, W-fsbs-3, fsbs);
			hscrollbar->redraw();
		}
	}

	heading->resize(X, Y-buttonheight, W, buttonheight);
//	else
//		heading->position(X, Y);
	Fl_Icon_Browser::resize(X,Y,W,H);
}




// *****************
// class Heading - implementation
// *****************

// The following code is modified from Fl_Tile.cxx and extensively commented, as I was trying
// to figure it out ;)

static void set_cursor(Fl_Group*t, Fl_Cursor c) {
	static Fl_Cursor cursor;
	if (cursor == c || !t->window()) return;
	cursor = c;
#ifdef __sgi
	t->window()->cursor(c,FL_RED,FL_WHITE);
#else
	t->window()->cursor(c);
#endif
}

static Fl_Cursor cursors[4] = {
	FL_CURSOR_DEFAULT,
	FL_CURSOR_WE,
	FL_CURSOR_NS,
	FL_CURSOR_MOVE
};

int EDE_Browser::Heading::handle(int event) {

	static int sdrag; // Type of drag
	static int sdx; // Event distance from the widget boundary
	static int sx; // Original event x
#define DRAGH 1
#define DRAGV 2
               
// width of grabbing area in pixels:
#define GRABAREA 4

	// Event coordinates
	int evx = Fl::event_x();
	int evy = Fl::event_y();

	if (event==FL_FOCUS) return 0; // this is a focusless widget!	
// 	if (event==FL_FOCUS && Fl::event_key()==FL_Tab) {
// 		parent()->take_focus(); // heading shouldn't take focus
// 		return 1;
// 	}

	switch (event) {

		case FL_MOVE:
		case FL_ENTER:
		case FL_PUSH: {
			int mindx = 100;
//			int mindy = 100;
//			int oldx = 0;
//			int oldy = 0;
			
			Fl_Widget*const* a = array();
			
			// Is there a button boundary within GRABAREA ?
			for (int i=0; i<children(); i++) {
				Fl_Widget* o = *a++;
				if (o == resizable()) continue; // resizable has a special meaning in Fl_Tile, but we don't use it in Heading
				if (o->y()<=evy+GRABAREA && o->y()+o->h()>=evy-GRABAREA) {
					int t = evx - (o->x()+o->w());
					if (abs(t) < mindx) {
						sdx = t;
						mindx = abs(t);
					}
				}
			}
			
			sdrag = 0; sx = 0;
			if (mindx <= GRABAREA) {sdrag = DRAGH; sx = evx;}
			set_cursor(this, cursors[sdrag]);
			if (sdrag) return 1;
			return Fl_Group::handle(event);
		}

		case FL_LEAVE:
			set_cursor(this, FL_CURSOR_DEFAULT);
			break;

		case FL_DRAG:
			// This is necessary if CONSOLIDATE_MOTION in Fl_x.cxx is turned off:
			// if (damage()) return 1; // don't fall behind
		case FL_RELEASE: {
			if (!sdrag) return 0; // should not happen
			Fl_Widget* r = resizable(); if (!r) r = this;
			
			// Calculate where the new boundary (newx) should be 
			int newx; 
			if (sdrag&DRAGH) {
				newx = Fl::event_x()-sdx;
				if (newx < r->x()) newx = r->x();
				else if (newx > r->x()+r->w()) newx = r->x()+r->w();
			}
			
			// Mouse movement distance (dx)
			int dx = Fl::event_x()-sx;
			
			Fl_Widget*const* a = array();
			
			// Here we check if any widget will get size 0 
			// because column size 0 is illegal in Browser
			for (int i=children(); i--; ) {
				Fl_Widget* o = a[i];
				int end = o->x()+o->w(); // End coord. of widget
				if ((end == newx-dx || newx<1) && o->w()+dx < 1) {
					set_changed();
					do_callback();
					set_cursor(this, FL_CURSOR_DEFAULT);
					return 1;
				}
			}

			if (newx>=w()) newx+=dx; // last button is resized by dragging the edge
			
			// Go again through list of widgets and resize everything
			int *columns = new int[children()+1];
			int j=0;
			for (int i=children(); i--; ) {
				Fl_Widget* o = *a++;
				int end = o->x()+o->w(); // End coord. of widget
				
				if (end == newx-dx) { 
					// Resize left widget
					o->damage_resize(   o->x(), o->y(), o->w()+dx, o->h());
				} else if (end > newx-dx) { 
					// Resize all remaining widgets to the right
					o->damage_resize(o->x()+dx, o->y(), o->w(), o->h());
				}
				// Push new width into the columns array
				columns[j++]=o->w();
			}
			columns[j]=0;

			// This is the EDE_Browser method. It will also resize the heading and
			// the browser if neccessary
			EDE_Browser*b = (EDE_Browser*)parent(); 
			b->column_widths(columns);
			b->redraw(); // OPTIMIZE (some smart damage in column_widths() ?)
			delete[] columns;
			
			// There will be many RELEASE events, so we update sx (used when calculating dx)
			sx=Fl::event_x();
			
			if (event == FL_DRAG) set_changed();
			do_callback();
			return 1;
		}
	}

	return Fl_Group::handle(event);
}
