/*
    SambaManger. A graphical frontend to configure the NetInfo enhanced samba.
    Copyright (C) 1998  Robert Frank

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

    This program 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 General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
		
		Robert Frank, frank@ifi.unibas.ch
*/

#import "NIProperty.h"
#import "NIDirectory.h"
#import <nikit/NIOpenPanel.h>
#import <regex.h>

//****************************************************************************
// The class variables
static 	id								strings;							// Localized strings for the panels
static	const	ni_namelist	noValues = {0, NULL};	// The empty name list

//****************************************************************************
// Macros
#define HAVEVALUES(p) 			(p >= 0) && \
														(p < properties->ni_proplist_len) && \
														properties->ni_proplist_val[p].nip_val.ni_namelist_len
#define VALUEINDEXOK(p,v)		(v >= 0) && (v < properties->ni_proplist_val[p].nip_val.ni_namelist_len)
#define PROPERTY(p)					properties->ni_proplist_val[p]
#define VALUELIST(p)				properties->ni_proplist_val[p].nip_val
#define VALUELISTITEM(p,v)	properties->ni_proplist_val[p].nip_val.ni_namelist_val[v]
#define VALUES(p)						properties->ni_proplist_val[p].nip_val.ni_namelist_len

//****************************************************************************
// The base kind 'NIProperty'
@implementation NIProperty
//*******************************
// Dummy methods for the compiler
- (int)willSelect:sender what:(int)kind path:(char **)ni_path title:(char **)panel_title state:(int *)anInt
{
		return 0;
}
- (int)didSelect:sender what:(int)kind value:(char **)selection domain:(const char *)niDomain state:(int)anInt
{
		return 0;
}
- (BOOL)propertyWillChange:sender value:(char **)string default:(BOOL *)remove index:(int)anInt
{
		return NO;
}
- propertyDidChange:sender value:(const char *)string
{
		return self;
}

//**************
// Local methods

- (const char *)openNetInfo:(const char *)baseDir withTitle:(const char *)openTitle domain:(const char **)from
{
NIOpenPanel	*openPanel;

		openPanel = [NIOpenPanel new];
		[openPanel setDirectoryPath:baseDir];
		[openPanel setPanelTitle:[strings valueForStringKey:"Title:Select in NetInfo Domain"]];
		[openPanel setListTitle:openTitle];
		if ([openPanel runModal] == NX_ALERTDEFAULT) {
			*from = [openPanel domain];
			return [openPanel directory];
		}
		return NULL;
}

- (const char *)openFilePath:(BOOL)dirsOnly
{
const char	*docFileType[1] = {NULL};
id					openPanel;

		openPanel = [[OpenPanel new] allowMultipleFiles:NO];
		[openPanel chooseDirectories:dirsOnly];
		if (dirsOnly)
			[openPanel setTitle:[strings valueForStringKey:"Title:Select Directory"]];
		else
			[openPanel setTitle:[strings valueForStringKey:"Title:Select File"]]; 
	
		switch([openPanel runModalForTypes:docFileType]) {
			case NX_OKTAG:
				return [openPanel filename];
			default: ;
		}
		return NULL;
}

- (const char *)usePanel:sender
// Display a panel for the selection of an existing value. Return any value selected or
// NULL to signal abortion.
{
int	m = setMode, rcode, anInt;
const char	*val = NULL, *p = path, *t = title, *where = NULL;

		do {
			// Is this what we wanted to call?
			if ([delegate respondsTo:@selector(willSelect:what:path:title:state:)])
				m = [delegate willSelect:self what:m path:&p title:&t state:&anInt];
	
			switch (m) {
				case NIPT_FILE:
					val = [self openFilePath:NO];
					break;
				case NIPT_DIR:
					val =  [self openFilePath:YES];
					break;
				case NIPT_NETINFO:
					val =  [self openNetInfo:p withTitle:t domain:&where];
					break;
				default: ;
			}
	
			// The null string signals abortion.
			if (!val)
				return val;
			
			// Copy to the temporary string buffer for possible modifications.
			if (temp)
				NXZoneFree([NXApp zone], temp);
			temp = NXCopyStringBufferFromZone(val ,[NXApp zone]);
			
			// Any modifications neccessary?
			if ([delegate respondsTo:@selector(didSelect:what:value:domain:state:)]) {
				rcode = [delegate didSelect:self what:m value:&temp domain:where state:anInt];
				if (rcode == 0) // accept
					return temp;
				else if (rcode < 0)
					return NULL;
			} else
				return temp;
		} while (YES);
}

//**************
// Class methods
+ init:strgs
{
		strings = strgs;
		return strings;
}

//***************
// Public methods
- init:sender properties:(ni_proplist *)props name:(const char *)label
{
		[super init];
		delegate = sender;
		properties = props;
		name = label;
		temp = NULL;
		tag = -1;

		return self;
}

- updateProperty
{
		return self;
}

- updateProperty:(const char *)value default:(BOOL)remove
// Update the first (and possibly only) value of this property.
// If there was no previous value (or no property at all) and remove is true,
// or if the old and new values are identical, no changes are made.
// Before accepting the new value, this method sends a propertyWillChange to
// its delegate. If this returns YES, the value is discarded an no changes are
// made. The delegate has the option to modify the value in the propertyWillChange.
// If  there is no delegate or the delegate does not respond to propertyWillChange,
// the value will be accepted. If there was a previous value and remove is true,
// the first value will be removed. If the list becomes empty, the entire property
// will be removed. If there was a previous value and remove is false, the first
// value will be updated. If there was no previous value or property, a new
// property and value will be entered. After this, the delegate will be notified
// by a call to propertyDidChange.
{
ni_property prop;
char				*copyOfVal = (char *)value;
int					index = [self index];

		// No changes?
		if (((index == NI_INDEX_NULL) && remove) || (HAVEVALUES(index) && value && !strcmp(VALUELISTITEM(index, 0), value)))
			return self;
		
		// Is the new value ok?
    if ([delegate respondsTo:@selector(propertyWillChange:value:default:index:)]) {
			copyOfVal = value?NXCopyStringBufferFromZone(value, [self zone]):NULL;
			if ([delegate propertyWillChange:self value:&copyOfVal default:&remove index:0]) {
				if (copyOfVal && (copyOfVal != value))
					NXZoneFree([NXApp zone], copyOfVal);
				return nil;
			}
		}

		if (index != NI_INDEX_NULL) {	// remove or update
			if (remove) {	// remove
				if (HAVEVALUES(index) > 1)
					ni_name_free(&(VALUELISTITEM(index,0)));
				else
					ni_proplist_delete(properties, index);
			} else { // update
				if (HAVEVALUES(index) > 1) {
					ni_name_free(&(VALUELISTITEM(index,0)));
					VALUELISTITEM(index,0) = ni_name_dup(copyOfVal);
				} else {
					ni_namelist_free(&(VALUELIST(index)));
    			ni_namelist_insert(&(VALUELIST(index)), copyOfVal, NI_INDEX_NULL);
				}
			}
		} else if (!remove) {  // create
			NI_INIT(&prop);
    	prop.nip_name = (char *)name;
    	ni_namelist_insert(&prop.nip_val, copyOfVal, NI_INDEX_NULL);
    	ni_proplist_insert(properties, prop, NI_INDEX_NULL);
			ni_namelist_free(&prop.nip_val);
			index = ni_proplist_match(*properties, name, NULL);
		}
		
		// Tell the delegate that the property was updated.
    if ([delegate respondsTo:@selector(propertyDidChange:value:)])
			[delegate propertyDidChange:self value:copyOfVal];

		// Redisplay if the value was changed!
		if (copyOfVal &&  strcmp(value, copyOfVal))
			[self display];

		if (copyOfVal && (copyOfVal != value))
			NXZoneFree([NXApp zone], copyOfVal);
		return self;
}

- (int)index
{
		return ni_proplist_match(*properties, name, NULL);
}

// Transfere the value from the property to the GUI
- display
{
		return self;
}

// Return the list of values or the empty list
- (const ni_namelist *)valueList
{
int					index = [self index];

		if (index != NI_INDEX_NULL)
			return &(VALUELIST(index));
		else
			return &noValues;
}

// Return the number of values
- (int)values
{
int	index = [self index];

		if (HAVEVALUES(index))
			return VALUES(index);
		else
			return 0;
}

// Return the value at index idx
- (const char *)valueAt:(int)idx
{
int	index = [self index];

	if (HAVEVALUES(index) && VALUEINDEXOK(index, idx))
		return VALUELISTITEM(index, idx);
	else
		return "";
}

// Insert a value at the end of the list, creating the property if necessary.
- insertValue:(const char *)value
{
		return [self insertValue:value at:NI_INDEX_NULL];
}

// Insert a value at position idx, creating the property if necessary.
- insertValue:(const char *)value at:(int)idx
{
int					index = [self index];
ni_property prop;

		if (HAVEVALUES(index))
			ni_namelist_insert(&(VALUELIST(index)), value , idx);
		else {
			NI_INIT(&prop);
    	prop.nip_name = (char *)name; // Not freed by ni_namelist_free!
    	ni_namelist_insert(&prop.nip_val, value, NI_INDEX_NULL);
    	ni_proplist_insert(properties, prop, NI_INDEX_NULL);
			ni_namelist_free(&prop.nip_val);
		}
		// Tell the delegate that the property was updated.
    if ([delegate respondsTo:@selector(propertyDidChange:value:)])
			[delegate propertyDidChange:self value:value];
		return self;
}

// Delete the value at index idx
- deleteValue:(int)idx
{
int	index = [self index];

		if (HAVEVALUES(index) && VALUEINDEXOK(index, idx)) {
			if (VALUES(index) > 1)
				ni_namelist_delete(&(VALUELIST(index)), idx);
			else
				ni_proplist_delete(properties, index);
		}
		// Tell the delegate that the property was updated.
    if ([delegate respondsTo:@selector(propertyDidChange:value:)])
			[delegate propertyDidChange:self value:NULL];

		return self;
}

// Change an existing value, create the property and insert the value, if necessary
// or, if value is NULL, delete the value
- updateValue:(const char *)value at:(int)idx
{
		[self deleteValue:idx];
		if (value)
			[self insertValue:value at:idx];

		return self;
}

// Locate the index of the value with the given value
- (int)findValue:(const char *)value how:(BOOL)mode
{
int	i, index = [self index];

		if (HAVEVALUES(index)) {
			if (mode) // NI_FIND_EXACT
				return ni_namelist_match(VALUELIST(index), value);
			else {
				for (i = 0; i < VALUES(index); i++)
					if (!recmp(value, VALUELISTITEM(index, i)))
						return i;
			}
		}
		return NI_INDEX_NULL;
}

// Remove all values
- removeValues
{
int	index = [self index];

		if (HAVEVALUES(index))
			ni_proplist_delete(properties, index);
		// Tell the delegate that the property was updated.
    if ([delegate respondsTo:@selector(propertyDidChange:value:)])
			[delegate propertyDidChange:self value:NULL];

		return self;
}

- free
{
		if (temp)
			NXZoneFree([self zone], temp);
		return [super free];
}

- (const char *)name
{
		return name;
}

- delegate
{
		return delegate;
}

- setDelegate:anObject
{
		delegate = anObject;
		return self;
}

- (int)tag
{
		return tag;
}

- setTag:(int)anInt
{
		tag = anInt;
		return self;
}


//**********************
// Delegates and Actions

- hasChanged:sender
{
		return self;
}
@end

//****************************************************************************
// Booleans
@implementation NIBoolProperty
- init:sender properties:(ni_proplist *)props name:(const char *)label outlet:o
{
		[super init:sender properties:props name:label];
		outlet = o;
		[outlet setTarget:self];
		[outlet setAction:@selector(hasChanged:)];
		[outlet selectCellAt:0 :0];
		yes = NXCopyStringBufferFromZone("yes", [self zone]);
		no = NXCopyStringBufferFromZone("no", [self zone]);
		return self;
}

- setValues:(const char *)true :(const char *)false
{
		yes = NXZoneRealloc([self zone], yes, strlen(yes)+1);
		no = NXZoneRealloc([self zone], no, strlen(no)+1);
		(void)strcpy(yes, true);
		(void)strcpy(no, false);
		return self;
}

- hasChanged:sender
{
int					col = [outlet selectedCol];
const char	*v;

		v = (col < 1)?NULL:((col == 1)?yes:no);
		[self updateProperty:v default:(col < 1)];
		return self;
}

- display
{
int					index = [self index];

		if (HAVEVALUES(index))
			if (!strcasecmp(VALUELISTITEM(index,0), "yes") ||
					!strcasecmp(VALUELISTITEM(index,0), "true") ||
					!strcasecmp(VALUELISTITEM(index,0), yes) ||
					VALUELISTITEM(index,0)[0] == '1')
				[outlet selectCellAt:0 :1];
			else
				[outlet selectCellAt:0 :2];
		else
			[outlet selectCellAt:0 :0];

		return self;
}
@end

//****************************************************************************
// Characters
@implementation NICharProperty
- init:sender properties:(ni_proplist *)props name:(const char *)label outlet:o
{
		[super init:sender properties:(ni_proplist *)props name:label];
		outlet = o;
		needUpdate = NO;
		[outlet setTextDelegate:self];
		[outlet setStringValue:""];
		return self;
}

- updateProperty
{
const char	*v;
id					obj = self;

		if (needUpdate) {
			v = [outlet stringValue];
			obj = [self updateProperty:v default:(!v || !*v)];
			needUpdate = obj == nil;
		}
		return obj;
}

- display
{
int	index = [self index];

		if (HAVEVALUES(index))
			[outlet setStringValue:(const char *)VALUELISTITEM(index,0)];
		else
			[outlet setStringValue:""];

		return self;
}

//**************
// Text delegates

- textDidEnd:textObject endChar:(unsigned short)whyEnd
{
		return [self updateProperty];
}

- textDidChange:sender
{
		needUpdate = YES;
		// Tell the delegate that the property was updated.
    if ([delegate respondsTo:@selector(propertyDidChange:value:)])
			[delegate propertyDidChange:self value:NULL];
		return self;
}
@end

//****************************************************************************
// Integers
@implementation NIIntProperty
//**************
// Slider action
- setVal:sender
{
int		v = [slider intValue];
char	buf[32];

		if (v != intVal) {
			sprintf(buf, "%d", v);
			if (![self updateProperty:buf default:(v < 0)])
				return nil;
			intVal = v;
			if (intVal < 0)
				[outlet setStringValue:defaultText];
			else if ((intVal == 0) && zero)
				[outlet setStringValue:zero];
			else
				[outlet setIntValue:intVal];
		}
		return self;
}

//*************
// main methods
- init:sender properties:(ni_proplist *)props name:(const char *)label
		text:t slider:s default:(const char *)d zero:(const char *)z
{
		[super init:sender properties:(ni_proplist *)props name:label outlet:t];
		slider = s;
		defaultText = d;
		zero = z;
		intVal = -1;
		
		[outlet setStringValue:d];

		if (slider) {
			[slider setMinValue:-1.0];
			[slider setTarget:self];
			[slider setAction:@selector(setVal:)];
			[slider setIntValue:-1];
		}
		
 		return self;
}

- display
{
int	index = [self index];

		if (HAVEVALUES(index)) {
			intVal = atoi(VALUELISTITEM(index,0));
			if (intVal >= 0) {
				if (slider)
					[slider setIntValue:intVal];
				if ((intVal == 0) && zero)
					[outlet setStringValue:zero];
				else
					[outlet setIntValue:intVal];
				return self;
			}
		}
		
		if (slider)
			[slider setIntValue:-1];
		[outlet setStringValue:defaultText];

		return self;
}

- updateProperty
{
char				buf[32];
const char	*s = [outlet stringValue];
int					v = atoi(s);

		if (s && !strcmp(s, defaultText))
			v = -1;

		if (v != intVal) {
			sprintf(buf, "%31d", v);
			if (![self updateProperty:buf default:(v < 0)])
				return nil;
			intVal = v;
			if (intVal < 0)
				[outlet setStringValue:defaultText];
			else if ((intVal == 0) && zero)
				[outlet setStringValue:zero];
			if (slider)
				[slider setIntValue:intVal];
			needUpdate = NO;
		}
		
		return self;
}

//**************
// Text delegate

- textDidEnd:textObject endChar:(unsigned short)whyEnd
{
		return [self updateProperty];
}
@end

//****************************************************************************
// Strings
@implementation NIStringProperty
- setString:sender
{
const char	*val;

		if (!(val = [self usePanel:sender]))
			return self;
		
		if ([self updateProperty:val default:(!val || !*val)])
			[outlet setStringValue:val];

		return self;
}

- init:sender properties:(ni_proplist *)props name:(const char *)label
		text:t button:b mode:(int)m path:(const char *)p title:(const char *)tString
{
		[super init:sender properties:(ni_proplist *)props name:label outlet:t];
		setMode = m;
		path = p;
		title = tString;
		button = b;
		if (button) {
			[button setTarget:self];
			[button setAction:@selector(setString:)];
		}

		return self;
}
@end

//****************************************************************************
// Browsers
// This will set the delegate and delegate of the browser to this object.
// If there is a text field, then its textDelegate will also be set to this object.
// The values are copied to a separate structure for resetting.
@implementation NIBrowserProperty

//******************************
// Browser delegates and actions

- hasChanged:sender
{
		if (textField) {
		  [textField setStringValue:[[sender selectedCell] stringValue]];
		  [textField selectText:self];
		}
		return self;
}

- (int)browser:sender fillMatrix:matrix inColumn:(int)column
{
int		i = 0,
			index = [self index];
id		cell;
		
		if (HAVEVALUES(index))
			for (i = 0; i < VALUES(index); i++) {
				[matrix addRow];
				cell = [matrix cellAt:i :0];
				[cell setStringValue:VALUELISTITEM(index,i)];
				[cell setLoaded:YES];
				[cell setLeaf:YES];
			}
		
		return i;
}

// The action taken when pressing the add button
- add:sender
{
int					i = NI_INDEX_NULL,
						index = [self index];
const char	*v = NULL;
char				*copyOfVal = (char *)v;
BOOL				remove = NO;

		if (textField)
			v = [textField stringValue];
		else 
			v = [self usePanel:sender];
		if (!v || !*v)
			return self;
		
		// Is the new value ok?
    if ([delegate respondsTo:@selector(propertyWillChange:value:default:index:)]) {
			copyOfVal = NXCopyStringBufferFromZone(v, [self zone]);
			if ([delegate propertyWillChange:self value:&copyOfVal default:&remove index:i]) {
				if (copyOfVal && (copyOfVal != v))
					NXZoneFree([NXApp zone], copyOfVal);
				return self;
			}
		}

		// No changes?
		if (HAVEVALUES(index)) {
			for (i = 0; i < VALUES(index); i++)
				if (!strcasecmp(VALUELISTITEM(index,i), v)) {
					if (copyOfVal && (copyOfVal != v))
						NXZoneFree([NXApp zone], copyOfVal);
					[[outlet matrixInColumn:0] selectCellAt:i :0];
					return self;
				} else if (strcasecmp(VALUELISTITEM(index,i), v) > 0)
					break; 

			ni_namelist_insert(&(VALUELIST(index)), copyOfVal , i);
			
		} else
		  if (![self updateProperty:copyOfVal default:(!copyOfVal || !*copyOfVal)]) {
				if (copyOfVal && (copyOfVal != v))
					NXZoneFree([NXApp zone], copyOfVal);
				return self;
		}
	
		[outlet reloadColumn:0];
		[[outlet matrixInColumn:0] selectCellAt:i :0];
		[textField selectText:self];

		// Tell the delegate that the property was updated.
		if ([delegate respondsTo:@selector(propertyDidChange:)])
			[delegate propertyDidChange:self value:copyOfVal];

		if (copyOfVal && (copyOfVal != v))
			NXZoneFree([NXApp zone], copyOfVal);

		return self;
}

// The action taken when pressing the delete button
- delete:sender
{
BOOL	remove = YES;
char	*p = NULL;
int		i,
			index = [self index];
id		selCell = [outlet selectedCell];

		if (selCell && HAVEVALUES(index)) {
			i = ni_namelist_match(VALUELIST(index), [selCell stringValue]);
			if (i != NI_INDEX_NULL) {
				if ([delegate respondsTo:@selector(propertyWillChange:value:default:index:)])
					if ([delegate propertyWillChange:self value:&p default:&remove index:i])
						return nil;
				if ([self values] > 1) {
					ni_namelist_delete(&(VALUELIST(index)), i);
					if (--i > 0)
						[[outlet matrixInColumn:0] selectCellAt:i :0];
					else if (HAVEVALUES(index))
						[[outlet matrixInColumn:0] selectCellAt:0 :0];
				} else
					ni_proplist_delete(properties, index);
				[outlet reloadColumn:0];
				if ([delegate respondsTo:@selector(propertyDidChange:value:)])
					[delegate propertyDidChange:self value:p];
			}
		}
		return self;
}

//***************
// Text delegates

// Only update the browser if return was pressed.
- textDidEnd:textObject endChar:(unsigned short)whyEnd
{
		if (whyEnd == NX_RETURN)
			[self add:textObject];

		return self;
}

//************
// The methods

- init:sender properties:(ni_proplist *)props name:(const char *)label
		text:t browser:b mode:(int)m path:(const char *)p
		add:a remove:r title:(const char *)tString;
{
		[super init:sender properties:(ni_proplist *)props name:label];
		outlet = b;
		[outlet setTarget:self];
		[outlet setAction:@selector(hasChanged:)];
		textField = t;
		addButton = a;
		deleteButton = r;
		setMode = m;
		path = p;
		title = tString;
		
		[b setDelegate:self];
		if (textField)
			[textField setTextDelegate:self];
		if (addButton) {
			[addButton setTarget:self];
			[addButton setAction:@selector(add:)];
		}
		if (deleteButton) {
			[deleteButton setTarget:self];
			[deleteButton setAction:@selector(delete:)];
		}
		
		return self;
}

// Set/reset the values
- display
{
		[outlet loadColumnZero];
		[[outlet matrixInColumn:0] selectCellAt:0 :0];		
		return self;
}
@end

//****************************************************************************
// Popups
@implementation NIPopupProperty
- hasChanged:sender
{
const char	*s = [[outlet target] selectedItem] ;
int		d = strcmp(defaultText, s);

		return [self updateProperty:s default:!d];
}

- init:sender properties:(ni_proplist *)props name:(const char *)label
		outlet:o default:(const char *)d;
{
		[super init:sender properties:props name:label];
		outlet = o;
		[[outlet target] setTarget:self];
		[[outlet target] setAction:@selector(hasChanged:)];
		defaultText = d;
		return self;
}

- display
{
int	index = [self index];

		if (HAVEVALUES(index))
			[outlet setTitle:(const char *)VALUELISTITEM(index,0)];
		else
			[outlet setTitle:defaultText];
		return self;
}
@end

//****************************************************************************
// Calls
@implementation NICallProperty
- nothing:sender
{
		return self;
}

- init:sender properties:(ni_proplist *)props name:(const char *)label displayAction:(SEL)action;
{
		[super init:sender properties:(ni_proplist *)props name:label];
		if (action)
			display = action;
		else {
			display = @selector(nothing:);
			delegate = self;
		}
		return self;
}

- display
{
		return [delegate perform:display with:self];
}

@end

