//
//  ECScene.m
//  SceneRenderProto
//
//  Created by 二鏡 on 11/11/14.
//  Copyright 2011年 二鏡庵. All rights reserved.
//

#import "ECScene.h"
#import "UTIs.h"
#import "ServiceFunctions.h"
#import "ECRegularLayer.h"
#import "ECCompositionLayer.h"

NSString *ECPasteboardTypeScene = @"com.mac.nikyo-an.pasteboard.scene";

static NSString *plistLayersKey = @"LayersPlist";
static NSString *plistSizeKey = @"Size";
static NSString *plistColorKey = @"Color";

static NSString *oLayerValueChangeContext = @"layerValueChanged";

@interface ECScene ()
- (void)startObserving:(ECLayer*)aLayer;
- (void)stopObserving:(ECLayer*)aLayer;
@end

NSString *gSceneFileExtension = @"ecscene";
/*
 * Scene File(Package)
 *
 * scene.ecscene/
 *    resources/       # レイヤの降順にオリジナルへのハードリンクを保持
 *       1.jpg      # top
 *       2.png      # ...
 *       ....       # base
 *
 *    info.plist    # scene自体と各レイヤの内容を持つplist
 *    thumbnail.jpg # sceneのサムネイル
 */

@implementation ECScene
@synthesize size,lightPreview, undo;

+ (void)initialize
{
    [ECLayer registerClass: [ECRegularLayer class]];
    [ECLayer registerClass: [ECCompositionLayer class]];
}

+ (NSSet*)keyPathsForValuesAffectingValueForKey:(NSString*)key
{
    if([key isEqualToString: @"update"])
        return [NSSet setWithObjects: @"layers", @"backgroundColor", nil];
    return [super keyPathsForValuesAffectingValueForKey: key];
}

+ (NSString*)pathComponent:(NSString*)body
{
    return [NSString stringWithFormat: @"%@.ecscene", body];
}

+ (NSString*)templateName0
{
    return NSLocalizedString(@"New Template.ecscene",@"");
}

+ (NSString*)templateNameWithCount:(NSUInteger)val
{
    return [NSString stringWithFormat: NSLocalizedString(@"New Template %lu.ecscene",@""), val];
}

- (id)initWithSize:(NSSize)aSize
{
    self = [super init];
    layers = [[NSMutableArray alloc] init];
    size = aSize;
    backgroundColor = CGColorCreateGenericRGB(0, 0, 0, 1.0);
    return self;
}

- (id)initWithContentsOfURL:(NSURL*)aURL
{
    self = [super init];
    // UTI test
    id ws = [NSWorkspace sharedWorkspace];
    id UTI = [ws typeOfFile: [aURL path] error: nil];
    if(UTI == nil)
        return nil;
    if(UTTypeConformsTo((CFStringRef)UTI, utiSceneFile) == NO)
        return nil;
    
    id plistURL = [aURL URLByAppendingPathComponent: @"info.plist"];
    id plist = [NSDictionary dictionaryWithContentsOfURL: plistURL];
    
    // scene level 
    backgroundColor = _cgcolorRGBFromData([plist objectForKey: plistColorKey]);
    size = [[NSUnarchiver unarchiveObjectWithData:
             [plist objectForKey: plistSizeKey]] sizeValue];
    
    // layer level
    layers = [[NSMutableArray alloc] init];
    NSArray *layerPlists = [plist objectForKey: plistLayersKey];
    for(id aPlist in layerPlists)
    {
        // parse each layer info.
        ECLayer *layer = [ECLayer layerWithPropertyList: aPlist
                                       packageURL: aURL];
        layer.scene = self;
        if(layer)
            [self addLayersObject: layer];
    }
    
    return self;
}

- (void)dealloc
{
    for(id layer in layers)
        [self stopObserving: layer];
    [layers release];
    CGColorRelease(backgroundColor);
    [super dealloc];
}

+ (NSSize)sizeOfContentsOfURL:(NSURL*)aURL
{
    NSSize size;
    @try {
        id plistURL = [aURL URLByAppendingPathComponent: @"info.plist"];
        id plist = [NSDictionary dictionaryWithContentsOfURL: plistURL];
        size = [[NSUnarchiver unarchiveObjectWithData:
                 [plist objectForKey: plistSizeKey]] sizeValue];
    }
    @catch(id ex)
    {
        return NSZeroSize;
    }
    return size;
}

#pragma mark NSCopying
- (id)copyWithZone:(NSZone *)zone
{
    ECScene *ret = [[ECScene allocWithZone:zone] init];
    ret->size = size;
    ret->backgroundColor = CGColorRetain(backgroundColor);
    
    // layerはdeep copyする
    ret->layers = [[NSMutableArray allocWithZone: zone] initWithArray: layers
                                                            copyItems: YES];
    [ret->layers setValue: ret forKey: @"scene"];
    for(id layer in ret->layers)
    {
        [ret startObserving: layer];
    }
    
    return ret;
}

#pragma mark NSCoding
- (id)initWithCoder:(NSCoder *)aDecoder
{
    if(self)
    {
        layers = [[aDecoder decodeObjectForKey: plistLayersKey] mutableCopy];
        backgroundColor = _cgcolorRGBFromData([aDecoder decodeObjectForKey: plistColorKey]);
        size = [aDecoder decodeSizeForKey: plistSizeKey];
        
        for(id layer in layers)
        {
            [layer setScene: self];
            [self startObserving: layer];
        }
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder
{
    [aCoder encodeObject: layers forKey: plistLayersKey];
    [aCoder encodeObject: _cgcolorRGBToData(backgroundColor) forKey:plistColorKey];
    [aCoder encodeSize: size forKey: plistSizeKey];
}

#pragma mark Animation
- (BOOL)isStatic
{
    // 全レイヤーがstaticなシーンはstaticである
    // 今のところシーンレベルのポストエフェクトはない
    for(ECLayer *layer in layers)
        if(layer.isStatic == NO)
            return NO;
    return YES;
}

-(NSArray*)writableTypesForPasteboard:(NSPasteboard*)pboard
{
    return [NSArray arrayWithObject: ECPasteboardTypeScene];
}

-(id)pasteboardPropertyListForType:(NSString*)type
{
    if([type isEqualToString: ECPasteboardTypeScene])
        return [NSKeyedArchiver archivedDataWithRootObject: self];
    return nil;
}

+ (NSArray *)readableTypesForPasteboard:(NSPasteboard *)pasteboard
{
    static NSArray *types = nil;
    if(!types)
        types = [[NSArray alloc] initWithObjects: ECPasteboardTypeScene,nil];
    return types;
}

+ (NSPasteboardReadingOptions)readingOptionsForType:(NSString *)type 
                                         pasteboard:(NSPasteboard *)pboard
{
    if([type isEqualToString: ECPasteboardTypeScene])
        return NSPasteboardReadingAsKeyedArchive;
    return 0;
}
#pragma mark Color
- (NSColor*)backgroundColor
{
    const CGFloat *rgba = CGColorGetComponents(backgroundColor);
    return [NSColor colorWithCalibratedRed:rgba[0] green:rgba[1] blue:rgba[2] alpha: 1.0]; // alphaは許容しない
}

- (void)setBackgroundColor:(NSColor*)aColor
{
    if(aColor == nil)
        return;
    
    [undo registerUndoWithTarget: self
                        selector: @selector(setBackgroundColor:)
                          object: [self backgroundColor]];
    
    CGFloat rgba[4];
    id color = [aColor colorUsingColorSpaceName: NSCalibratedRGBColorSpace];
    [color getRed: &rgba[0] green: &rgba[1] blue: &rgba[2] alpha: &rgba[3]];
    CGColorRelease(backgroundColor);
    backgroundColor = CGColorCreateGenericRGB(rgba[0], rgba[1], rgba[2], 1.0);
}

#pragma mark Undo Observing

- (void)setUndo:(NSUndoManager*)undoManager
{
    if(undo == undoManager)
        return;
    
    id center = [NSNotificationCenter defaultCenter];
    if(undo)
        [center removeObserver: self
                          name: NSUndoManagerCheckpointNotification
                        object: undo];
    
    undo = undoManager;
    if(undo)
        [center addObserver: self
                   selector: @selector(undoManagerCheckpoint:)
                       name: NSUndoManagerCheckpointNotification
                     object: undo];
}

- (void)startObserving:(ECLayer*)aLayer
{
    id keys = [[aLayer class] undoObservationKeys];
    for(id key in keys)
    {
        [aLayer addObserver: self 
                 forKeyPath: key
                    options: (NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)
                    context: oLayerValueChangeContext];
    }
}

- (void)stopObserving:(ECLayer*)aLayer
{
    id keys = [[aLayer class] undoObservationKeys];
    for(id key in keys)
    {
        [aLayer removeObserver: self
                    forKeyPath: key];
    }
}

- (void)observeValueForKeyPath:(NSString *)keyPath 
                      ofObject:(id)object 
                        change:(NSDictionary *)change 
                       context:(void *)context {
    if (context == oLayerValueChangeContext)
    {
        if(undo == nil || [undo isUndoRegistrationEnabled] == NO)
            return;
        
        [self willChangeValueForKey: @"update"];
        id newVal = [change objectForKey: NSKeyValueChangeNewKey];
        id oldVal = [change objectForKey: NSKeyValueChangeOldKey];
        
        if([newVal isEqual: oldVal] == NO)
        {
            if(currentUndoGroupMapping == nil)
            {
                currentUndoGroupMapping = [[NSMapTable mapTableWithStrongToStrongObjects] retain];
                [undo registerUndoWithTarget: self selector: @selector(applyChanges:) object: currentUndoGroupMapping];
            }
            
            id undoDic = [currentUndoGroupMapping objectForKey: object];
            if(undoDic == nil)
            {
                undoDic = [[NSMutableDictionary alloc] init];
                [currentUndoGroupMapping setObject: undoDic forKey: object];
                [undoDic release];
            }
            [undoDic setObject: oldVal forKey: keyPath];
        }
        
        [self didChangeValueForKey: @"update"];
        return ;
    }
    
    [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}

- (void)undoManagerCheckpoint:(id)notif
{
    [currentUndoGroupMapping release];
    currentUndoGroupMapping = nil;
}

- (void)applyChanges:(NSMapTable*)changeMapping
{
    for(ECLayer *layer in changeMapping)
    {
        id params = [changeMapping objectForKey: layer];
        if(params)
        {
            [layer setValuesForKeysWithDictionary: params];
        }
    }
}
#pragma mark Layers
- (NSArray*)layers
{
    return [[layers copy] autorelease];
}

- (NSUInteger)countOfLayers
{
    return [layers count];
}

- (id)objectInLayersAtIndex:(NSUInteger)i
{
    return [layers objectAtIndex: i];
}

- (void)insertObject:(ECLayer*)aLayer
     inLayersAtIndex:(NSUInteger)i
{
    [[undo prepareWithInvocationTarget: self]
     removeObjectFromLayersAtIndex: i];
    [aLayer setScene: self];
    [self startObserving: aLayer];
    [layers insertObject: aLayer 
                 atIndex: i];
}

- (void)removeObjectFromLayersAtIndex:(NSUInteger)i
{
    id layer = [layers objectAtIndex: i];
    [[undo prepareWithInvocationTarget: self]
     insertObject: layer inLayersAtIndex:i];
    [self stopObserving: layer];
    [layer cleanupRendering];
    [layer setScene: nil];
    [layers removeObjectAtIndex: i];
}

- (void)exchangeLayerObjectAtIndex:(NSUInteger)i
                 withObjectAtIndex:(NSUInteger)j
{
    [self willChangeValueForKey: @"layers"];
    [[undo prepareWithInvocationTarget: self]
     exchangeLayerObjectAtIndex: i withObjectAtIndex: j];
    [layers exchangeObjectAtIndex: i
                withObjectAtIndex: j];
    [self didChangeValueForKey: @"layers"];
}

- (void)addLayersObject:(ECLayer*)aLayer
{
    [aLayer setScene: self];
    [self insertObject: aLayer inLayersAtIndex: [layers count]];
}

- (void)removeLayersObject:(ECLayer*)aLayer
{
    NSUInteger idx = [layers indexOfObject: aLayer];
    if(idx != NSNotFound)
        [self removeObjectFromLayersAtIndex: idx];
}

#pragma mark Support
- (CGSize)recommendSizeForImageSize:(CGSize)imgSize
{
    CGSize ret;
    if(imgSize.width < size.width)
    {
        if(imgSize.height < size.height)
        { // 小さいのでそのまま
            ret = imgSize;
        }
        else
        { // 縦に合わせる
            CGFloat w = floor(imgSize.width/imgSize.height*size.height);
            CGFloat h = size.height;
            ret = CGSizeMake(w,h);
        }
    }
    else
    {
        if(imgSize.height < size.height)
        { // 横に合わせる
            CGFloat w = size.width;
            CGFloat h = floor(imgSize.height/imgSize.width*size.width);
            ret = CGSizeMake(w,h);
        }
        else
        {
            CGFloat ratioW = imgSize.width/size.width;
            CGFloat ratioH = imgSize.height/size.height;
            if(ratioW > ratioH)
            { // 横に合わせる
                CGFloat w = size.width;
                CGFloat h = floor(imgSize.height/imgSize.width*size.width);
                ret = CGSizeMake(w,h);
            }
            else
            { // 縦に合わせる
                CGFloat w = floor(imgSize.width/imgSize.height*size.height);
                CGFloat h = size.height;
                ret = CGSizeMake(w,h);
            }
        }
    }
    return ret;
}

- (ECLayer*)layerAtPoint:(NSPoint)aPoint
{
    CGPoint p = NSPointToCGPoint(aPoint);    
    id iter = [layers objectEnumerator];
    for(ECLayer *layer in iter)
    {
        if(layer.hide)
            continue;
        if([layer pointInRect: p])
            return layer;
    }
    
    return nil;
}

- (NSString*)nextLayerName
{
    id format = NSLocalizedString(@"Layer %ld",@"");
    return [NSString stringWithFormat: format, [layers count]+1];
}

#pragma mark -
- (BOOL)update
{
    return YES;
}

- (void)drawSceneWithContext:(CGContextRef)aContext
{
    CGContextSetFillColorWithColor(aContext, backgroundColor);
    NSRect rect;
    rect.size = size;
    rect.origin = NSZeroPoint;
    CGContextFillRect(aContext, rect);
    
    id iter = [layers reverseObjectEnumerator];
    for(ECLayer *layer in iter)
    {
        if(layer.hide)
            continue;
        [layer renderInContext: aContext];
    }
}

- (CGImageRef)allocImageAtTime:(NSUInteger)msec
{
    CGColorSpaceRef cs = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB);
    
    size_t bpr = size.width*4;
    CGBitmapInfo info = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrderDefault;
    CGContextRef context = CGBitmapContextCreate(NULL, size.width, size.height, 8, bpr, cs, info);

    // compositing
    {
        CGContextSetFillColorWithColor(context, backgroundColor);
        NSRect rect;
        rect.size = size;
        rect.origin = NSZeroPoint;
        CGContextFillRect(context, rect);
        
        id iter = [layers reverseObjectEnumerator];
        for(ECLayer *layer in iter)
        {
            if(layer.hide)
                continue;
            if(lightPreview || layer.isStatic)
                [layer renderInContext: context];
            else
            {
                [layer renderAtTime: msec inContext: context];
            }
        }
    }
    CGImageRef ret = CGBitmapContextCreateImage(context);
    
    CGContextRelease(context);
    CFRelease(cs);
    
    return ret;
}

- (CGImageRef)allocSnapshot:(CGFloat)ratio
{
    CGColorSpaceRef cs = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB);
    size_t width = floor(size.width*ratio);
    size_t height = floor(size.height*ratio);
    size_t bpr = width*4;
    CGBitmapInfo info = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrderDefault;
    CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, bpr, cs, info);

    CGContextScaleCTM(context,ratio,ratio);
    CGContextSetAllowsAntialiasing(context, true);
    CGContextSetFillColorWithColor(context, backgroundColor);

    NSRect rect;
    rect.size = size;
    rect.origin = NSZeroPoint;
    CGContextFillRect(context, rect);
    
    id iter = [layers reverseObjectEnumerator];
    for(ECLayer *layer in iter)
    {
        if(layer.hide)
            continue;
        [layer renderThumbnailInContext: context];
    }
    
    CGImageRef ret = CGBitmapContextCreateImage(context);
    
    CGContextRelease(context);
    CFRelease(cs);
    
    return ret;
}

- (void)cleanupRendering
{
    for(ECLayer *layer in layers)
        [layer cleanupRendering];
}

- (void)_writeThumbnail:(NSURL*)aURL
{
    CGFloat base = MAX(size.width,size.height);
    CGFloat ratio = 256.0/base; // 長辺を256であわせる
    CGImageDestinationRef dst = 
    CGImageDestinationCreateWithURL((CFURLRef)aURL,kUTTypeJPEG, 1, nil);
    CGImageRef img = [self allocSnapshot: ratio];
    CFDictionaryRef prop = (CFDictionaryRef)[NSDictionary dictionaryWithObject: [NSNumber numberWithFloat: 0.85]
                                                                        forKey: (id)kCGImageDestinationLossyCompressionQuality];
    CGImageDestinationAddImage(dst, img, prop);
    CGImageDestinationFinalize(dst);
    CFRelease(dst);
    CFRelease(img);
}

- (BOOL)atomicWriteToURL:(NSURL*)tempPackage
{
    id fm = [NSFileManager defaultManager];

    // フォルダ構成
    id attr = [NSDictionary dictionaryWithObjectsAndKeys:
               [NSNumber numberWithBool: YES], NSFileExtensionHidden,
               nil];
    if([fm createDirectoryAtURL: tempPackage
    withIntermediateDirectories: YES
                     attributes: attr
                          error: nil] == NO)
        return NO;
    
    // image folder
    id resourceFolder = [tempPackage URLByAppendingPathComponent: @"resources"];
    if([fm createDirectoryAtURL: resourceFolder
    withIntermediateDirectories: YES
                     attributes: nil
                          error: nil] == NO)
        return NO;

    id layerPlists = [NSMutableArray array];
    int i = 0;
    // imageのハードリンクとプロパティリストをつくる
    for(ECLayer *layer in layers)
    {
        NSURL *auxURL = [layer auxResourceURL];
        id name = nil;
        if(auxURL)
        {
            id type = [auxURL pathExtension];
            name = [NSString stringWithFormat: @"%ld.%@",i++,type];
            id newURL = [resourceFolder URLByAppendingPathComponent: name];
            if([fm linkItemAtURL: auxURL
                           toURL: newURL
                           error: nil] == NO)
                return NO;
        }
        id plist = [layer propertyListWithAuxResourceName: name];
        [layerPlists addObject: plist];
    }
    
    // thumbnail
    id thumbURL = [tempPackage URLByAppendingPathComponent: @"thumbnail.jpg"];
    [self _writeThumbnail: thumbURL];

    // scene情報
    id colorData = _cgcolorRGBToData(backgroundColor);
    id sizeData = [NSArchiver archivedDataWithRootObject: [NSValue valueWithSize: size]];
    id dic = [NSDictionary dictionaryWithObjectsAndKeys:
              layerPlists, plistLayersKey,
              sizeData, plistSizeKey,
              colorData, plistColorKey,
              nil];
    
    id infoURL = [tempPackage URLByAppendingPathComponent: @"info.plist"];
    if([dic writeToURL: infoURL
            atomically: NO] == NO)
        return NO;
    
    return YES;
}

- (BOOL)writeToURL:(NSURL*)aURL
{
    id fm = [NSFileManager defaultManager];
    BOOL isDir, isExist;
    isExist = [fm fileExistsAtPath: [aURL path] isDirectory: &isDir];
    if(isExist != NO && isDir == NO)
    {
        // 既存の通常ファイルがあるので保存できない
        // フォルダの場合は拡張子テストを通過しているものと見なす
        return NO;
    }
    
    // prepare temporary folder
    id error;
    id tempFolder = [fm URLForDirectory: NSItemReplacementDirectory
                               inDomain: NSUserDomainMask
                      appropriateForURL: aURL
                                 create: YES
                                  error: &error];
    
    // 一時フォルダを取得できない
    if(tempFolder == nil)
        return NO;

    id tempPackage = [tempFolder URLByAppendingPathComponent: [aURL lastPathComponent]];
    if([self atomicWriteToURL: tempPackage] == NO)
    {
        // 一時ファイルの生成に失敗した
        [fm removeItemAtURL: tempFolder error: nil];
        return NO;
    }
    
    if(isExist)
        [fm removeItemAtURL: aURL error: nil];
    
    BOOL didMove = [fm moveItemAtURL: tempPackage
                               toURL: aURL
                               error: nil];
    [fm removeItemAtURL: tempFolder error: nil];
    return didMove;
}
@end


@implementation ECScene (RegularLayerSupport)
- (NSUInteger)longestTextLength
{
    NSUInteger maxLength = 0;
    for(ECRegularLayer *layer in layers)
    {
        if([layer isKindOfClass: [ECRegularLayer class]] == NO)
            continue;
        if(layer.countTextWait == NO)
            continue;
        id str = layer.string;
        maxLength = MAX(maxLength, [str length]);
    }
    return maxLength;
}

@end