#import "ReconItem.h"
#import "Bridge.h"

#import <Carbon/Carbon.h>

@implementation ReconItem

- init {
        [super init];
        selected = NO; // NB only used/updated during sorts. Not a
                                   // reliable indicator of whether item is selected
        fileSize = -1.;
        bytesTransferred = -1.;
        return self;
}

- (void)dealloc
{
    [path release];
    [fullPath release];
    // [direction release];  // assuming retained by cache, so not retained
    // [directionSortString release];  // no retain/release necessary because is constant
        [super dealloc];
}

- (ReconItem *)parent
{
        return parent;
}

- (void)setParent:(ReconItem *)p
{
        parent = p;
}

- (void)willChange
{
        // propagate up parent chain
        [parent willChange];
}

- (NSArray *)children
{
        return nil;
}

- (BOOL)selected
{
    return selected;
}

- (void)setSelected:(BOOL)x
{
    selected = x;
}

- (NSString *)path
{
    return path;
}

- (void)setPath:(NSString *)aPath
{
        [path autorelease];
        path = [aPath retain];

        // invalidate
        [fullPath autorelease];
        fullPath = nil;
}

- (NSString *)fullPath
{
        if (!fullPath) {
                NSString *parentPath = [parent fullPath];
                [self setFullPath:(([parentPath length] > 0) ? [parentPath stringByAppendingFormat:@"/%@", path] : path)];
        }

    return fullPath;
}

- (void)setFullPath:(NSString *)p
{
        [fullPath autorelease];
        fullPath = [p retain];
}

- (NSString *)left
{
    return nil;
}

- (NSString *)right
{
    return nil;
}

static NSMutableDictionary *_ChangeIconsByType = nil;

- (NSImage *)changeIconFor:(NSString *)type other:(NSString *)other
{
        if (![type length]) {
                if ([other isEqual:@"Created"]) {
                        type = @"Absent";
                } else if ([other length]) {
                        type = @"Unmodified";
                } else
                        return nil;
        }

        NSImage *result = [_ChangeIconsByType objectForKey:type];
        if (!result) {
                NSString *imageName = [NSString stringWithFormat:@"Change_%@.png", type];
                result = [NSImage imageNamed:imageName];
                if (!_ChangeIconsByType) _ChangeIconsByType = [[NSMutableDictionary alloc] init];
                [_ChangeIconsByType setObject:result forKey:type];
        }
        return result;
}

- (NSImage *)leftIcon
{
        return [self changeIconFor:[self left] other:[self right]];
}

- (NSImage *)rightIcon
{
        return [self changeIconFor:[self right] other:[self left]];
}


- (double)computeFileSize
{
        return 0.;
}

- (double)bytesTransferred
{
        return 0.;
}

- (long)fileCount
{
        return 1;
}

- (double)fileSize
{
        if (fileSize == -1.) fileSize = [self computeFileSize];
        return fileSize;
}

- (NSString *)formatFileSize:(double)size
{
        if (size == 0) return @"--";
        if (size < 1024) return @"< 1KB"; // return [NSString stringWithFormat:@"%i bytes", size];
        size /= 1024;
        if (size < 1024) return [NSString stringWithFormat:@"%i KB", (int)size];
        size /= 1024;
        if (size < 1024) return [NSString stringWithFormat:@"%1.1f MB", size];
        size = size / 1024;
        return [NSString stringWithFormat:@"%1.1f GB", size];
}

- (NSString *)fileSizeString
{
        return [self formatFileSize:[self fileSize]];
}

- (NSString *)bytesTransferredString
{
        return [self formatFileSize:[self bytesTransferred]];
}

- (NSNumber *)percentTransferred
{
        double size = [self computeFileSize];
        return (size > 0) ? [NSNumber numberWithDouble:([self bytesTransferred] / (size) * 100.0)]
                                          : nil;
}

static NSMutableDictionary *_iconsByExtension = nil;

- (NSImage *)iconForExtension:(NSString *)extension
{
        NSImage *icon = [_iconsByExtension objectForKey:extension];
        if (!_iconsByExtension) _iconsByExtension = [[NSMutableDictionary alloc] init];
        if (!icon) {
                icon = [[NSWorkspace sharedWorkspace] iconForFileType:extension];
                [icon setSize:NSMakeSize(16.0, 16.0)];
                [_iconsByExtension setObject:icon forKey:extension];
        }
        return icon;
}

- (NSImage *)fileIcon
{
        return [self iconForExtension:NSFileTypeForHFSTypeCode(kOpenFolderIcon)];
}

- (NSString *)dirString
{
        return @"<-?->";
}

- (NSImage *)direction
{
    if (direction) return direction;
    NSString * dirString = [self dirString];

    BOOL changedFromDefault = [self changedFromDefault];

    if ([dirString isEqual:@"<-?->"]) {
        if (changedFromDefault | resolved) {
            direction = [NSImage imageNamed: @"table-skip.tif"];
                        directionSortString = @"3";
                }
        else {
            direction = [NSImage imageNamed: @"table-conflict.tif"];
                        directionSortString = @"2";
        }
    }

    else if ([dirString isEqual:@"---->"]) {
        if (changedFromDefault) {
            direction = [NSImage imageNamed: @"table-right-blue.tif"];
            directionSortString = @"6";
        }
        else {
            direction = [NSImage imageNamed: @"table-right-green.tif"];
            directionSortString = @"8";
        }
    }

    else if ([dirString isEqual:@"<----"]) {
        if (changedFromDefault) {
            direction = [NSImage imageNamed: @"table-left-blue.tif"];
            directionSortString = @"5";
        }
        else {
            direction = [NSImage imageNamed: @"table-left-green.tif"];
            directionSortString = @"7";
        }
    }

    else if ([dirString isEqual:@"<-M->"]) {
        direction = [NSImage imageNamed: @"table-merge.tif"];
        directionSortString = @"4";
    }

        else if ([dirString isEqual:@"<--->"]) {
                direction = [NSImage imageNamed: @"table-mixed.tif"];
                directionSortString = @"9";
        }

    else {
        direction = [NSImage imageNamed: @"table-error.tif"];
        directionSortString = @"1";
    }

    [direction retain];
    return direction;
}

- (void)setDirection:(char *)d
{
    [direction autorelease];
    direction = nil;
}

- (void)doAction:(unichar)action
{
    switch (action) {
    case '>':
        [self setDirection:"unisonRiSetRight"];
        break;
    case '<':
        [self setDirection:"unisonRiSetLeft"];
        break;
    case '/':
        [self setDirection:"unisonRiSetConflict"];
        resolved = YES;
        break;
    case '-':
        [self setDirection:"unisonRiForceOlder"];
        break;
    case '+':
        [self setDirection:"unisonRiForceNewer"];
        break;
    case 'm':
        [self setDirection:"unisonRiSetMerge"];
        break;
    case 'd':
        [self showDiffs];
        break;
    case 'R':
        [self revertDirection];
        break;
    default:
        NSLog(@"ReconItem.doAction : unknown action");
        break;
    }
}

- (void)doIgnore:(unichar)action
{
    switch (action) {
    case 'I':
                ocamlCall("xS", "unisonIgnorePath", [self fullPath]);
        break;
    case 'E':
                ocamlCall("xS", "unisonIgnoreExt", [self path]);
        break;
    case 'N':
                ocamlCall("xS", "unisonIgnoreName", [self path]);
        break;
    default:
        NSLog(@"ReconItem.doIgnore : unknown ignore");
        break;
    }
}

/* Sorting functions. These have names equal to
   column identifiers + "SortKey", and return NSStrings that
   can be automatically sorted with their compare method */

- (NSString *) leftSortKey
{
    return [self replicaSortKey:[self left]];
}

- (NSString *) rightSortKey
{
    return [self replicaSortKey:[self right]];
}

- (NSString *) replicaSortKey:(NSString *)sortString
{
    /* sort order for left and right replicas */

    if ([sortString isEqualToString:@"Created"]) return @"1";
    else if ([sortString isEqualToString:@"Deleted"]) return @"2";
    else if ([sortString isEqualToString:@"Modified"]) return @"3";
    else if ([sortString isEqualToString:@""]) return @"4";
    else return @"5";
}

- (NSString *) directionSortKey
{
    /* Since the direction indicators are unsortable images, use
       directionSortString instead */

    if ([directionSortString isEqual:@""])
        [self direction];
    return directionSortString;
}

- (NSString *) progressSortKey
{
    /* Percentages, "done" and "" are sorted OK without help,
       but "start " should be sorted after "" and before "0%" */

    NSString * progressString = [self progress];
    if ([progressString isEqualToString:@"start "]) progressString = @" ";
    return progressString;
}

- (NSString *) pathSortKey
{
    /* default alphanumeric sort is fine for paths */
    return [self path];
}

- (NSString *)progress
{
        return nil;
}

- (BOOL)transferInProgress
{
        double soFar = [self bytesTransferred];
        return (soFar > 0) && (soFar < [self fileSize]);
}

- (void)resetProgress
{
}

- (NSString *)progressString
{
        NSString *progress = [self progress];
        if ([progress length] == 0. || [progress hasSuffix:@"%"])
                progress = [self transferInProgress] ? [self bytesTransferredString] : @"";
        else if ([progress isEqual:@"done"]) progress = @"";
        return progress;
}

- (NSString *)details
{
        return nil;
}

- (NSString *)updateDetails
{
    return [self details];
}

- (BOOL)isConflict
{
        return NO;
}

- (BOOL)changedFromDefault
{
        return NO;
}

- (void)revertDirection
{
        [self willChange];
    [direction release];
    direction = nil;
    resolved = NO;
}

- (BOOL)canDiff
{
        return NO;
}

- (void)showDiffs
{
}

- (ReconItem *)collapseParentsWithSingleChildren:(BOOL)isRoot
{
        return self;
}

- (NSString *)description
{
    return [NSString stringWithFormat:@"ReconItem: %@ %@", fullPath, path];
}

- (NSString *)debugDescription
{
    return [self description];
}
@end


// --- Leaf items -- actually corresponding to ReconItems in OCaml
@implementation LeafReconItem

- initWithRiAndIndex:(OCamlValue *)v index:(long)i
{
    [super init];
    ri = [v retain];
        index = i;
    resolved = NO;
    directionSortString = @"";
    return self;
}

-(void)dealloc
{
    [ri release];
    [left release];
    [right release];
    [progress release];
    [details release];

    [super dealloc];
}

- (NSString *)path
{
    if (!path) path = [(NSString *)ocamlCall("S@", "unisonRiToPath", ri) retain];
    return path;
}

- (NSString *)left
{
    if (!left) left = [(NSString *)ocamlCall("S@", "unisonRiToLeft", ri) retain];
        return left;
}

- (NSString *)right
{
    if (!right)	right = [(NSString *)ocamlCall("S@", "unisonRiToRight", ri) retain];
        return right;
}

- (double)computeFileSize
{
  return [(NSNumber *)ocamlCall("N@", "unisonRiToFileSize", ri) doubleValue];
}

- (double)bytesTransferred
{
        if (bytesTransferred == -1.) {
                // need to force to fileSize if done, otherwise may not match up to 100%
                bytesTransferred = ([[self progress] isEqual:@"done"]) ? [self fileSize]
                  : [(NSNumber*)ocamlCall("N@", "unisonRiToBytesTransferred", ri) doubleValue];
        }
        return bytesTransferred;
}

- (NSImage *)fileIcon
{
        NSString *extension = [[self path] pathExtension];

        if ([@"" isEqual:extension]) {
                NSString *type = (NSString *)ocamlCall("S@", "unisonRiToFileType", ri);
                extension = [type isEqual:@"dir"]
                        ? NSFileTypeForHFSTypeCode(kGenericFolderIcon)
                        : NSFileTypeForHFSTypeCode(kGenericDocumentIcon);
        }
        return [self iconForExtension:extension];
}

- (NSString *)dirString
{
        return (NSString *)ocamlCall("S@", "unisonRiToDirection", ri);
}

- (void)setDirection:(char *)d
{
        [self willChange];
    [super setDirection:d];
        ocamlCall("x@", d, ri);
}

- (NSString *)progress
{
    if (!progress) {
                progress = 	[(NSString *)ocamlCall("S@", "unisonRiToProgress", ri) retain];
                if ([progress isEqual:@"FAILED"]) [self updateDetails];
    }
        return progress;
}

- (void)resetProgress
{
    // Get rid of the memoized progress because we expect it to change
        [self willChange];
        bytesTransferred = -1.;
    [progress release];

        // Force update now so we get the result while the OCaml thread is available
        // [self progress];
        // [self bytesTransferred];
    progress = nil;
}

- (NSString *)details
{
    if (details) return details;
    return [self updateDetails];
}

- (NSString *)updateDetails
{
        [details autorelease];
        details = [(NSString *)ocamlCall("S@", "unisonRiToDetails", ri) retain];
    return details;
}

- (BOOL)isConflict
{
        return ((long)ocamlCall("i@", "unisonRiIsConflict", ri) ? YES : NO);
}

- (BOOL)changedFromDefault
{
        return ((long)ocamlCall("i@", "changedFromDefault", ri) ? YES : NO);
}

- (void)revertDirection
{
        ocamlCall("x@", "unisonRiRevert", ri);
        [super revertDirection];
}

- (BOOL)canDiff
{
        return ((long)ocamlCall("i@", "canDiff", ri) ? YES : NO);
}

- (void)showDiffs
{
        ocamlCall("x@i", "runShowDiffs", ri, index);
}

@end

@interface NSImage (TintedImage)
- (NSImage *)tintedImageWithColor:(NSColor *) tint operation:(NSCompositingOperation) op;
@end

@implementation NSImage (TintedImage)

- (NSImage *)tintedImageWithColor:(NSColor *) tint operation:(NSCompositingOperation) op
{
        NSSize size = [self size];
        NSRect imageBounds = NSMakeRect(0, 0, size.width, size.height);
        NSImage *newImage = [[NSImage alloc] initWithSize:size];

        [newImage lockFocus];
        [self compositeToPoint:NSZeroPoint operation:NSCompositeSourceOver];
        [tint set];
        NSRectFillUsingOperation(imageBounds, op);
        [newImage unlockFocus];

        return [newImage autorelease];
}

@end

// ----  Parent nodes in grouped items
@implementation ParentReconItem

- init
{
        [super init];
        _children = [[NSMutableArray alloc] init];
        return self;
}

- initWithPath:(NSString *)aPath
{
        [self init];
        path = [aPath retain];
        return self;
}

- (void)dealloc
{
        [_children release];
        [super dealloc];
}

- (NSArray *)children;
{
        return _children;
}

- (ReconItem *)findChildWithPath:(NSArray *)pathArray level:(NSUInteger)level{
    ReconItem *item;
    NSString *element = [pathArray count] ? [pathArray objectAtIndex:level] : @"";

    for (item in _children) {
        if ([item isKindOfClass:[ParentReconItem class]] && [[item path] isEqual:element]) {
            return item;
        }
    }
    return nil;
}

- (void)addChild:(ReconItem *)item pathArray:(NSArray *)pathArray level:(NSUInteger)level
{
  //    NSLog(@"Adding child: %@", pathArray);
        NSString *element = [pathArray count] ? [pathArray objectAtIndex:level] : @"";

        // if we're at the leaf of the path, then add the item
        if (((0 == [pathArray count]) && (0 == level)) || (level == [pathArray count]-1)) {
                [item setParent:self];
                [item setPath:element];
                [_children addObject:item];
                return;
        }

        // find / add matching parent node
        ReconItem *parentItem = [self findChildWithPath:pathArray level:level];
        if (parentItem == nil || ![parentItem isKindOfClass:[ParentReconItem class]]) {
                parentItem = [[ParentReconItem alloc] initWithPath:element];
                [parentItem setParent:self];
                [_children addObject:parentItem];
                [parentItem release];
        }

        [(ParentReconItem *)parentItem addChild:item pathArray:pathArray level:level+1];
}

- (void)addChild:(ReconItem *)item nested:(BOOL)nested
{
        [item setPath:nil]; // invalidate/reset

        if (nested) {
                [self addChild:item pathArray:[[item path] pathComponents] level:0];
        } else {
                [item setParent:self];
                [_children addObject:item];
        }
}

- (void)sortUsingDescriptors:(NSArray *)sortDescriptors
{
        // sort our children
        [_children sortUsingDescriptors:sortDescriptors];

        // then have them sort theirs
        NSUInteger i = [_children count];
        while (i--) {
                id child = [_children objectAtIndex:i];
                if ([child isKindOfClass:[ParentReconItem class]]) [child sortUsingDescriptors:sortDescriptors];
        }
}

- (ReconItem *)collapseParentsWithSingleChildren:(BOOL)isRoot
{
        // replace ourselves?
        if (!isRoot && [_children count] == 1) {
                ReconItem *child = [_children lastObject];
                [child setPath:[path stringByAppendingFormat:@"/%@", [child path]]];
                return [child collapseParentsWithSingleChildren:NO];
        }

        // recurse
        NSUInteger i = [_children count];
        while (i--) {
                ReconItem *child = [_children objectAtIndex:i];
                ReconItem *replacement = [child collapseParentsWithSingleChildren:NO];
                if (child != replacement) {
                        [_children replaceObjectAtIndex:i withObject:replacement];
                        [replacement setParent:self];
                }
        }
        return self;
}

- (void)willChange
{
        // invalidate child-based state
        // Assuming caches / constant, so not retained / released
        // [direction autorelease];
        // [directionSortString autorelease];
        direction = nil;
        directionSortString = nil;
        bytesTransferred = -1.;
        // fileSize = -1;
    // resolved = NO;

        // propagate up parent chain
        [parent willChange];
}

// Propagation methods
- (void)doAction:(unichar)action
{
        NSUInteger i = [_children count];
        while (i--) {
                ReconItem *child = [_children objectAtIndex:i];
                [child doAction:action];
        }
}

- (void)doIgnore:(unichar)action
{
        // handle Path ignores at this level, name and extension at the child nodes
        if (action == 'I') {
                [super doIgnore:'I'];
        } else {
                NSUInteger i = [_children count];
                while (i--) {
                        ReconItem *child = [_children objectAtIndex:i];
                        [child doIgnore:action];
                }
        }
}

// Rollup methods
- (long)fileCount
{
        if (fileCount == 0) {
                NSUInteger i = [_children count];
                while (i--) {
                        ReconItem *child = [_children objectAtIndex:i];
                        fileCount += [child fileCount];
                }
        }
        return fileCount;
}

- (double)computeFileSize
{
        double size = 0;
        NSUInteger i = [_children count];
        while (i--) {
                ReconItem *child = [_children objectAtIndex:i];
                size += [child fileSize];
        }
        return size;
}

- (double)bytesTransferred
{
        if (bytesTransferred == -1.) {
                bytesTransferred = 0.;
                NSUInteger i = [_children count];
                while (i--) {
                        ReconItem *child = [_children objectAtIndex:i];
                        bytesTransferred += [child bytesTransferred];
                }
        }
        return bytesTransferred;
}



- (NSString *)dirString
{
        NSString *rollup = nil;
        NSUInteger i = [_children count];
        while (i--) {
                ReconItem *child = [_children objectAtIndex:i];
                NSString *dirString = [child dirString];
                if (!rollup || [dirString isEqual:rollup]) {
                        rollup = dirString;
                } else {
                        // conflict
                        if ([dirString isEqual:@"---->"] || [dirString isEqual:@"<----"] || [dirString isEqual:@"<--->"]) {
                                if ([rollup isEqual:@"---->"] || [rollup isEqual:@"<----"] || [rollup isEqual:@"<--->"]) {
                                        rollup = @"<--->";
                                }
                        } else {
                                rollup = @"<-?->";
                        }
                }
        }
        // NSLog(@"dirString for %@: %@", path, rollup);
        return rollup;
}

- (BOOL)hasConflictedChildren
{
        NSString *dirString = [self dirString];
        BOOL result = [dirString isEqual:@"<--->"] || [dirString isEqual:@"<-?->"];
        // NSLog(@"hasConflictedChildren (%@): %@: %i", [self path], dirString, result);
        return result;
}

static NSMutableDictionary *_parentImages = nil;
static NSColor *_veryLightGreyColor = nil;
- (NSImage *)direction
{
        if (!_parentImages) {
                _parentImages = [[NSMutableDictionary alloc] init];
                _veryLightGreyColor = [[NSColor colorWithCalibratedRed:0.7 green:0.7 blue:0.7 alpha:1.0] retain];
        }
        NSImage *baseImage = [super direction];
        NSImage *parentImage = [_parentImages objectForKey:baseImage];
        if (!parentImage) {
                // make parent images a grey version of the leaf images
                parentImage = [baseImage tintedImageWithColor:_veryLightGreyColor operation:NSCompositeSourceIn];
                [_parentImages setObject:parentImage forKey:baseImage];
        }
        return parentImage;
}

@end
