//
//  KMBSLogViewController.m
//  BathyScaphe
//
//  Created by 堀 昌樹 on 11/12/24.
//  Copyright (c) 2011年 __MyCompanyName__. All rights reserved.
//

#import "KMBSLogViewController.h"
#import "KMDocument.h"
#import "KMStatusLineViewController.h"
#import "KMHistoryStack.h"

#import "CMRThreadView.h"

#import "CMRMainMenuManager.h"
#import "AppDefaults.h"
#import "CMRMessageAttributesTemplate.h"
#import "missing.h"
#import "CMRThreadAttributes.h"
#import "CMRThreadSignature.h"

#import <SGAppKit/SGAppKit.h>

#import "CMRThreadMessageBufferReader.h"
#import "CMRThreadComposingTask.h"

#import "BSBoardInfoInspector.h"

#import "KMWorkerEmulator.h"

#import "KMReplyMessenger.h"
#import "KMBSLogViewDelegate.h"
#import "KMBSLogPopUp.h"


NSString *const CMRThreadViewerDidChangeThreadNotification  = @"CMRThreadViewerDidChangeThreadNotification";
NSString *const CMRThreadViewerRunSpamFilterNotification = @"CMRThreadViewerRunSpamFilterNotification";
NSString *const BSThreadViewerWillStartFindingNotification = @"BSThreadViewerWillStartFindingNotification";
NSString *const BSThreadViewerDidEndFindingNotification = @"BSThreadViewerDidEndFindingNotification";

@interface KMBSLogViewController()
@property (assign, readwrite) NSLayoutManager *layoutManager;
@property (assign, readwrite) NSTextContainer *textContainer;
@property (nonatomic, retain, readwrite) NSCountedSet *countedSet;

@property (readonly) KMReplyMessenger *messenger;
@property (nonatomic, retain) NSTextStorage *privateTextStorage;
@property (nonatomic, retain) KMHistoryStack *histories;
@property (nonatomic, retain) KMBSLogViewDelegate *logViewDelegate;
@property (nonatomic, retain) KMBSLogPopUp *popUp;
@property (nonatomic, retain, readwrite) KMLinkHanlder *linkHandler;

@property (readonly) NSWindow *window;

- (void)composeDocumentMessage:(id)sender;
@end

@interface KMBSLogViewController(BuildViews)
- (void)cleanUpTitleRuler:(NSTimer *)aTimer;
- (void)setupScrollView;
- (void)setupTextView;
@end

@interface KMBSLogViewController(CMRThreadLayout_Dummy)
- (void)scrollMessageWithRange:(NSRange)aRange;

- (NSDate *)lastUpdatedDateFromFirstHeaderAttachmentEffectiveRange:(NSRangePointer)effectiveRange;
- (NSRange)firstLastUpdatedHeaderAttachmentRange;
- (void)clearLastUpdatedHeader;
- (void)insertLastUpdatedHeader;

// range of NSTextStorage.
static NSUInteger KMBinarySearchIndex(KMBSLogViewController *target, NSUInteger min, NSUInteger max, NSRange aRange);
static void KMSetRnageByIndex(KMBSLogViewController *obj, NSRange range, NSUInteger index);
static void KMClearRangesFromIndex(KMBSLogViewController *obj, NSUInteger index);
- (NSRange)rangeAtMessageIndex:(NSUInteger)anIndex;
- (NSUInteger)messageIndexForRange:(NSRange)aRange;

- (NSUInteger)firstMessageIndexForDocumentVisibleRect;
- (NSUInteger)lastMessageIndexForDocumentVisibleRect;

// Move
- (NSUInteger)nextVisibleMessageIndexOfIndex:(NSUInteger) anIndex;
- (NSUInteger)previousVisibleMessageIndexOfIndex:(NSUInteger)anIndex;
- (NSUInteger)nextBookmarkIndexOfIndex:(NSUInteger)anIndex;
- (NSUInteger)previousBookmarkIndexOfIndex:(NSUInteger) anIndex;

@end

@interface KMBSLogViewController (NSTextViewDelegate) <KMBSLogViewDelegateOwner, KMBSLogPopUpOwner>
@end

#import "CMRReplyMessenger.h"
#import "CMRReplyDocumentFileManager.h"
@interface KMBSLogViewController(Replay)
- (CMRReplyMessenger *)plainReply:(id)sender;
- (void)quoteWithMessenger:(CMRReplyMessenger *)aMessenger;
@end

@implementation KMBSLogViewController
@synthesize statusLine = _statusLine;
@synthesize documentView = _documentView;
@synthesize layoutManager = _layoutManager;
@synthesize textContainer = _textContainer;
@synthesize countedSet = _countedSet;
@synthesize messenger = _messenger;
@synthesize privateTextStorage = _privateTextStorage;
@synthesize histories = _histories;
@synthesize logViewDelegate = _logViewDelegate;
@synthesize popUp = _popUp;
@synthesize linkHandler = _linkHandler;

@synthesize numberOfMessage = _numberOfMessage;

@synthesize hasTitleRuler = _hasTitleRuler;

+ (NSString *)localizableStringsTableName
{
#define APP_TVIEW_LOCALIZABLE_FILE			@"ThreadViewer"
	return APP_TVIEW_LOCALIZABLE_FILE;
}

- (id)init
{
	self = [super initWithNibName:@"KMBSLogView" bundle:nil];
	if(self) {
		worker = [[KMWorkerEmulator alloc] init];
		updatingLock = [[NSLock alloc] init];
		
		self.histories = [[[KMHistoryStack alloc] init] autorelease];
		self.countedSet = [[[NSCountedSet alloc] init] autorelease];
		self.linkHandler = [[[KMLinkHanlder alloc] init] autorelease];
		self.linkHandler.logViewController = self;
		
		NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
		[nc addObserver:self
			   selector:@selector(threadViewThemeDidChange:)
				   name:AppDefaultsThreadViewThemeDidChangeNotification
				 object:CMRPref];
		[nc addObserver:self
			   selector:@selector(appDefaultsLayoutSettingsUpdated:)
				   name:AppDefaultsLayoutSettingsUpdatedNotification
				 object:CMRPref];
	}
	
	return self;
}
- (void)dealloc
{
	NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
	[nc removeObserver:self
				  name:AppDefaultsThreadViewThemeDidChangeNotification
				object:CMRPref];
	[nc removeObserver:self
				  name:AppDefaultsLayoutSettingsUpdatedNotification
				object:CMRPref];
	
	[_documentView release];
	[_privateTextStorage release];
	[updatingLock release];
	[_statusLine release];
	[worker release];
	[ranges release];
	[_histories release];
	[_messenger release];
	[_logViewDelegate release];
	[_popUp release];
	[_linkHandler release];
	
	[super dealloc];
}
- (void)loadView
{
	[super loadView];
	
	[self setupScrollView];
	[self setupTextView];
	[self.statusLine validateIdxNavLazily:self];
}

- (KMDocument *)doc
{
	return (KMDocument *)self.representedObject;
}
- (NSScrollView *)documentScrollView
{
	return (NSScrollView *)self.view;
}
- (KMReplyMessenger *)messenger
{
	if(!_messenger) {
		_messenger = [[KMReplyMessenger alloc] init];
		_messenger.document = self.doc;
	}
	return _messenger;
}
- (NSWindow *)window
{
	return self.view.window;
}

- (void)scrollMessageAtIndex:(NSUInteger)anIndex
{
	NSRange range = [self rangeAtMessageIndex:anIndex];
	if(range.location == NSNotFound) return;
	if(NSMaxRange(range) > self.documentView.string.length) return;
	
	[self scrollMessageWithRange:range];
}

#pragma mark-
- (void)registerToHistory:(KMDocument *)doc
{
	id histItem = [doc.threadAttr threadSignature];
	if(!histItem) {
		[self performSelector:_cmd withObject:doc afterDelay:0.1];
		return;
	}
	[self.histories push:histItem];
}

- (void)setRepresentedObject:(id)representedObject
{
	if(self.representedObject == representedObject) return;
	
	if(self.representedObject) {
		if ([CMRPref oldMessageScrollingBehavior]) {
			self.doc.lastViewingIndex = [self firstMessageIndexForDocumentVisibleRect];
		} else {
			self.doc.lastViewingIndex = [self lastMessageIndexForDocumentVisibleRect];
		}
	}
	
	// clear text
	@synchronized(self) {
		[self.privateTextStorage setAttributedString:[[[NSAttributedString alloc] initWithString:@""] autorelease]];
	}
	self.numberOfMessage = 0;
	KMClearRangesFromIndex(self, 0);
	[self performSelector:@selector(cleanUpTitleRuler:)
			   withObject:nil
			   afterDelay:0.0];
	self.countedSet = [[[NSCountedSet alloc] init] autorelease];
	
	NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
	[nc removeObserver:self
				  name:KMDocumentDidChangeNotification
				object:self.representedObject];
	[nc removeObserver:self
				  name:KMDocumentDidChangeMessageNotification
				object:self.representedObject];
	
	[super setRepresentedObject:representedObject];
	if(!representedObject) return;
	
	[nc addObserver:self
		   selector:@selector(documentDidChangeNotification:)
			   name:KMDocumentDidChangeNotification
			 object:representedObject];
	[nc addObserver:self
		   selector:@selector(messageDidChangeNotification:)
			   name:KMDocumentDidChangeMessageNotification
			 object:representedObject];
	
	if([self.doc numberOfReadedMessages] != 0) {
		[self composeDocumentMessage:self];
	} else {
		[representedObject reload:self];
	}
	[self registerToHistory:self.doc];
	
	self.popUp.doc = self.doc;
	self.logViewDelegate.doc = self.doc;
}

- (void)setHasTitleRuler:(BOOL)hasTitleRuler
{
	_hasTitleRuler = hasTitleRuler;
	[self cleanUpTitleRuler:nil];
}
- (BOOL)hasTitleRuler
{
	return _hasTitleRuler;
}

// Composing sequence
/*
 -[KMBSLogViewController composeDcumentMessage:]
 
 on other thread
 -[KMBSLogViewController addMessagesFromBuffer:]
 -[KMBSLogViewController threadComposingDidFinish:]
 
 on main thread
 -[KMBSLogViewController appendMessageAttributeString:]
*/
- (void)composeDocumentMessage:(id)sender
{
	// unlock in -appendMessageAttributeString: method.
	if(![updatingLock tryLock]) {
		[self performSelector:_cmd
				   withObject:sender
				   afterDelay:0.0];
		return;
	}
	
	NSString *boardName = self.doc.boardName;
	NSString *threadTitle = self.doc.threadTitle;
	NSString *datIdentifier = self.doc.datIdentifier;
	
	CMRThreadMessageBufferReader *reader = [CMRThreadMessageBufferReader readerWithContents:self.doc.messageBuffer];
	[reader setNextMessageIndex:self.numberOfMessage];
	CMRThreadComposingTask *task = [CMRThreadComposingTask taskWithThreadReader:reader];
	[task setThreadTitle:threadTitle];
	[task setIdentifier:datIdentifier];
	[task setDelegate:self];
	worker.layoutDummy = self;
	[worker push:task];
	
	if(boardName && threadTitle) {
		NSString *rulerTitle = [NSString stringWithFormat:@"%@ %C %@", threadTitle, 0x2014, boardName];
		BSTitleRulerView *view_ = (BSTitleRulerView *)[self.documentScrollView horizontalRulerView];
		[view_ setTitleStr:rulerTitle];
		[view_ setPathStr:self.doc.path];
	}
	
	if(sender == self) {
		[self.doc reload:self];
	}
}

- (void)colorizeID
{
	@synchronized(self) {
		[self colorizeID:self.documentView];
	}
}
- (void)appendMessageAttributeString:(NSMutableAttributedString *)aTextBuffer
{
	if(!aTextBuffer) goto finish;
	
	BOOL isFirst = NO; // 初めての読み込みの場合は読んだ位置までスクロールする
	
	if([aTextBuffer length]) {
		[aTextBuffer fixAttributesInRange:[aTextBuffer range]];
		@synchronized(self) {
			if(self.privateTextStorage.length != 0) {
				[self insertLastUpdatedHeader];
			} else {
				isFirst = YES;
			}
			[self.privateTextStorage appendAttributedString:aTextBuffer];
		}
	}
	
	if(isFirst) {
		NSUInteger lastIndex = self.doc.lastViewingIndex;
		if(lastIndex != NSNotFound && lastIndex != 0) {
			[self scrollMessageAtIndex:lastIndex];
		}
	}
	if([CMRPref scrollToLastUpdated]) {
		NSRange range = [self firstLastUpdatedHeaderAttachmentRange];
		if(range.location != NSNotFound) {
			[self scrollMessageWithRange:range];
		}
	}
	// ID 色付け
    if ([CMRPref shouldColorIDString]) {
        [self colorizeID];
    }
	
finish:
	// lock in -composeDocumentMessage: method.
	[updatingLock unlock];
}
- (void)threadComposingDidFinish:(NSMutableAttributedString *)aTextBuffer
{
	[self performSelectorOnMainThread:@selector(appendMessageAttributeString:)
						   withObject:aTextBuffer
						waitUntilDone:YES];
}
- (void)addMessagesFromBuffer:(CMRThreadMessageBuffer *)buf
{
	// NSLog(@"buf class is %@.", NSStringFromClass([buf class]));
	self.numberOfMessage += [buf count];
	[self.statusLine validateIdxNavLazily:self];
}
- (void)addMessageRange:(NSRange)range
{
	// repeat by adding messange
}

- (void)documentDidChangeNotification:(id)no
{
	// 再取得
	if(self.doc.numberOfReadedMessages == 0) {
		KMClearRangesFromIndex(self, 0);
		self.numberOfMessage = 0;
		[self.privateTextStorage deleteCharactersInRange:NSMakeRange(0, self.privateTextStorage.length)];
		return;
	}
	[self composeDocumentMessage:nil];
}
@end

#import "CMRThreadMessage.h"
#import "CMRThreadMessageBuffer.h"
#import "CMRAttributedMessageComposer.h"
@implementation KMBSLogViewController(CMRThreadLayout_Dummy)
- (CMRThreadLayout *)threadLayout
{
	return (CMRThreadLayout *)self;
}

- (void)scrollMessageWithRange:(NSRange)aRange
{
	CMRThreadView	*textView = (CMRThreadView *)[self documentView];
    BOOL needsAdjust = ![CMRPref oldMessageScrollingBehavior];
	
    if (needsAdjust) {
        // 2010-08-15 tsawada2
        // non-contiguous layout でこのおまじないが効く
        [textView scrollRangeToVisible:aRange];
    }
	NSRect			characterBoundingRect;
	NSRect			newVisibleRect;
    NSRect          currentVisibleRect;
	NSPoint			newOrigin;
	NSClipView		*clipView;
	
	if (NSNotFound == aRange.location || 0 == aRange.length) {
		NSBeep();
		return;
	}
	
	characterBoundingRect = [textView boundingRectForCharacterInRange:aRange];
	if (NSEqualRects(NSZeroRect, characterBoundingRect)) {
        return;
	}
	
	clipView = [self.documentScrollView contentView];
	currentVisibleRect = [clipView documentVisibleRect];
	
	newOrigin = [textView bounds].origin;
	newOrigin.y = characterBoundingRect.origin.y;	
	
    newVisibleRect = currentVisibleRect;
	newVisibleRect.origin = newOrigin;
	
	if (!NSEqualRects(newVisibleRect, currentVisibleRect)) {
		// 表示予定領域(newVisibleRect)のGlyphがレイアウトされていることを保証する
        [self.layoutManager ensureLayoutForBoundingRect:newVisibleRect inTextContainer:self.textContainer];
		// ----------------------------------------
		// Simulate user scroll
		// ----------------------------------------
        if (needsAdjust) {
            newVisibleRect.origin = [clipView constrainScrollPoint:newOrigin];
        }
		newVisibleRect = [[clipView documentView] adjustScroll:newVisibleRect];
		[clipView scrollToPoint:newVisibleRect.origin];
		[self.documentScrollView reflectScrolledClipView:clipView];
	}
	
	[self.statusLine validateIdxNavLazily:self];
}

- (void)messageDidChangeNotification:(NSNotification *)notification
{
	id info = [notification userInfo];
	NSNumber *indexValue = [info objectForKey:KMDocumentChangedMessageIndexKey];
	NSUInteger anIndex = [indexValue integerValue];
	
	
	NSMutableAttributedString		*textBuffer_;
	CMRAttributedMessageComposer	*composer_;
	CMRThreadMessage				*m;
	NSRange							mesRange_;
	
	if (NSNotFound == anIndex || [self.doc numberOfReadedMessages] <= anIndex) {
		return;
    }
	
	
	do {
		m = [self.doc messageAtIndex:anIndex];
		mesRange_ = [self rangeAtMessageIndex:anIndex];
		// 非表示のレスは生成しない
		if (![m isVisible]) {
			if (mesRange_.length != 0) {
				@synchronized(self) {
					[self.privateTextStorage deleteCharactersInRange:mesRange_];
				}
			}
			break;
		}
		
		composer_ = [[CMRAttributedMessageComposer alloc] init];
		textBuffer_ = [[NSMutableAttributedString alloc] init];
		
		[composer_ setComposingMask:CMRInvisibleMask compose:NO];
		[composer_ setContentsStorage:textBuffer_];
		
		[composer_ composeThreadMessage:m];
		
		@synchronized(self) {
			[self.privateTextStorage replaceCharactersInRange:mesRange_
										 withAttributedString:textBuffer_];
		}
		
		[textBuffer_ release];
		[composer_ release];
		textBuffer_ = nil;
		composer_ = nil;
	} while (0);
	
    [[self layoutManager] invalidateDisplayForCharacterRange:mesRange_];
    [[self layoutManager] ensureLayoutForCharacterRange:mesRange_];
	KMClearRangesFromIndex(self, anIndex);
}

- (NSDate *)lastUpdatedDateFromHeaderAttachment
{
	return [self lastUpdatedDateFromFirstHeaderAttachmentEffectiveRange:NULL];
}

- (NSRange)firstLastUpdatedHeaderAttachmentRange
{
	NSRange effectiveRange_;
	
	[self lastUpdatedDateFromFirstHeaderAttachmentEffectiveRange:&effectiveRange_];
	return effectiveRange_;
}

- (NSDate *)lastUpdatedDateFromFirstHeaderAttachmentEffectiveRange:(NSRangePointer)effectiveRange
{
	__block BOOL found = NO;
	__block id value_ = nil;
	
	@synchronized(self) {
		[self.privateTextStorage enumerateAttribute:CMRMessageLastUpdatedHeaderAttributeName
											inRange:NSMakeRange(0, [self.privateTextStorage length])
											options:NSAttributedStringEnumerationReverse
										 usingBlock:^(id value, NSRange range, BOOL *stop) {											 
											 if(value) {
												 if (effectiveRange != NULL) {
													 *effectiveRange = range;
												 }
												 if ([value isKindOfClass:[NSDate class]]) {
													 value_ = value;
												 }
												 *stop = YES;
												 found = YES;
											 }
										 }];
	}
	
	if(!found && effectiveRange != NULL) {
		*effectiveRange = NSMakeRange(NSNotFound, 0);
	}
	return value_;
}

- (void)appendLastUpdatedHeader:(BOOL)flag
{
	NSAttributedString	*header_;
	NSRange				range_;
	id					templateMgr = [CMRMessageAttributesTemplate sharedTemplate];
	
	header_ = [templateMgr lastUpdatedHeaderAttachment];
	if (!header_) { 
		return;
    }
	@synchronized(self) {
		NSTextStorage		*tS_ = self.privateTextStorage;
		[tS_ beginEditing];
		range_.location = [tS_ length];
		[tS_ appendAttributedString:header_];
		range_.length = [tS_ length] - range_.location;
		// 現在の日付を属性として追加
		[tS_ addAttribute:CMRMessageLastUpdatedHeaderAttributeName value:[NSDate date] range:range_];
		[tS_ endEditing];
	}
}

- (void)appendLastUpdatedHeader
{
    [self appendLastUpdatedHeader:YES];
}

- (void)clearLastUpdatedHeader:(BOOL)flag
{
	NSRange headerRange_;
	
	headerRange_ = [self firstLastUpdatedHeaderAttachmentRange];
	if (NSNotFound == headerRange_.location) {
        return;
    }
	
	@synchronized(self) {
		[self.privateTextStorage deleteCharactersInRange:headerRange_];
	}
}

- (void)clearLastUpdatedHeader
{
    [self clearLastUpdatedHeader:YES];
}

- (void)insertLastUpdatedHeader
{
    [self clearLastUpdatedHeader:NO];
    [self appendLastUpdatedHeader:NO];
}

static BOOL KMIsRangeInRange(NSRange aRange, NSRange ofRange)
{
	return NSIntersectionRange(aRange, ofRange).length != 0;
}
static NSUInteger KMBinarySearchIndex(KMBSLogViewController *target, NSUInteger min, NSUInteger max, NSRange aRange)
{
	if(aRange.location == NSNotFound || aRange.length == 0) return NSNotFound;
	if(min == NSNotFound || max == NSNotFound) return NSNotFound;
	if(min == max) {
		if(max == 0) return NSNotFound;
		return max;
	}
	if(min > max) return NSNotFound;
	
	NSUInteger mid = (max + min) / 2;
	
	NSRange midRange, minRange;
	minRange = [target rangeAtMessageIndex:min];
	if(KMIsRangeInRange(minRange, aRange)) return min;
	midRange = [target rangeAtMessageIndex:mid];
	if(KMIsRangeInRange(midRange, aRange)) {
		NSUInteger index;
		for(index = mid - 1; index > min; index--) {
			NSRange range = [target rangeAtMessageIndex:index];
			if(!KMIsRangeInRange(range, aRange)) {
				break;
			}
		}
		return index + 1;
	}
	if(aRange.location > midRange.location) {
		return KMBinarySearchIndex(target, mid + 1, max, aRange);
	} else {
		return KMBinarySearchIndex(target, min + 1, mid, aRange);
	}
}
static void KMSetRnageByIndex(KMBSLogViewController *obj, NSRange range, NSUInteger index)
{
	if(!obj->ranges) {
		obj->ranges = [[NSMutableDictionary alloc] initWithCapacity:1024];
		if(!obj->ranges) {
			[NSException raise:NSMallocException format:@"Can not allocate memory of KMBSLogViewController's range"];
		}
	}
	[obj->ranges setObject:[NSValue valueWithRange:range]
					forKey:[NSNumber numberWithUnsignedInteger:index]];
}
static NSRange KMRangeOfIndex(KMBSLogViewController *obj, NSUInteger index)
{
	if(!obj->ranges) return NSMakeRange(NSNotFound, 0);
	id val = [obj->ranges objectForKey:[NSNumber numberWithUnsignedInteger:index]];
	if(!val) return NSMakeRange(NSNotFound, 0);
	return [val rangeValue];
}
static void KMClearRangesFromIndex(KMBSLogViewController *obj, NSUInteger index)
{
	if(!obj->ranges) return;
	
	NSMutableArray *delKeys = [NSMutableArray array];
	for(NSNumber *key in obj->ranges) {
		if([key unsignedIntegerValue] > index) {
			[delKeys addObject:key];
		}
	}
	[obj->ranges removeObjectsForKeys:delKeys];
}
- (NSRange)rangeAtMessageIndex:(NSUInteger)anIndex
{
	NSRange stackRange = KMRangeOfIndex(self, anIndex);
	if(stackRange.location != NSNotFound) return stackRange;
	
	NSNumber *indexValue = [NSNumber numberWithInteger:anIndex];
	__block NSRange targetRange = {NSNotFound,0};
	__block BOOL lastMessage = YES;
	
	@synchronized(self) {
		NSRange range = NSMakeRange(0, [self.privateTextStorage length]);
		[self.privateTextStorage enumerateAttribute:CMRMessageIndexAttributeName
											inRange:range
											options:0
										 usingBlock:^(id value, NSRange range, BOOL *stop) {
											 if(!value) return;
											 if([value integerValue] > anIndex) {
												 *stop = YES;
												 targetRange.length = range.location - targetRange.location;
												 lastMessage = NO;
											 }
											 if([value isEqual:indexValue]) {
												 targetRange = range;
											 }
										 }];
		if(targetRange.location == NSNotFound) return targetRange;
		if(lastMessage) {
			targetRange.length = self.privateTextStorage.length - targetRange.location;
		}
	}
	KMSetRnageByIndex(self, targetRange, anIndex);
	
	return targetRange;
}
- (NSUInteger)messageIndexForRange:(NSRange)aRange
{
	return KMBinarySearchIndex(self, 0, self.numberOfMessage - 1, aRange);
}
- (NSUInteger)firstMessageIndexInVisibleRectForDocument:(KMDocument *)document
{
	if(self.doc != document) return NSNotFound;
	
	NSRange visibleRange_;
	
	visibleRange_ = [self.documentView characterRangeForDocumentVisibleRect];
	
	// 各レスの最後には空行が含まれるため、表示されている範囲を
	// そのまま渡すと見た目との齟齬が気になる。
	// よって、位置を改行ひとつ分ずらす。
	if (visibleRange_.length > 1) {
		visibleRange_.location += 1;
		visibleRange_.length -= 1;	//範囲チェックを省く簡便のため
	}
	
	return [self messageIndexForRange:visibleRange_];
}
- (NSUInteger)lastMessageIndexForRangeSilverGull:(NSRange)aRange
{
//	NSUInteger				index_;
//	
//	index_ = [[self messageRanges] count] -1;
//	
//	NSRange		mesRng_;
//	NSRange		intersection_;
//	
//	mesRng_ = [[self messageRanges] last];
//	intersection_ = NSIntersectionRange(mesRng_, aRange);
//	if (NSMaxRange(intersection_) == NSMaxRange(mesRng_)) {
//		return index_;
//	}
	
	return [self messageIndexForRange:aRange];
}
- (NSUInteger)lastMessageIndexInVisibleRectForDocument:(KMDocument *)document
{
	if(self.doc != document) return NSNotFound;
	
	NSRange visibleRange_;
	
	visibleRange_ = [self.documentView characterRangeForDocumentVisibleRect];
	
	if (visibleRange_.length > 1) {
		visibleRange_.location += 1;
		visibleRange_.length -= 1;
	}
	
    // とりあえずの修正、1.6.2 以降でもっときちんと
	return [self lastMessageIndexForRangeSilverGull:visibleRange_];
}
- (NSUInteger)lastMessageIndexForDocumentVisibleRect
{
	return [self lastMessageIndexInVisibleRectForDocument:self.doc];
}
- (NSUInteger)firstMessageIndexForDocumentVisibleRect
{
	return [self firstMessageIndexInVisibleRectForDocument:self.doc];
}

// 次／前のレス
- (NSUInteger)nextMessageIndexOfIndex:(NSUInteger)index attribute:(UInt32)flags value:(BOOL)attributeIsSet
{
	NSUInteger i;
    NSUInteger cnt;
	CMRThreadMessage *m;
	
	if (NSNotFound == index) {
		return NSNotFound;
    }
	cnt = self.numberOfMessage;
	if (cnt <= index) {
		return NSNotFound;
    }
	for (i = index +1; i < cnt; i++) {
		m = [self.doc messageAtIndex:i];
		if (attributeIsSet == (([m flags] & flags) != 0)) {
			return i;
        }
	}
	
	return NSNotFound;
}

- (NSUInteger)previousMessageIndexOfIndex:(NSUInteger)index attribute:(UInt32)flags value:(BOOL)attributeIsSet
{
    NSInteger i;
	CMRThreadMessage *m;
	
	if (NSNotFound == index) {
		return NSNotFound;
	}
	if (0 == index) {
		return NSNotFound;
	}
	for (i = (index - 1); i >= 0; i--) {
		m = [self.doc messageAtIndex:i];
		if (attributeIsSet == (([m flags] & flags) != 0)) {
			return i;
        }
	}
	
	return NSNotFound;
}

- (NSUInteger)messageIndexOfLaterDate:(NSDate *)baseDate attribute:(UInt32)flags value:(BOOL)attributeIsSet
{
	NSUInteger i;
    NSUInteger cnt;
	CMRThreadMessage *m;
	id msgDate;
	
	if (!baseDate) {
		return NSNotFound;
    }
	
	cnt = [self.doc numberOfReadedMessages];
	
	for (i = 0; i < cnt; i++) {
		m = [self.doc messageAtIndex:i];
		msgDate = [m date];
		if (!msgDate || ![msgDate isKindOfClass:[NSDate class]]) {
            continue;
        }
		if (([(NSDate *)msgDate compare: baseDate] != NSOrderedAscending) && (attributeIsSet == (([m flags] & flags) != 0))) {
			return i;
		}
	}
	
	return NSNotFound;
}
- (NSUInteger)firstUnlaidMessageIndex
{
	NSLog(@"%s is deprecated.", __PRETTY_FUNCTION__);
	return self.numberOfMessage;
}

#pragma mark Jumpable index
- (NSUInteger)nextVisibleMessageIndex
{
	return [self nextVisibleMessageIndexOfIndex:[self firstMessageIndexForDocumentVisibleRect]];
}

- (NSUInteger)previousVisibleMessageIndex
{
	return [self previousVisibleMessageIndexOfIndex:[self firstMessageIndexForDocumentVisibleRect]];
}

static UInt32 attributeMaskForVisibleMessageIndexDetection()
{
	if (kSpamFilterInvisibleAbonedBehavior == [CMRPref spamFilterBehavior]) {
		return (CMRInvisibleAbonedMask|CMRSpamMask);
	} else {
		return CMRInvisibleAbonedMask;
	}
}

- (NSUInteger)nextVisibleMessageIndexOfIndex:(NSUInteger) anIndex
{
	return [self nextMessageIndexOfIndex:anIndex 
							   attribute:attributeMaskForVisibleMessageIndexDetection()
								   value:NO];
}

- (NSUInteger)previousVisibleMessageIndexOfIndex:(NSUInteger)anIndex
{
	return [self previousMessageIndexOfIndex:anIndex 
								   attribute:attributeMaskForVisibleMessageIndexDetection()
									   value:NO];
}

#pragma mark Jumping to bookmarks
- (NSUInteger)nextBookmarkIndex
{
	return [self nextBookmarkIndexOfIndex:[self firstMessageIndexForDocumentVisibleRect]];
}

- (NSUInteger)previousBookmarkIndex
{
	return [self previousBookmarkIndexOfIndex:[self firstMessageIndexForDocumentVisibleRect]];
}

- (NSUInteger)nextBookmarkIndexOfIndex:(NSUInteger)anIndex
{
	return [self nextMessageIndexOfIndex:anIndex attribute:CMRBookmarkMask value:YES];
}

- (NSUInteger)previousBookmarkIndexOfIndex:(NSUInteger) anIndex
{
	return [self previousMessageIndexOfIndex:anIndex attribute:CMRBookmarkMask value:YES];
}
#pragma mark Jumping to Specific date's Message
- (NSUInteger)messageIndexOfLaterDate:(NSDate *)baseDate
{
	return [self messageIndexOfLaterDate:baseDate attribute:attributeMaskForVisibleMessageIndexDetection() value:NO];
}

// KMDocument
- (NSArray *)messagesAtIndexes:(NSIndexSet *)indexes
{
	return [self.doc messagesAtIndexes:indexes];
}
- (NSUInteger)numberOfReadedMessages
{
	return [self.doc numberOfReadedMessages];
}
@end

@implementation KMBSLogViewController(BuildViews)
#pragma mark Title Ruler
- (BOOL)shouldShowTitleRulerView
{
	return self.hasTitleRuler;
}

- (BSTitleRulerModeType)rulerModeForInformDatOchi
{
	if([self shouldShowTitleRulerView]) {
		return BSTitleRulerShowTitleAndInfoMode;
	}
	return BSTitleRulerShowInfoOnlyMode;
}

+ (NSString *)titleRulerAppearanceFilePath
{
	NSString *path;
	NSBundle *appSupport = [NSBundle applicationSpecificBundle];
	
	path = [appSupport pathForResource:@"BSTitleRulerAppearance" ofType:@"plist"];
	if (!path) {
		path = [[NSBundle mainBundle] pathForResource:@"BSTitleRulerAppearance" ofType:@"plist"];
	}
	return path;
}

- (void)setupTitleRulerWithScrollView:(NSScrollView *)scrollView_
{
	id ruler;
	NSString *path = [[self class] titleRulerAppearanceFilePath];
	UTILAssertNotNil(path);
	BSTitleRulerAppearance *foo = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
	
	[[scrollView_ class] setRulerViewClass:[BSTitleRulerView class]];
	ruler = [[BSTitleRulerView alloc] initWithScrollView:scrollView_ appearance:foo];
	[ruler setTitleStr:NSLocalizedString(@"titleRuler default title", @"Startup Message")];
	
	[scrollView_ setHorizontalRulerView:ruler];
    [ruler release];
	[scrollView_ setHasHorizontalRuler:YES];
	[scrollView_ setRulersVisible:[self shouldShowTitleRulerView]];
}

- (void)cleanUpTitleRuler:(NSTimer *)aTimer
{
	BSTitleRulerView *view_ = (BSTitleRulerView *)[self.documentScrollView horizontalRulerView];
	
	[self.documentScrollView setRulersVisible:[self shouldShowTitleRulerView]];
	[view_ setCurrentMode:BSTitleRulerShowTitleOnlyMode];
	if(self.doc.threadTitle && self.doc.boardName) {
		NSString *rulerTitle = [NSString stringWithFormat:@"%@ %C %@", self.doc.threadTitle, 0x2014, self.doc.boardName];
		[view_ setTitleStr:rulerTitle];
		[view_ setPathStr:self.doc.path];
	} else {
		[view_ setTitleStr:NSLocalizedString(@"titleRuler default title", @"Startup Message")];
		[view_ setPathStr:nil];
	}
}

- (void)contentViewBoundsDidChange:(NSNotification *)notification
{
	UTILAssertNotificationName(
							   notification,
							   NSViewBoundsDidChangeNotification);
	UTILAssertNotificationObject(
								 notification,
								 [self.documentScrollView contentView]);
	
	[self.statusLine validateIdxNavLazily:self];
}

- (NSScrollerKnobStyle)appropriateKnobStyleForThreadViewBGColor
{
	CGFloat r,g,b;
	CGFloat distanceWhite, distanceBlack;
	NSColor *color = [[[CMRPref threadViewTheme] backgroundColor] colorUsingColorSpaceName:NSCalibratedRGBColorSpace];
	[color getRed:&r green:&g blue:&b alpha:NULL];
	distanceBlack = fabs(r) + fabs(g) + fabs(b);
	distanceWhite = fabs(r - 1.0) + fabs(g - 1.0) + fabs(b - 1.0);
		
	return (distanceBlack < distanceWhite) ? NSScrollerKnobStyleLight : NSScrollerKnobStyleDefault;
}

- (void)setupScrollView
{
	NSClipView		*contentView_;
	
	contentView_ = [self.documentScrollView contentView];
	[contentView_ setPostsBoundsChangedNotifications:YES];
	
	[[NSNotificationCenter defaultCenter] addObserver:self
											 selector:@selector(contentViewBoundsDidChange:)
												 name:NSViewBoundsDidChangeNotification
											   object:contentView_];
	
	[self.documentScrollView setBorderType:NSNoBorder];
	[self.documentScrollView setHasHorizontalScroller:NO];
	[self.documentScrollView setHasVerticalScroller:YES];
	
	[self setupTitleRulerWithScrollView:self.documentScrollView];
}

- (NSMenu *)loadContextualMenuForTextView
{
	NSMenu	*menu_;
	
	NSMenu	*textViewMenu_;
	NSMenuItem	*item_;
	
	menu_ = [[CMRMainMenuManager defaultManager] threadContexualMenuTemplate];
	textViewMenu_ = [CMRThreadView messageMenu];
	
	[menu_ addItem:[NSMenuItem separatorItem]];
	
	for(item_ in [textViewMenu_ itemArray]) {
		item_ = [item_ copy];
		[menu_ addItem:item_];
		[item_ release];
	}
	
	return menu_;
}

- (void)threadViewThemeDidChange:(NSNotification *)notification
{
	[self setupTextViewBackground];
	[self updateLayoutSettings];
	
	
	@synchronized(self) {
		[self.privateTextStorage setAttributedString:[[[NSAttributedString alloc] initWithString:@""] autorelease]];
	}
	self.numberOfMessage = 0;
	[self composeDocumentMessage:self];
}
- (void)appDefaultsLayoutSettingsUpdated:(NSNotification *)notification
{
	UTILAssertNotificationName(notification, AppDefaultsLayoutSettingsUpdatedNotification);
	UTILAssertNotificationObject(notification, CMRPref);
	
	if (!self.documentView) return;
	[self updateLayoutSettings];
	[self.documentScrollView setNeedsDisplay:YES];
}

- (void)setupTextViewBackground
{
	NSColor		*color = [[CMRPref threadViewTheme] backgroundColor];
	
	// textView
	[self.documentView setDrawsBackground:YES];
	[self.documentView setBackgroundColor:color];
	// scrollView
	[self.documentScrollView setDrawsBackground:YES];
	[self.documentScrollView setBackgroundColor:color];
	if (floor(NSAppKitVersionNumber) > NSAppKitVersionNumber10_6) {
		 [self.documentScrollView setScrollerKnobStyle:[self appropriateKnobStyleForThreadViewBGColor]];
	 }
}
- (void)updateLayoutSettings
{
	[(BSLayoutManager *)self.layoutManager setShouldAntialias:[CMRPref shouldThreadAntialias]];
	[self.documentView setLinkTextAttributes:[[CMRMessageAttributesTemplate sharedTemplate] attributesForAnchor]];
}

- (void)setupTextView
{
	NSLayoutManager		*layout;
	NSTextContainer		*container;
	NSTextView			*view;
	NSRect				cFrame;
	
	cFrame.origin = NSZeroPoint; 
	cFrame.size = [self.documentScrollView contentSize];
	
	/* LayoutManager */
	layout = [[BSLayoutManager alloc] init];
	[layout setAllowsNonContiguousLayout:YES];
	self.layoutManager = layout;
	
	self.privateTextStorage = [[[NSTextStorage alloc] init] autorelease];
	[self.privateTextStorage addLayoutManager:layout];
	[layout release];
	
	/* TextContainer */
	container = [[NSTextContainer alloc] initWithContainerSize:NSMakeSize(NSWidth(cFrame), 1e7)];
	[layout addTextContainer:container];
	self.textContainer = container;
	[container release];
	
	/* TextView */
	view = [[CMRThreadView alloc] initWithFrame:cFrame textContainer:container];
	
	[view setMinSize:NSMakeSize(0.0, NSHeight(cFrame))];
	[view setMaxSize:NSMakeSize(1e7, 1e7)];
	[view setVerticallyResizable:YES];
	[view setHorizontallyResizable:NO];
	[view setAutoresizingMask:NSViewWidthSizable];
	
	[container setWidthTracksTextView:YES];
	
	[view setEditable:NO];
	[view setSelectable:YES];
	[view setAllowsUndo:NO];
	[view setImportsGraphics:NO];
	[view setFieldEditor:NO];
	
	[view setMenu:[self loadContextualMenuForTextView]];
	self.logViewDelegate = [[[KMBSLogViewDelegate alloc] init] autorelease];
	self.logViewDelegate.owner = self;
	self.logViewDelegate.doc = self.doc;
	[view setDelegate:self.logViewDelegate];
	
    [view setDisplaysLinkToolTips:NO];
	
	self.documentView = view;
	
	[self setupTextViewBackground];
	[self updateLayoutSettings];
	
	[self.documentScrollView setDocumentView:view];
	
	self.popUp = [[[KMBSLogPopUp alloc] init] autorelease];
	self.popUp.owner = self;
	self.popUp.documentView = self.documentView;
	
	[view release];
}

@end

#import <SGAppKit/BSHistoryOverlayController.h>
// 10.6 SDK でビルド警告が出ないようにするために
enum {
    NSEventPhaseNone        = 0, // event not associated with a phase.
    NSEventPhaseBegan       = 0x1 << 0,
    NSEventPhaseStationary  = 0x1 << 1,
    NSEventPhaseChanged     = 0x1 << 2,
    NSEventPhaseEnded       = 0x1 << 3,
    NSEventPhaseCancelled   = 0x1 << 4,
};
typedef NSUInteger NSEventPhase;

enum {
    NSEventSwipeTrackingLockDirection = 0x1 << 0,
    NSEventSwipeTrackingClampGestureAmount = 0x1 << 1
};
typedef NSUInteger NSEventSwipeTrackingOptions;

@interface NSEvent(LionStub)
- (CGFloat)scrollingDeltaX;
- (CGFloat)scrollingDeltaY;
- (NSUInteger)phase;
+ (BOOL)isSwipeTrackingFromScrollEventsEnabled;
- (void)trackSwipeEventWithOptions:(NSUInteger)options
          dampenAmountThresholdMin:(CGFloat)minDampenThreshold
                               max:(CGFloat)maxDampenThreshold
                      usingHandler:(void (^)(CGFloat gestureAmount, NSUInteger phase, BOOL isComplete, BOOL *stop))trackingHandler;
@end

@implementation KMBSLogViewController (NSTextViewDelegate)

- (void)colorizeID:(NSTextView *)textView
{
	NSTextStorage *ts = [textView textStorage];
	NSLayoutManager *lm = [textView layoutManager];
	[ts enumerateAttribute:BSMessageIDAttributeName
				   inRange:NSMakeRange(0, [ts length])
				   options:0
				usingBlock:^(id idString, NSRange coloringRange, BOOL *stop) {
					NSUInteger countOfId = [self.countedSet countForObject:idString];
					NSColor *color = nil;
					if ((countOfId > 1) && (countOfId < 5)) {
						color = [[CMRPref threadViewTheme] informativeIDColor];
					} else if ((countOfId > 4) && (countOfId < 10)) {
						color = [[CMRPref threadViewTheme] warningIDColor];
					} else if (countOfId > 9) {
						color = [[CMRPref threadViewTheme] criticalIDColor];
					} else {
						return;
					}
					[lm addTemporaryAttribute:NSForegroundColorAttributeName 
										value:color
							forCharacterRange:coloringRange];
					
				}];
}

- (void)replyTo:(NSIndexSet *)messageIndexes
{
	[self.messenger clearQuotation];
	self.messenger.index = [messageIndexes firstIndex];
	
	NSRange selectedRange = [self.documentView selectedRange];
	if(selectedRange.length != 0) {
		self.messenger.range = selectedRange;
		NSUInteger index = [[self threadLayout] messageIndexForRange:selectedRange];
		if([messageIndexes containsIndex:index]) {
			self.messenger.string = [[self.documentView string] substringWithRange:selectedRange];
		}
	}
	[self.messenger reply:self];
}
- (void)magnifyEnough:(CGFloat)additionalScaleFactor
{
    if (additionalScaleFactor > 0.5) {
        [self biggerText:self];
    } else if (additionalScaleFactor < -0.5) {
        [self smallerText:self];
    }
}
- (void)rotateEnough:(CGFloat)rotatedDegree
{
    BSTitleRulerView *ruler = (BSTitleRulerView *)[self.documentScrollView horizontalRulerView];
    
    [ruler setCurrentMode:[self rulerModeForInformDatOchi]];
    [ruler setInfoStr:[self localizedString:@"titleRuler info rotate gesture title"]];
    [self.documentScrollView setRulersVisible:YES];
    [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(cleanUpTitleRuler:) userInfo:nil repeats:NO];
}
#pragma mark Gesuture Support
- (BOOL)wantsScrollEventsForSwipeTrackingOnAxis:(NSInteger)axis
{
    if (![CMRPref multitouchGestureEnabled]) {
        return NO;
    }
    return axis == 1;
}
- (void)scrollWheel:(NSEvent *)event
{
    if (floor(NSAppKitVersionNumber) <= NSAppKitVersionNumber10_6) {
        return;
    }
	
    if ([event phase] == NSEventPhaseNone) {
        return; // Not a gesture scroll event.
    }
	
    CGFloat foo = [event scrollingDeltaX];
    CGFloat bar = [event scrollingDeltaY];
    if (fabsf(foo) <= fabsf(bar)) { // Not horizontal
        return;
    }
    if (![NSEvent isSwipeTrackingFromScrollEventsEnabled]) {
        return;
    }
    
    BOOL goForward = (foo < 0);
    HistoryOverlayController* historyOverlay = [[HistoryOverlayController alloc] initForMode:goForward ? kHistoryOverlayModeForward : kHistoryOverlayModeBack];
	
    [event trackSwipeEventWithOptions:NSEventSwipeTrackingClampGestureAmount
             dampenAmountThresholdMin:([self.histories canForward] ? -1 : 0)
                                  max:([self.histories canBack] ? 1 : 0)
                         usingHandler:^(CGFloat gestureAmount, NSUInteger phase, BOOL isComplete, BOOL *stop) {
                             if (phase == NSEventPhaseBegan) {
                                 NSRect rect = [self.documentScrollView frame];
                                 rect = [self.documentScrollView convertRect:rect toView:nil];
                                 NSPoint point = rect.origin;
                                 point = [self.view.window convertBaseToScreen:point];
                                 rect.origin = point;
                                 [historyOverlay showPanelWithinRect:rect];
                                 return;
                             }
							 
                             if (phase == NSEventPhaseEnded) {
                                 if (goForward) {
                                     [self historyMenuPerformForward:self];
                                 } else {
                                     [self historyMenuPerformBack:self];
                                 }
                             }
                             [historyOverlay setProgress:gestureAmount];
							 
							 if (isComplete) {
								 [historyOverlay dismiss];
								 [historyOverlay release];
							 }
						 }];
}
@end

#import "KMThreadDeleteCenter.h"
@interface KMBSLogViewController(KMThreadDeleteCenterDelegate) <KMThreadDeleteCenterDelegate>
@end
@implementation KMBSLogViewController(KMThreadDeleteCenterDelegate)
- (void)deleteCenter:(KMThreadDeleteCenter *)center retrieveThreads:(NSArray *)threads
{
	[self.doc retrieve:self];
}
@end

#import "BSLabelMenuItemView.h"
@interface KMBSLogViewController(BSLabelMenuItemViewValidation) <BSLabelMenuItemViewValidation>
- (BOOL)validateLabelMenuItem:(BSLabelMenuItemView *)item;
@end

@implementation KMBSLogViewController(BSLabelMenuItemViewValidation)
- (BOOL)validateLabelMenuItem:(BSLabelMenuItemView *)item
{
	[item deselectAll];
	
	NSUInteger label = self.doc.threadAttr.label;
	[item setSelected:YES forLabel:label clearOthers:NO];
	
	return YES;
}

- (IBAction)toggleLabeledThread:(id)sender
{
	NSUInteger label = 0;
	if ([sender isKindOfClass:[BSLabelMenuItemView class]]) {
		label = [sender clickedLabel];
	} else {
		label = [sender tag];
		NSUInteger currentLabel = [self.doc.threadAttr label];
		if (currentLabel == label) {
			label = 0;
		}
	}
	[self.doc setLabelValue:label];
}
@end

@implementation KMBSLogViewController(Actions)
#import "CMRFavoritesManager+KMAddition.h"

#define kReplyItemKey				@"Reply..."
#define kReplyToItemKey				@"Reply 2..."

#define kDeleteWithoutAlertKey			@"Delete Log"
#define kDeleteWithAlertKey				@"Delete Log..."

/*** アクション・メニュー ***/
#define kActionMenuItemTag				(100)	/* 「アクション」 */
#define kActionSpamHeader				(111)	/* 「迷惑レス」ヘッダ */
#define kActionAAHeader					(222)	/* 「AA」ヘッダ */
#define kActionBookmarkHeader			(333)	/* 「ブックマーク」ヘッダ */
#define kActionLocalAbonedHeader		(444)	/* 「ローカルあぼーん」ヘッダ */
#define kActionInvisibleAbonedHeader	(555)	/* 「透明あぼーん」ヘッダ */

- (NSPoint)locationForInformationPopUp
{
	id			docView_;
	NSPoint		loc;
	
	docView_ = [self.documentView enclosingScrollView];
	docView_ = [docView_ contentView];
	
	loc = [docView_ frame].origin;
	loc.y = NSMaxY([docView_ frame]);
	
	docView_ = [self.documentView enclosingScrollView];
	loc = [docView_ convertPoint:loc toView:nil];
	loc = [[docView_ window] convertBaseToScreen:loc];
	return loc;
}

/*** レス属性 ***/
static NSInteger messageMaskForTag(NSInteger tag)
{
	if (kActionInvisibleAbonedHeader <= tag) {
		return CMRInvisibleAbonedMask;
	} else if (kActionLocalAbonedHeader <= tag) {
		return CMRLocalAbonedMask;
	} else if (kActionBookmarkHeader <= tag) {
		return CMRBookmarkMask;
	} else if (kActionAAHeader <= tag) {
		return CMRAsciiArtMask;
	} else if (kActionSpamHeader <= tag) {
		return CMRSpamMask;
	} 
	return 0;
}

- (IBAction) showMessageMatchesAttributes : (id) sender
{
	NSPoint				location_;
	NSUInteger			composingMask_;
	
	composingMask_ = messageMaskForTag([sender tag]);
	
	location_ = [self locationForInformationPopUp];
	[self.popUp tryShowPopUpMatchAttributes:composingMask_ locationHint:location_];
}

- (IBAction)reply:(id)sender
{
	[self.messenger clearQuotation];
	
	NSRange selectedRange = [self.documentView selectedRange];
	if(selectedRange.length != 0) {
		self.messenger.range = selectedRange;
		self.messenger.index = [[self threadLayout] messageIndexForRange:selectedRange];
		self.messenger.string = [[self.documentView string] substringWithRange:selectedRange];
	}
	[self.messenger reply:sender];
}

- (IBAction)retrieveThread:(id)sender
{
	if ([CMRPref oldMessageScrollingBehavior]) {
		self.doc.lastViewingIndex = [self firstMessageIndexForDocumentVisibleRect];
	} else {
		self.doc.lastViewingIndex = [self lastMessageIndexForDocumentVisibleRect];
	}
	
	KMThreadDeleteCenter *center = [[[KMThreadDeleteCenter alloc] init] autorelease];
	center.delegate = self;
	[center showAlert:CMRThreadViewerRetrieveAlertType targetThreads:[NSArray arrayWithObject:self.doc.threadAttr]];
}
- (IBAction)deleteThread:(id)sender
{
	KMThreadDeleteCenter *center = [[[KMThreadDeleteCenter alloc] init] autorelease];
	center.delegate = self;
	[center showAlert:CMRThreadViewerDeletionAlertType targetThreads:[NSArray arrayWithObject:self.doc.threadAttr]];
}

- (IBAction)showBoardInspectorPanel:(id)sender
{
    NSString *boardName = self.doc.boardName;
    if (!boardName) {
        return;
    }
    [[BSBoardInfoInspector sharedInstance] showInspectorForTargetBoard:boardName];
}

- (IBAction)addFavorites:(id)sender
{
	[[CMRFavoritesManager defaultManager] doActionWithThreadAttributes:[NSArray arrayWithObject:self.doc.threadAttr]];
}

- (IBAction)historyMenuPerformBack:(id)sender
{
	if(![self.histories canBack]) return;
	
	CMRThreadSignature *sig = [self.histories back];
	id dc = [NSDocumentController sharedDocumentController];
	NSError *error = nil;
	KMDocument *doc = [dc openDocumentWithContentsOfURL:[NSURL fileURLWithPath:[sig threadDocumentPath]]
												display:NO
												  error:&error];
	[self.histories ignoreNextPush];
	[doc addWindowController:self.view.window.windowController];
}
- (IBAction)historyMenuPerformForward:(id)sender
{
	if(![self.histories canForward]) return;
	
	CMRThreadSignature *sig = [self.histories forward];
	id dc = [NSDocumentController sharedDocumentController];
	NSError *error = nil;
	KMDocument *doc = [dc openDocumentWithContentsOfURL:[NSURL fileURLWithPath:[sig threadDocumentPath]]
												display:NO
												  error:&error];
	[self.histories ignoreNextPush];
	[doc addWindowController:self.view.window.windowController];
}
- (BOOL)validateActionMenuItem:(NSMenuItem *)theItem
{
	NSInteger			tag = [theItem tag];
//	SEL			action = [theItem action];
	NSUInteger	mask;
		
	mask = messageMaskForTag(tag);
	if (mask != 0) {
		NSUInteger	nMatches;		
		
		nMatches = [self.doc numberOfMessageAttributes:mask];
		
		{
			NSString	*title_ = @"";
			NSString	*key_   = nil;
			
			if (kActionSpamHeader == tag) {
				key_ = @"ActionSpamHeaderFormat";
			} else if (kActionAAHeader == tag) {
				key_ = @"ActionAAHeaderFormat";
			} else if (kActionBookmarkHeader == tag) {
				key_ = @"ActionBookmarkHeaderFormat";
			} else if (kActionLocalAbonedHeader == tag) {
				key_ = @"ActionLocalAbonedHeaderFormat";
			} else if (kActionInvisibleAbonedHeader == tag) {
				key_ = @"ActionInvisibleAbonedHeaderFormat";
			}
			if (key_) {
				// ヘッダ
				title_ = [self localizedString:key_];
				title_ = [NSString stringWithFormat:title_, (unsigned long)nMatches];
				[theItem setTitle:title_];
			}
		}
		
		return (nMatches != 0);
	}
	return NO;
}

- (BOOL)validateReplyItem:(id)theItem
{
	if(!self.doc) return NO;
	
	NSString		*title_;
	
	title_ = (0 == ([self.documentView selectedRange]).length)
	? [self localizedString:kReplyItemKey]
	: [self localizedString:kReplyToItemKey];
	
	[theItem setTitle:title_];		
	
	return YES;//([self threadAttributes]);
}

- (void)validateDeleteThreadItemTitle:(id)theItem
{
	if ([theItem isKindOfClass:[NSMenuItem class]]) {
        NSString *key = [CMRPref quietDeletion] ? kDeleteWithoutAlertKey : kDeleteWithAlertKey;
		[theItem setTitle:[self localizedString:key]];
	}
}

- (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)theItem
{
	SEL	action_ = [theItem action];
	
	//		if (action_ == @selector(clearMessageAttributes:) || action_ == @selector(showMessageMatchesAttributes:) ||
	//			action_ == @selector(setOnMessageAttributes:)) {
    if (action_ == @selector(showMessageMatchesAttributes:)) {
        return [self validateActionMenuItem:(NSMenuItem *)theItem];
    }
	
	// レス
	if (action_ == @selector(reply:)) {
		return [self validateReplyItem:theItem];
	}
	
	// お気に入りに追加
	if (action_ == @selector(addFavorites:)) {
		if(!self.doc.threadAttr) return NO;
		return [[CMRFavoritesManager defaultManager] validateItem:theItem withThreadAttributes:[NSArray arrayWithObject:self.doc.threadAttr]];
	}
	
	// ログを削除(...)
	if (action_ == @selector(deleteThread:)) {
		[self validateDeleteThreadItemTitle:theItem];
		return self.doc ? YES : NO;
	}
	
	// 移動
	if (action_ == @selector(scrollFirstMessage:))
		return [self canScrollFirstMessage];
	if (action_ == @selector(scrollLastMessage:))
		return [self canScrollLastMessage];
	if (action_ == @selector(scrollPrevMessage:))
		return [self canScrollPrevMessage];
	if (action_ == @selector(scrollNextMessage:))
		return [self canScrollNextMessage];
	if (action_ == @selector(scrollToLastReadedIndex:)) 
		return [self canScrollToLastReadedMessage];
	if (action_ == @selector(scrollToLastUpdatedIndex:)) 
		return [self canScrollToLastUpdatedMessage];
	// ブックマークに移動
	if (action_ == @selector(scrollPreviousBookmark:)) 
		return ([self previousBookmarkIndex] != NSNotFound);
	if (action_ == @selector(scrollNextBookmark:)) 
		return ([self nextBookmarkIndex] != NSNotFound);
	if (action_ == @selector(scrollToFirstTodayMessage:))
		return [self canScrollToMessage]; // とりあえず
	if (action_ == @selector(scrollToLatest50FirstIndex:))
		return [self canScrollToMessage]; // とりあえず
	if (action_ == @selector(showIndexPanel:))
		return [self canScrollToMessage]; // とりあえず
	
	// 検索と文字の拡大／縮小
	if (action_ == @selector(showStandardFindPanel:) ||
        action_ == @selector(findNextText:) ||
	    action_ == @selector(findPreviousText:) ||
	    action_ == @selector(findFirstText:) ||
	    action_ == @selector(findAll:) ||
	    action_ == @selector(findAllByFilter:) ||
	    action_ == @selector(biggerText:) ||
	    action_ == @selector(smallerText:) ||
	    action_ == @selector(runSpamFilter:) ||
	    action_ == @selector(runAsciiArtDetector:) ||
	    action_ == @selector(scaleSegmentedControlPushed:)) { // For Segmented Control
        return [[self.documentView textStorage] length];
    }
	
    if (action_ == @selector(actualSizeText:)) {
        if ([[self.documentView textStorage] length]) {
            return (self.scaleCount != 0);
        }
        return NO;
    }
	
	// 履歴：戻る／進む
	if (action_ == @selector(historyMenuPerformForward:)) {
		return [self.histories canForward];
	}
	
	if (action_ == @selector(historyMenuPerformBack:)) {
		return [self.histories canBack];
	}
	
	// 選択テキストのコビー、抽出
//	if (action_ == @selector(copySelectedResURL:) || action_ == @selector(extractUsingSelectedText:)) {
//        return ([[self textView] selectedRange].length != 0);
//    }
//	if (action_ == @selector(copyURL:) || action_ == @selector(copyThreadAttributes:) || action_ == @selector(shareThreadInfo:)) {
//        return ([self selectedThreads] && [self numberOfSelectedThreads]);
//    }
	
//	if (action_ == @selector(reloadThread:)) {
//		return ([self threadAttributes] && ![(CMRThreadDocument *)[self document] isDatOchiThread]);
//    }
	
    if (action_ == @selector(retrieveThread:)) {
        if (!self.doc.threadAttr) {
            return NO;
        } else {
            if (![self.doc.threadAttr isDatOchiThread]) {
                return YES;
            } else {
                return [CMRPref shouldLoginIfNeeded];
            }
        }
    }
	
    // 掲示板関連アクション
//    if (action_ == @selector(showLocalRules:)) {
//        [(NSMenuItem *)theItem setTitle:NSLocalizedString(@"Show Local Rules", @"")];
//        return [self validateBBSActionItems];
//    }
    if (action_ == @selector(showBoardInspectorPanel:)) {
//        [self validateShowBoardInspectorPanelItemTitle:theItem]; // メニュータイトルの変更のために
//        return [self validateBBSActionItems];
		return self.doc.boardName ? YES : NO;
    }
//    if (action_ == @selector(openBBSInBrowser:)) {
//        [(NSMenuItem *)theItem setTitle:NSLocalizedString(@"Open BBS in Browser", @"")];
//        return [self validateBBSActionItems];
//    }
	
//	return [super validateUserInterfaceItem:theItem];
	return NO;
}
@end

#import "BSDateFormatter.h"
#import "BSIndexPanelController.h"
@implementation KMBSLogViewController (MoveAction)
/* 最初／最後のレス */
- (IBAction)scrollFirstMessage:(id)sender
{
	[self scrollMessageAtIndex:0];
}

- (IBAction)scrollLastMessage:(id)sender
{
	[self scrollMessageAtIndex:self.numberOfMessage -1];
}

/* 次／前のレス */
- (IBAction)scrollPreviousMessage:(id)sender
{
	[self scrollPrevMessage:sender];
}

- (IBAction)scrollPrevMessage:(id)sender
{
	[self scrollMessageAtIndex:[self previousVisibleMessageIndex]];
}

- (IBAction)scrollNextMessage:(id)sender
{
	[self scrollMessageAtIndex:[self nextVisibleMessageIndex]];
}

/* 次／前のブックマーク */
- (IBAction)scrollPreviousBookmark:(id)sender
{
	[self scrollMessageAtIndex:[self previousBookmarkIndex]];
}

- (IBAction)scrollNextBookmark:(id)sender
{
	[self scrollMessageAtIndex:[self nextBookmarkIndex]];
}

/* その他 */
- (IBAction)scrollToLastReadedIndex:(id)sender
{
    [self scrollMessageAtIndex:[self.doc.threadAttr lastIndex]];
}

//- (IBAction)scrollToLastUpdatedIndex:(id)sender
//{
//	[[self threadLayout] scrollToLastUpdatedIndex:sender];
//}

// From CMRThreadLayout
- (IBAction)scrollToLastUpdatedIndex:(id)sender
{
	[self scrollMessageWithRange:[self firstLastUpdatedHeaderAttachmentRange]];
}

- (IBAction)scrollToFirstTodayMessage:(id)sender
{
	NSDate *aDate = [[BSDateFormatter sharedDateFormatter] baseDateOfToday];
	NSUInteger index_ = [self messageIndexOfLaterDate:aDate];
	if (index_ != NSNotFound) {
		[self scrollMessageAtIndex:index_];
	} else {
		NSBeep();
	}
}

- (IBAction)scrollToLatest50FirstIndex:(id)sender
{
    NSUInteger index;
    NSUInteger lastIndex = self.numberOfMessage;
    if (lastIndex < 50) {
        index = 0;
    } else {
        index = lastIndex - 50;
    }
    [self scrollMessageAtIndex:index];
}

- (IBAction)scrollPrevBookmarkOrFirst:(id)sender
{
    NSUInteger index = [self previousBookmarkIndex];
    if (index != NSNotFound) {
        [self scrollPreviousBookmark:sender];
    } else {
        [self scrollFirstMessage:sender];
    }
}

- (IBAction)scrollNextBookmarkOrLast:(id)sender
{
    NSUInteger index = [self nextBookmarkIndex];
    if (index != NSNotFound) {
        [self scrollNextBookmark:sender];
    } else {
        [self scrollLastMessage:sender];
    }
}

- (IBAction)showIndexPanel:(id)sender
{
    BSIndexPanelController *wc = [[BSIndexPanelController alloc] init];
    [wc beginSheetModalForThreadViewer:self];
}

- (IBAction)scrollFromNavigator:(id)sender
{
    switch ([sender selectedSegment]) {
        case 0:
            [self scrollPrevBookmarkOrFirst:sender];
            break;
        case 1:
            [self scrollPrevMessage:sender];
            break;
        case 2:
            [self scrollToLastUpdatedIndex:sender];
            break;
        case 3:
            [self scrollNextMessage:sender];
            break;
        case 4:
            [self scrollNextBookmarkOrLast:sender];
            break;
        default:
            break;
    }
}
@end

@implementation KMBSLogViewController (MoveActionValidation)
- (BOOL)canScrollToMessage
{
	return self.numberOfMessage != 0;
}

- (BOOL)canScrollFirstMessage
{
	if (![self canScrollToMessage]) {
        return NO;
    }
    NSInteger index_;
    NSInteger min_;
    index_ = [self firstMessageIndexForDocumentVisibleRect];
    if (index_ == NSNotFound) {
        index_ = 0;
    }
    index_++;
    min_ = (index_ == 0) ? 0 : 1;
	return (index_ != min_);
}

- (BOOL)canScrollLastMessage
{
	if (![self canScrollToMessage]) {
        return NO;
    }
    NSRect lastLine = [self.documentView frame];
	lastLine.origin.y = lastLine.size.height - 2;
	lastLine.size.height = 1;
	NSRect visible = [self.documentView visibleRect];
	return !NSIntersectsRect(visible, lastLine);
}

- (BOOL)canScrollPrevMessage
{
	return [self canScrollFirstMessage];
}

- (BOOL)canScrollNextMessage
{
	return [self canScrollLastMessage];
}

- (BOOL)canScrollToLastReadedMessage
{
	if (![self canScrollToMessage]) {
		return NO;
	}
	if (NSNotFound == [self.doc.threadAttr lastIndex]) {
		return NO;
	}
	return YES;
}

- (BOOL)canScrollToLastUpdatedMessage
{
	NSRange range_;
	
	if (![self canScrollToMessage]) {
        return NO;
	}
	range_ = [self firstLastUpdatedHeaderAttachmentRange];
	if (NSNotFound == range_.location) {
        return NO;
	}
	return YES;
}
@end

#include <objc/runtime.h>
@implementation KMBSLogViewController (ScalingText)
- (NSInteger)scaleCount
{
	return [objc_getAssociatedObject(self, @"scaleCount") integerValue];
}
- (void)setScaleCount:(NSInteger)scaleCount
{
	objc_setAssociatedObject(self, @"scaleCount", [NSNumber numberWithInteger:scaleCount], OBJC_ASSOCIATION_RETAIN);
}
#pragma mark Scaling Text View
- (void)scaleTextView:(float)rate
{
	NSClipView *clipView_ = [self.documentScrollView contentView];
	NSTextView *textView_ = self.documentView;
	
	NSUInteger curIndex = [[self threadLayout] firstMessageIndexForDocumentVisibleRect];
	
	NSSize	curBoundsSize = [clipView_ bounds].size;	
	NSSize	curFrameSize = [textView_ frame].size;
	
	[clipView_ setBoundsSize:NSMakeSize(curBoundsSize.width*rate, curBoundsSize.height*rate)];
	[textView_ setFrameSize:NSMakeSize(curFrameSize.width*rate, curFrameSize.height*rate)];
	
	[clipView_ setNeedsDisplay:YES]; // really need?
	
	[clipView_ setCopiesOnScroll:NO]; // これがキモ
	[[self threadLayout] scrollMessageAtIndex:curIndex]; // スクロール位置補正
	
	// テキストビューやクリップビューだけ再描画させても良さそうだが、
	// 時々ツールバーとの境界線が消えてしまうことがあるので、ウインドウごと再描画させる
	[[self window] display]; 
	[clipView_ setCopiesOnScroll:YES];
}

- (IBAction)biggerText:(id)sender
{
    self.scaleCount++;
	[self scaleTextView:0.8];
}

- (IBAction)smallerText:(id)sender
{
    self.scaleCount--;
	[self scaleTextView:1.25];
}

- (IBAction)actualSizeText:(id)sender
{
    float rate = (self.scaleCount > 0) ? 1.25 : 0.8;
    int hoge = abs(self.scaleCount);
    [self scaleTextView:(powf(rate, hoge))];
    self.scaleCount = 0;
}

- (IBAction)scaleSegmentedControlPushed:(id)sender
{
	NSInteger	i;
	i = [sender selectedSegment];
	
	if (i == -1) {
		NSLog(@"No selection?");
	} else if (i == 1) {
		[self biggerText:nil];
	} else {
		[self smallerText:nil];
	}
}
@end

