#import <netinfo/ni.h>
#import "UsersDataSource.h"

@implementation UsersDataSource

-(void)dealloc {
    [_allowedUsers      release];

    [_allUsersCache     release];
    [_sortedCache       release];
    [_loggedInUsers     release];
    [_realShells        release];
}

- (void)setAllowedUsers:(NSArray*)allowedUsers {
    [_allowedUsers autorelease];
    if (allowedUsers == nil) {
        _allowedUsers = [[NSMutableArray alloc] init];
    } else {
        _allowedUsers = [allowedUsers mutableCopy];
    }
}

- (void)cleanAllowedUsers {
    BOOL found;
    int i, j;
    NSString* shortname;
    for (i = 0; i < [_allowedUsers count]; ++i) {
        shortname = [_allowedUsers objectAtIndex:i];
        found = FALSE;
        for (j = 0; j < [_sortedCache count]; ++j) {
            if ([[[_sortedCache objectAtIndex:j] objectForKey:@"Shortname"]
                 isEqualToString:shortname]) {
                 found = TRUE;
                 break;
            }
        }
        if (!found) {
            [_allowedUsers removeObjectAtIndex:i];
            --i;
        }
    }
}

- (NSArray*)allowedUsersCopy {
    return [_allowedUsers copy];
}

- (int)numberOfRowsInTableView:(NSTableView *)aTableView {
    return [_sortedCache count];
}

- (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex {
    if ([@"allow" isEqualToString:[aTableColumn identifier]]) {
        NSString* shortname = [[_sortedCache objectAtIndex:rowIndex] objectForKey:@"Shortname"];
        //printf("shortname: %s\n", [shortname cString]);
        //printf("length of _allowedUsers: %d\n", [_allowedUsers count]);
        //printf("index: %d\n", [_allowedUsers indexOfObject:shortname]);
        if ([_allowedUsers indexOfObject:shortname] == NSNotFound) {
            return [NSNumber numberWithBool:FALSE];
        } else {
            return [NSNumber numberWithBool:TRUE];
        }
    }
    return [[_sortedCache objectAtIndex:rowIndex] objectForKey:@"Username"];
}

- (void)tableView:(NSTableView *)aTableView setObjectValue:(id)anObject forTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex {
    NSString* shortname = [[_sortedCache objectAtIndex:rowIndex] objectForKey:@"Shortname"];
    if ([anObject boolValue]) {
        [_allowedUsers addObject:shortname];
    } else {
        [_allowedUsers removeObject:shortname];
    }
}

// ----------------------------------------------------------------------
//  The code below is a (more or less) verbatim copy from WinSwitch
//  ("http://winswitch.wincent.com/").
// ----------------------------------------------------------------------

int compareByUsername(NSDictionary* first, NSDictionary* second, void* context) {
    return [(NSString*)[first objectForKey:@"Username"] compare:
            (NSString*)[second objectForKey:@"Username"]];
}

//  Maintain a cache of all known users for rapid lookup
- (NSArray *)_allUsersCacheForcingRebuild:(BOOL)rebuild
{
    if (rebuild) // must rebuild cache (user added, icon changed etc)
    {
        [_allUsersCache release];   // dispose of old caches
        [_sortedCache   release];
        _refreshAllUsersCache = NO; // no need to rebuild next time around
    }
    else if (_sortedCache)          // cache already exists, no rebuild required
        return _sortedCache;        // return pre-built cache
    
    _allUsersCache  = [[NSMutableDictionary alloc] initWithCapacity:1];
    _sortedCache    = [[NSMutableArray alloc] initWithCapacity:1];

    // get UIDs in "users" directory in current (local) domain from NetInfo
    void *handle;
    ni_status status = ni_open(NULL, ".", &handle);
    if (status != NI_OK) return _sortedCache; // can't even connect to NetInfo!
    ni_id dir;
    const char *search = "/users";
    status = ni_pathsearch(handle, &dir, search);   // find "users" dir
    if (status == NI_OK)
    {
        ni_entrylist users;
        status = ni_list(handle, &dir, "uid", &users);  // check for UIDs > 500
        if (status == NI_OK)
        {
            unsigned i;
            for (i = 0; i < users.ni_entrylist_len; i++)
            {
                ni_name user = 
                users.ni_entrylist_val[i].names->ni_namelist_val[0];
                uid_t uid = strtol(user, NULL, 10);
                // must cast to signed otherwise user "nobody" (-2) passes test
                if ((signed)uid <= 500) continue;
                
                // add user to caches
                NSMutableDictionary *theUser = 
                    [NSMutableDictionary dictionary];
                NSNumber *UIDKey = [NSNumber numberWithInt:uid];
                [_allUsersCache setObject:theUser forKey:UIDKey];
                [_sortedCache addObject:theUser];
                
                // UID is also an object in the dictionary
                [theUser setObject:UIDKey forKey:@"UID"];
                
                // does user have a real shell?
                NSArray  *realShells = [self _realShells];
                NSString *shell = [self _propertyForKey:@"shell" user:uid];
                
                if (shell && [realShells containsObject:shell])
                    [theUser setObject:[NSNumber numberWithBool:YES]
                                forKey:@"RealShell"];
                else // "user" has no real shell so is probably a daemon
                {
                    [theUser setObject:[NSNumber numberWithBool:NO]
                                forKey:@"RealShell"];
                    continue;
                }
                
                // long username, used for sorting
                NSString *shortname = [self _propertyForKey:@"name" user:uid];
                NSString *username = [self _propertyForKey:@"realname"
                                                      user:uid];
                if (!username)
                    username = shortname; // use short name as fallback
                [theUser setObject:shortname forKey:@"Shortname"];
                [theUser setObject:username forKey:@"Username"];
                
                // username + image + tick (NSAttributedString)
                // (dimmed for current user)
                /*NSAttributedString *usernamePlusTick;
                NSString *iconPath = [self _iconPathForUID:uid];
                if (!iconPath)  // no custom icon for this user, use generic
                    iconPath = [[self bundle] pathForImageResource:@"generic"];
                uid_t current_user = getuid();
                if (uid == current_user) // store image + tick, dimmed
                {
                    usernamePlusTick = [self _menuString:username 
                                            withIconPath:iconPath
                                                   state:NSOnState
                                               dimImages:YES];
                    [theUser setObject:usernamePlusTick 
                                forKey:@"UsernamePlusTick"];
                }
                else // not current user: store with & without tick (not dimmed)
                {
                    usernamePlusTick = [self _menuString:username 
                                            withIconPath:iconPath
                                                   state:NSOnState
                                               dimImages:NO];
                    [theUser setObject:usernamePlusTick 
                                forKey:@"UsernamePlusTick"];
                    
                    // NSAttributedString -> username + image + no tick
                    NSAttributedString *usernameNoTick =
                        [self _menuString:username
                             withIconPath:iconPath
                                    state:NSOffState
                                dimImages:NO];
                    [theUser setObject:usernameNoTick 
                                forKey:@"UsernameNoTick"];
                }*/
            }
            ni_entrylist_free(&users);
        }
    }
    ni_free(handle);    // closes NetInfo connection
        
    // keep pointers in sync between _loggedInUsers and _allUsersCache
    // this allows for id-based comparisons between the two lists
    if (_loggedInUsers)
    {
        // enumeration should be quite fast as there are unlikely
        // to be a large number of simultaneous sessions
        unsigned count = [_loggedInUsers count];
        unsigned i; 
        for (i = 0; i < count; i++)
        {
            NSNumber *oldUserKey = 
                [[_loggedInUsers objectAtIndex:i] objectForKey:@"UID"];
            NSDictionary *newUserDictionary = 
                [_allUsersCache objectForKey:oldUserKey];
            if (newUserDictionary)
                // match found: update pointer
                [_loggedInUsers replaceObjectAtIndex:i 
                                          withObject:newUserDictionary];
        }
    }
    
    // instead of returning the raw dictionary, return it as a sorted array
    //NSSortDescriptor *descriptor = [NSSortDescriptor alloc];
    //[descriptor initWithKey:@"Username" 
    //              ascending:YES
    //               selector:@selector(caseInsensitiveCompare:)];
    //[_sortedCache sortUsingDescriptors:
    //    [NSArray arrayWithObject:descriptor]];
    [_sortedCache sortUsingFunction:compareByUsername context:nil];
    //[descriptor release];
    
    return _sortedCache;
}

// run once only, scans /etc/shells to find out what the "real shells" are
- (NSArray *)_realShells
{
    if (!_realShells)   // first time here, initialize list of shells
    {
        NSString *shells = [NSString stringWithContentsOfFile:@"/etc/shells"];
        if (shells) // file read successfully, try to extract paths from output
        {
            // this would be so much easier with a regular expression
            _realShells = 
            [[shells componentsSeparatedByString:@"\n"] mutableCopy];

            // must use signed ints here because the for loop will run towards 0
            // if we "continue" on the last line we'd hit -1 and keep looping
            signed max = (signed)[_realShells count];
            signed i;

            // strip out empty lines and lines not beinging with "/"
            for (i = max - 1; i >= 0; i--)
            {
                NSString *line = [_realShells objectAtIndex:i];
                
                if ([line isEqualToString:@""] || ![line hasPrefix:@"/"])
                {
                    [_realShells removeObjectAtIndex:i];
                    continue;
                }
                
                // strip out comments
                NSRange comment = [line rangeOfString:@"#"];
                
                if (!NSEqualRanges(comment, NSMakeRange(NSNotFound, 0)))
                {
                    // this line has a comment! strip it
                    line = [line substringToIndex:comment.location];
                    [_realShells replaceObjectAtIndex:i withObject:line];
                }
                
                // strip trailing whitespace
                line = [line stringByTrimmingCharactersInSet:
                    [NSCharacterSet whitespaceAndNewlineCharacterSet]];
                [_realShells replaceObjectAtIndex:i withObject:line];
            }
        }
        else        // file could not be read: supply defaults
            _realShells = [[NSArray alloc] initWithObjects:@"/bin/bash",
                @"/bin/csh", @"/bin/sh", @"/bin/tcsh", @"/bin/zsh", nil];
    }    
    return _realShells; // release message is sent in the dealloc method
}

// query NetInfo for the property of a given key in the directory matching UID
- (NSString *)_propertyForKey:(NSString *)key user:(uid_t)UID
{
    NSString *returnString = nil;
    void *handle;
    ni_status status = ni_open(NULL, ".", &handle);
    if (status != NI_OK) return nil;
    ni_id dir;
    const char *search = 
        [[NSString stringWithFormat:@"/users/uid=%d", UID] UTF8String];
    status = ni_pathsearch(handle, &dir, search);
    if (status == NI_OK)
    {
        ni_namelist properties;
        NI_INIT(&properties);
        status = ni_lookupprop(handle, &dir, [key UTF8String], &properties);
        if ((status == NI_OK) && (properties.ni_namelist_val[0]))
        {
            // note that only first property is returned
            returnString = 
            [NSString stringWithUTF8String:properties.ni_namelist_val[0]];
        }
        ni_namelist_free(&properties);
    }
    ni_free(handle);
    return returnString;    // returns nil on failure
}

@end
