#include "pch.h"
#include "objc_utils.h"
#include "log.h"
#include "app.h"
#include <atomic>
#include <iomanip>
#include <ctime>
#include <sstream>

#ifdef __OBJC__
#import <Photos/Photos.h>
@implementation PathWithModDate
@end
@implementation ObjcUtils
+ (NSArray*)getFilesAtPathSortedByModificationDate:(NSString*)folderPath
{
    NSArray *allPaths = [NSFileManager.defaultManager contentsOfDirectoryAtPath:folderPath error:nil];

    NSMutableArray *sortedPaths = [NSMutableArray new];
    for (NSString *path in allPaths) {
        NSString *fullPath = [folderPath stringByAppendingPathComponent:path];

        NSDictionary *attr = [NSFileManager.defaultManager attributesOfItemAtPath:fullPath error:nil];
        NSDate *modDate = [attr objectForKey:NSFileModificationDate];

        PathWithModDate *pathWithDate = [[PathWithModDate alloc] init];
        pathWithDate.path = fullPath;
        pathWithDate.modDate = modDate;
        [sortedPaths addObject:pathWithDate];
    }

    [sortedPaths sortUsingComparator:^(PathWithModDate *path1, PathWithModDate *path2) {
        // Descending (most recently modified first)
        return [path2.modDate compare:path1.modDate];
    }];

    return sortedPaths;
}
@end
#endif

std::wstring NSStringToStringW ( NSString* Str )
{
    NSStringEncoding pEncode    =   CFStringConvertEncodingToNSStringEncoding ( kCFStringEncodingUTF32LE );
    NSData* pSData              =   [ Str dataUsingEncoding : pEncode ];
   
    return std::wstring ( (wchar_t*) [ pSData bytes ], [ pSData length] / sizeof ( wchar_t ) );
}

NSString* StringWToNSString ( const std::wstring& Str )
{
    NSString* pString = [ [ NSString alloc ]
                        initWithBytes : (char*)Str.data()
                               length : Str.size() * sizeof(wchar_t)
                             encoding : CFStringConvertEncodingToNSStringEncoding ( kCFStringEncodingUTF32LE ) ];
    return pString;
}

std::string base_address;
std::atomic_flag handler_executed;

void save_recovery()
{
    auto t = std::time(nullptr);
    auto tm = *std::localtime(&t);
    std::ostringstream oss;
    oss << std::put_time(&tm, "%d-%m-%Y %H-%M-%S");
    auto path = App::I->data_path + "/" + App::I->doc_name + "-recovery (" + oss.str() + ").ppi";

    if (App::I->canvas && App::I->canvas->m_canvas)
        App::I->canvas->m_canvas->project_save_thread(path, false);
}

void exception_handler(NSException *exception)
{
    if (handler_executed.test_and_set())
        exit(0);
    LOG("exception %s\n", [[exception name] cStringUsingEncoding:NSUTF8StringEncoding]);
    NSString* callstack = [[NSThread callStackSymbols] componentsJoinedByString:@"\n"];
    LOG("base:\n%s\ncallstack:\n%s", base_address.c_str(), [callstack cStringUsingEncoding:NSUTF8StringEncoding]);
    save_recovery();
    uinstall_global_handlers();
    //exit(0);
}

static void signal_handler (int signo) {
    if (handler_executed.test_and_set())
        exit(0);
    LOG("signal %d\n", signo);
    NSString* callstack = [[NSThread callStackSymbols] componentsJoinedByString:@"\n"];
    LOG("base:\n%s\ncallstack:\n%s", base_address.c_str(), [callstack cStringUsingEncoding:NSUTF8StringEncoding]);
    save_recovery();
    uinstall_global_handlers();
    //exit(0);
}

void SignalHandler(int sig, siginfo_t *info, void *context)
{
    save_recovery();
    //exit(0);
}

void TerminateHandler()
{
    save_recovery();
    //exit(0);
}

void uinstall_global_handlers()
{
    signal(SIGABRT, SIG_DFL);
    signal(SIGILL, SIG_DFL);
    signal(SIGSEGV, SIG_DFL);
    signal(SIGFPE, SIG_DFL);
    signal(SIGBUS, SIG_DFL);
    signal(SIGPIPE, SIG_DFL);
}

void install_global_handlers()
{
    NSString* callstack = [[NSThread callStackSymbols] componentsJoinedByString:@"\n"];
    base_address = [callstack cStringUsingEncoding:NSUTF8StringEncoding];
    NSSetUncaughtExceptionHandler(exception_handler);
    signal(SIGABRT, signal_handler);
    signal(SIGILL, signal_handler);
    signal(SIGSEGV, signal_handler);
    signal(SIGFPE, signal_handler);
    signal(SIGBUS, signal_handler);
    signal(SIGPIPE, signal_handler);
    
    signal(SIGQUIT, signal_handler);
    signal(SIGTRAP, signal_handler);
    signal(SIGEMT, signal_handler);
    signal(SIGSYS, signal_handler);
    signal(SIGALRM, signal_handler);
    signal(SIGXCPU, signal_handler);
    signal(SIGXFSZ, signal_handler);

/*
    struct sigaction mySigAction;
    mySigAction.sa_sigaction = SignalHandler;
    mySigAction.sa_flags = SA_SIGINFO;
    
    sigemptyset(&mySigAction.sa_mask);
    sigaction(SIGQUIT, &mySigAction, NULL);
    sigaction(SIGILL, &mySigAction, NULL);
    sigaction(SIGTRAP, &mySigAction, NULL);
    sigaction(SIGABRT, &mySigAction, NULL);
    sigaction(SIGEMT, &mySigAction, NULL);
    sigaction(SIGFPE, &mySigAction, NULL);
    sigaction(SIGBUS, &mySigAction, NULL);
    sigaction(SIGSEGV, &mySigAction, NULL);
    sigaction(SIGSYS, &mySigAction, NULL);
    sigaction(SIGPIPE, &mySigAction, NULL);
    sigaction(SIGALRM, &mySigAction, NULL);
    sigaction(SIGXCPU, &mySigAction, NULL);
    sigaction(SIGXFSZ, &mySigAction, NULL);
    
    std::set_terminate(TerminateHandler);
*/
}

void delete_all_files_in_path(const std::string& source_path)
{
    NSString *path = [NSString stringWithUTF8String:source_path.c_str()];
    NSDirectoryEnumerator* en = [[NSFileManager defaultManager] enumeratorAtPath:path];
    LOG("delete all files from %s", source_path.c_str());
    while (NSString* file = [en nextObject])
    {
        NSString* file_path = [path stringByAppendingPathComponent:file];
        [[NSFileManager defaultManager] removeItemAtPath:file_path error:nil];
        LOG("delete: %s", [file_path UTF8String]);
    }
}

static void stillImageDataReleaseCallback(void *releaseRefCon, const void *baseAddress)
{
    free((void *)baseAddress);
}

void export_mp4(const std::string& rec_path, int width, int height, int tot, void(^progress_callback)(float))
{
    int progress = 0;
    NSString* mov_path = [NSString stringWithFormat:@"%s/out.mp4", rec_path.c_str()];
    if ([[NSFileManager defaultManager] fileExistsAtPath:mov_path])
    {
        NSLog(@"remove existing mp4");
        [[NSFileManager defaultManager] removeItemAtPath:mov_path error:nil];
    }
    NSURL* url = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%s/out.mp4", rec_path.c_str()]];
    AVAssetWriter* writer = [AVAssetWriter assetWriterWithURL:url fileType:AVFileTypeMPEG4 error:nil];
    writer.shouldOptimizeForNetworkUse = NO;
    NSDictionary *videoCompressionSettings = @{
        AVVideoCodecKey                  : AVVideoCodecH264,
        AVVideoWidthKey                  : @(width),
        AVVideoHeightKey                 : @(height),
        AVVideoCompressionPropertiesKey  : @{ AVVideoAverageBitRateKey : @(8<<20) }
    };
    if (![writer canApplyOutputSettings:videoCompressionSettings forMediaType:AVMediaTypeVideo])
    {
        NSLog(@"Couldn't add asset writer video input.");
        return;
    }
    AVAssetWriterInput* input = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo
                                                              outputSettings:videoCompressionSettings
                                                            sourceFormatHint:nil];
    input.expectsMediaDataInRealTime = YES;
  
    NSDictionary *adaptorDict = @{
                                (id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA),
                                (id)kCVPixelBufferWidthKey : @(width),
                                (id)kCVPixelBufferHeightKey : @(height)
                                };

    AVAssetWriterInputPixelBufferAdaptor* _pixelBufferAdaptor = [[AVAssetWriterInputPixelBufferAdaptor alloc]
                         initWithAssetWriterInput:input
                         sourcePixelBufferAttributes:adaptorDict];


    // Add asset writer input to asset writer
    if (![writer canAddInput:input]) {
        NSLog(@"Couldn't add input to writer.");
        return;
    }

    [writer addInput:input];

    CMTime t;
    t.timescale = 30;
    t.flags = kCMTimeFlags_Valid;
    t.epoch = 0;
    t.value = 0;
    //[writer startSessionAtSourceTime:t];
    
    CVPixelBufferRef buff = NULL;
    uint8_t* data = (uint8_t*)calloc(1, width * height * 4);
    CVPixelBufferCreateWithBytes(kCFAllocatorDefault, width, height, kCVPixelFormatType_32RGBA, data,
        width * 4, stillImageDataReleaseCallback, nil, nil, &buff);
    OSStatus err = CVPixelBufferPoolCreatePixelBuffer(nil, _pixelBufferAdaptor.pixelBufferPool, &buff);


    if (writer.status == AVAssetWriterStatusUnknown)
    {
        // If the asset writer status is unknown, implies writing hasn't started yet, hence start writing with start time as the buffer's presentation timestamp
        if ([writer startWriting])
        {
            [writer startSessionAtSourceTime:t];
        }
    }

    if (writer.status == AVAssetWriterStatusWriting)
    {
        for (int i = 0; i < tot; i++)
        {
            // If the asset writer status is writing, append sample buffer to its corresponding asset writer input
            if (input.readyForMoreMediaData)
            {
                char path[256];
                snprintf(path, sizeof(path), "%s/%04d.jpg", rec_path.c_str(), i);
                NSString* img_path = [NSString stringWithUTF8String:path];
                NSLog(@"frame: %@", img_path);
#if __OSX__
                NSImage *image = [[NSImage alloc] initWithContentsOfFile:img_path];
                if (!image)
                    break;
                NSRect imageRect = NSMakeRect(0, 0, image.size.width, image.size.height);
                CGImageRef cgImage = [image CGImageForProposedRect:&imageRect context:NULL hints:nil];
#elif __IOS__
                UIImage* image = [UIImage imageNamed:img_path];
                if (!image)
                    break;
                CGImageRef cgImage = image.CGImage;
#endif

                NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
                             [NSNumber numberWithBool:YES], kCVPixelBufferCGImageCompatibilityKey,
                             [NSNumber numberWithBool:YES], kCVPixelBufferCGBitmapContextCompatibilityKey, nil];
                CGSize sz = image.size;
                CVPixelBufferRef pxbuffer = NULL;
                {
                    CGImageRef image = cgImage;
                    CGSize size = sz;
                    CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault, size.width, size.height, kCVPixelFormatType_32ARGB, (__bridge CFDictionaryRef) options, &pxbuffer);
                    // CVReturn status = CVPixelBufferPoolCreatePixelBuffer(NULL, adaptor.pixelBufferPool, &pxbuffer);

                    //NSParameterAssert(status == kCVReturnSuccess && pxbuffer != NULL);

                    CVPixelBufferLockBaseAddress(pxbuffer, 0);
                    void *pxdata = CVPixelBufferGetBaseAddress(pxbuffer);
                    //NSParameterAssert(pxdata != NULL);

                    CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
                    CGContextRef context = CGBitmapContextCreate(pxdata, size.width, size.height, 8, 4*size.width, rgbColorSpace, kCGImageAlphaPremultipliedFirst);
                    //NSParameterAssert(context);

                    CGContextDrawImage(context, CGRectMake(0, 0, CGImageGetWidth(image), CGImageGetHeight(image)), image);

                    CGColorSpaceRelease(rgbColorSpace);
                    CGContextRelease(context);

                    CVPixelBufferUnlockBaseAddress(pxbuffer, 0);
                }

                t.value = i;
                if (![_pixelBufferAdaptor appendPixelBuffer:pxbuffer withPresentationTime:t])
                {
                    NSLog(@"error %@", [writer.error localizedFailureReason]);
                }
                CFRelease(pxbuffer);

                progress++;
                progress_callback((float)progress / tot * 100.f);
            }
        }
        [input markAsFinished];
        [writer finishWritingWithCompletionHandler:^{
            NSString* path = [NSString stringWithFormat:@"%s/out.mp4", rec_path.c_str()];
            NSLog(@"saved video %@", path);
#if __IOS__
            if (UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(path))
            {
                NSLog(@"saving to camera roll");
                UISaveVideoAtPathToSavedPhotosAlbum(path, nil, nil, nil);
            }
#endif
        }];
    }

    if (writer.status == AVAssetWriterStatusFailed)
    {
        NSLog(@"failed");
    }
    
}

void save_image_library(const std::string& path)
{
#if __IOS__
    [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
        NSURL* url = [NSURL fileURLWithPath:[NSString stringWithUTF8String:path.c_str()]];
        PHAssetChangeRequest *changeRequest = [PHAssetChangeRequest creationRequestForAssetFromImageAtFileURL:url];
        changeRequest.creationDate          = [NSDate date];
    } completionHandler:^(BOOL success, NSError *error) {
        if (success) {
            NSLog(@"successfully saved");
        }
        else {
            NSLog(@"error saving to photos: %@", error);
        }
    }];
#endif
}

bool apple_create_dir(const std::string& path)
{
    NSError* err = nil;
    NSString* p = [NSString stringWithUTF8String:(path+"/").c_str()];
    NSURL* url = [NSURL fileURLWithPath:p isDirectory:YES];
    if (![[NSFileManager defaultManager] createDirectoryAtURL:url withIntermediateDirectories:YES attributes:nil error:&err])
    {
        LOG("error creating path: %s", path.c_str());
        LOG("error creating path: %s", [[err description] cStringUsingEncoding:NSUTF8StringEncoding]);
        return false;
    }
    return true;
}
