/*
  Simple DirectMedia Layer
  Copyright (C) 2021 Valve Corporation

  This software is provided 'as-is', without any express or implied
  warranty.  In no event will the authors be held liable for any damages
  arising from the use of this software.

  Permission is granted to anyone to use this software for any purpose,
  including commercial applications, and to alter it and redistribute it
  freely, subject to the following restrictions:

  1. The origin of this software must not be misrepresented; you must not
     claim that you wrote the original software. If you use this software
     in a product, an acknowledgment in the product documentation would be
     appreciated but is not required.
  2. Altered source versions must be plainly marked as such, and must not be
     misrepresented as being the original software.
  3. This notice may not be removed or altered from any source distribution.
*/
#include "SDL_internal.h"

#ifdef SDL_VIDEO_CAPTURE

#include "SDL3/SDL.h"
#include "SDL3/SDL_video_capture.h"
#include "SDL_sysvideocapture.h"
#include "SDL_video_capture_c.h"
#include "../thread/SDL_systhread.h"

#if defined(HAVE_COREMEDIA) && defined(__MACOS__) && (__MAC_OS_X_VERSION_MAX_ALLOWED < 101500)
/* AVCaptureDeviceTypeBuiltInWideAngleCamera requires macOS SDK 10.15 */
#undef HAVE_COREMEDIA
#endif

#if TARGET_OS_TV
#undef HAVE_COREMEDIA
#endif

#ifndef HAVE_COREMEDIA
int InitDevice(SDL_VideoCaptureDevice *_this) {
    return -1;
}
int OpenDevice(SDL_VideoCaptureDevice *_this) {
    return -1;
}
int AcquireFrame(SDL_VideoCaptureDevice *_this, SDL_VideoCaptureFrame *frame) {
    return -1;
}
void CloseDevice(SDL_VideoCaptureDevice *_this) {
}
int GetDeviceName(SDL_VideoCaptureDeviceID instance_id, char *buf, int size) {
    return -1;
}
int GetDeviceSpec(SDL_VideoCaptureDevice *_this, SDL_VideoCaptureSpec *spec) {
    return -1;
}
int GetFormat(SDL_VideoCaptureDevice *_this, int index, Uint32 *format) {
    return -1;
}
int GetFrameSize(SDL_VideoCaptureDevice *_this, Uint32 format, int index, int *width, int *height) {
    return -1;
}
SDL_VideoCaptureDeviceID *GetVideoCaptureDevices(int *count) {
    return NULL;
}
int GetNumFormats(SDL_VideoCaptureDevice *_this) {
    return 0;
}
int GetNumFrameSizes(SDL_VideoCaptureDevice *_this, Uint32 format) {
    return 0;
}
int ReleaseFrame(SDL_VideoCaptureDevice *_this, SDL_VideoCaptureFrame *frame) {
    return 0;
}
int StartCapture(SDL_VideoCaptureDevice *_this) {
    return 0;
}
int StopCapture(SDL_VideoCaptureDevice *_this) {
    return 0;
}
int SDL_SYS_VideoCaptureInit(void) {
    return 0;
}
int SDL_SYS_VideoCaptureQuit(void) {
    return 0;
}


#else

#import <AVFoundation/AVFoundation.h>
#import <CoreMedia/CoreMedia.h>

/*
 * Need to link with:: CoreMedia CoreVideo
 *
 * Add in pInfo.list:
 *  <key>NSCameraUsageDescription</key> <string>Access camera</string>
 *
 *
 * MACOSX:
 * Add to the Code Sign Entitlement file:
 * <key>com.apple.security.device.camera</key> <true/>
 *
 *
 * IOS:
 *
 * - Need to link with:: CoreMedia CoreVideo
 * - Add #define SDL_VIDEO_CAPTURE 1
 *   to SDL_build_config_ios.h
 */

@class MySampleBufferDelegate;

struct SDL_PrivateVideoCaptureData
{
    dispatch_queue_t queue;
    MySampleBufferDelegate *delegate;
    AVCaptureSession *session;
    CMSimpleQueueRef frame_queue;
};

static NSString *
fourcc_to_nstring(Uint32 code)
{
    Uint8 buf[4];
    *(Uint32 *)buf = code;
    return [NSString stringWithFormat:@"%c%c%c%c", buf[3], buf[2], buf[1], buf[0]];
}

static NSArray<AVCaptureDevice *> *
discover_devices()
{
    NSArray *deviceType = @[AVCaptureDeviceTypeBuiltInWideAngleCamera];

    AVCaptureDeviceDiscoverySession *discoverySession = [AVCaptureDeviceDiscoverySession
                    discoverySessionWithDeviceTypes:deviceType
                    mediaType:AVMediaTypeVideo
                    position:AVCaptureDevicePositionUnspecified];

    NSArray<AVCaptureDevice *> *devices = discoverySession.devices;

    if ([devices count] > 0) {
        return devices;
    } else {
        AVCaptureDevice *captureDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
        if (captureDevice == nil) {
            return devices;
        } else {
            NSArray<AVCaptureDevice *> *default_device = @[ captureDevice ];
            return default_device;
        }
    }

    return devices;
}

static AVCaptureDevice *
get_device_by_name(const char *dev_name)
{
    NSArray<AVCaptureDevice *> *devices = discover_devices();

    for (AVCaptureDevice *device in devices) {
        char buf[1024];
        NSString *cameraID = [device localizedName];
        const char *str = [cameraID UTF8String];
        SDL_snprintf(buf, sizeof (buf) - 1, "%s", str);
        if (SDL_strcmp(buf, dev_name) == 0) {
            return device;
        }
    }
    return nil;
}

static Uint32
nsfourcc_to_sdlformat(NSString *nsfourcc)
{
  const char *str = [nsfourcc UTF8String];

  /* FIXME
   * on IOS this mode gives 2 planes, and it's NV12
   * on macos, 1 plane/ YVYU
   *
   */
#ifdef __MACOS__
  if (SDL_strcmp("420v", str) == 0)  return SDL_PIXELFORMAT_YVYU;
#else
  if (SDL_strcmp("420v", str) == 0)  return SDL_PIXELFORMAT_NV12;
#endif
  if (SDL_strcmp("yuvs", str) == 0)  return SDL_PIXELFORMAT_UYVY;
  if (SDL_strcmp("420f", str) == 0)  return SDL_PIXELFORMAT_UNKNOWN;

  SDL_Log("Unknown format '%s'", str);

  return SDL_PIXELFORMAT_UNKNOWN;
}

static NSString *
sdlformat_to_nsfourcc(Uint32 fmt)
{
  const char *str = "";
  NSString *result;

#ifdef __MACOS__
  if (fmt == SDL_PIXELFORMAT_YVYU)  str = "420v";
#else
  if (fmt == SDL_PIXELFORMAT_NV12)  str = "420v";
#endif
  if (fmt == SDL_PIXELFORMAT_UYVY)  str = "yuvs";

  result = [[NSString alloc] initWithUTF8String: str];

  return result;
}


@interface MySampleBufferDelegate : NSObject<AVCaptureVideoDataOutputSampleBufferDelegate>
    @property struct SDL_PrivateVideoCaptureData *hidden;
    - (void) set: (struct SDL_PrivateVideoCaptureData *) val;
@end

@implementation MySampleBufferDelegate

    - (void) set: (struct SDL_PrivateVideoCaptureData *) val {
        _hidden = val;
    }

    - (void) captureOutput:(AVCaptureOutput *)output
        didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
        fromConnection:(AVCaptureConnection *) connection {
            CFRetain(sampleBuffer);
            CMSimpleQueueEnqueue(_hidden->frame_queue, sampleBuffer);
        }

    - (void)captureOutput:(AVCaptureOutput *)output
        didDropSampleBuffer:(CMSampleBufferRef)sampleBuffer
        fromConnection:(AVCaptureConnection *)connection {
            SDL_Log("Drop frame..");
        }
@end

int
OpenDevice(SDL_VideoCaptureDevice *_this)
{
    _this->hidden = (struct SDL_PrivateVideoCaptureData *) SDL_calloc(1, sizeof (struct SDL_PrivateVideoCaptureData));
    if (_this->hidden == NULL) {
        SDL_OutOfMemory();
        goto error;
    }

    return 0;

error:
    return -1;
}

void
CloseDevice(SDL_VideoCaptureDevice *_this)
{
    if (!_this) {
        return;
    }

    if (_this->hidden) {
        AVCaptureSession *session = _this->hidden->session;

        if (session) {
            AVCaptureInput *input;
            AVCaptureVideoDataOutput *output;
            input = [session.inputs objectAtIndex:0];
            [session removeInput:input];
            output = (AVCaptureVideoDataOutput*)[session.outputs objectAtIndex:0];
            [session removeOutput:output];
            // TODO more cleanup ?
        }

        if (_this->hidden->frame_queue) {
            CFRelease(_this->hidden->frame_queue);
        }

        SDL_free(_this->hidden);
        _this->hidden = NULL;
    }
}

int
InitDevice(SDL_VideoCaptureDevice *_this)
{
    NSString *fmt = sdlformat_to_nsfourcc(_this->spec.format);
    int w = _this->spec.width;
    int h = _this->spec.height;

    NSError *error = nil;
    AVCaptureDevice *device = nil;
    AVCaptureDeviceInput *input = nil;
    AVCaptureVideoDataOutput *output = nil;

    AVCaptureDeviceFormat *spec_format = nil;

#ifdef __MACOS__
    if (@available(macOS 10.15, *)) {
        /* good. */
    } else {
        return -1;
    }
#endif

    device = get_device_by_name(_this->dev_name);
    if (!device) {
        goto error;
    }

    _this->hidden->session = [[AVCaptureSession alloc] init];
    if (_this->hidden->session == nil) {
        goto error;
    }

    [_this->hidden->session setSessionPreset:AVCaptureSessionPresetHigh];

    // Pick format that matches the spec
    {
        NSArray<AVCaptureDeviceFormat *> *formats = [device formats];
        for (AVCaptureDeviceFormat *format in formats) {
            CMFormatDescriptionRef formatDescription = [format formatDescription];
            FourCharCode mediaSubType = CMFormatDescriptionGetMediaSubType(formatDescription);
            NSString *str = fourcc_to_nstring(mediaSubType);
            if (str == fmt) {
                CMVideoDimensions dim = CMVideoFormatDescriptionGetDimensions(formatDescription);
                if (dim.width == w && dim.height == h) {
                    spec_format = format;
                    break;
                }
            }
        }
    }

    if (spec_format == nil) {
        SDL_SetError("format not found");
        goto error;
    }

    // Set format
    if ([device lockForConfiguration:NULL] == YES) {
        device.activeFormat = spec_format;
        [device unlockForConfiguration];
    } else {
        SDL_SetError("Cannot lockForConfiguration");
        goto error;
    }

    // Input
    input = [AVCaptureDeviceInput deviceInputWithDevice:device error:&error];
    if (!input) {
        SDL_SetError("Cannot create AVCaptureDeviceInput");
        goto error;
    }

    // Output
    output = [[AVCaptureVideoDataOutput alloc] init];

#ifdef __MACOS__
    // FIXME this now fail on ios ... but not using anything works...

    // Specify the pixel format
    output.videoSettings =
        [NSDictionary dictionaryWithObject:
        [NSNumber numberWithInt:kCVPixelFormatType_422YpCbCr8]
            forKey:(id)kCVPixelBufferPixelFormatTypeKey];
#endif

    _this->hidden->delegate = [[MySampleBufferDelegate alloc] init];
    [_this->hidden->delegate set:_this->hidden];


    CMSimpleQueueCreate(kCFAllocatorDefault, 30 /* buffers */, &_this->hidden->frame_queue);
    if (_this->hidden->frame_queue == nil) {
        goto error;
    }

    _this->hidden->queue = dispatch_queue_create("my_queue", NULL);
    [output setSampleBufferDelegate:_this->hidden->delegate queue:_this->hidden->queue];


    if ([_this->hidden->session canAddInput:input] ){
        [_this->hidden->session addInput:input];
    } else {
        SDL_SetError("Cannot add AVCaptureDeviceInput");
        goto error;
    }

    if ([_this->hidden->session canAddOutput:output] ){
        [_this->hidden->session addOutput:output];
    } else {
        SDL_SetError("Cannot add AVCaptureVideoDataOutput");
        goto error;
    }

    [_this->hidden->session commitConfiguration];

    return 0;

error:
    return -1;
}

int
GetDeviceSpec(SDL_VideoCaptureDevice *_this, SDL_VideoCaptureSpec *spec)
{
    if (spec) {
        *spec = _this->spec;
        return 0;
    }
    return -1;
}

int
StartCapture(SDL_VideoCaptureDevice *_this)
{
    [_this->hidden->session startRunning];
    return 0;
}

int
StopCapture(SDL_VideoCaptureDevice *_this)
{
    [_this->hidden->session stopRunning];
    return 0;
}

int
AcquireFrame(SDL_VideoCaptureDevice *_this, SDL_VideoCaptureFrame *frame)
{
    if (CMSimpleQueueGetCount(_this->hidden->frame_queue) > 0) {
        int i, numPlanes, planar;
        CMSampleBufferRef sampleBuffer;
        CVImageBufferRef image;

        sampleBuffer = (CMSampleBufferRef)CMSimpleQueueDequeue(_this->hidden->frame_queue);
        frame->internal = (void *) sampleBuffer;
        frame->timestampNS = SDL_GetTicksNS();

        i = 0;
        image = CMSampleBufferGetImageBuffer(sampleBuffer);
        numPlanes = CVPixelBufferGetPlaneCount(image);
        planar = CVPixelBufferIsPlanar(image);

#if 0
        int w = CVPixelBufferGetWidth(image);
        int h = CVPixelBufferGetHeight(image);
        int sz = CVPixelBufferGetDataSize(image);
        int pitch = CVPixelBufferGetBytesPerRow(image);
        SDL_Log("buffer planar=%d count:%d %d x %d sz=%d pitch=%d", planar, numPlanes, w, h, sz, pitch);
#endif

        CVPixelBufferLockBaseAddress(image, 0);

        if (planar == 0 && numPlanes == 0) {
            frame->pitch[0] = CVPixelBufferGetBytesPerRow(image);
            frame->data[0] = CVPixelBufferGetBaseAddress(image);
            frame->num_planes = 1;
        } else {
            for (i = 0; i < numPlanes && i < 3; i++) {
                int rowStride = 0;
                uint8_t *data = NULL;
                frame->num_planes += 1;

                rowStride = CVPixelBufferGetBytesPerRowOfPlane(image, i);
                data = CVPixelBufferGetBaseAddressOfPlane(image, i);
                frame->data[i] = data;
                frame->pitch[i] = rowStride;
            }
        }

        /* Unlocked when frame is released */

    } else {
        // no frame
        SDL_Delay(20); // TODO fix some delay
    }
    return 0;
}

int
ReleaseFrame(SDL_VideoCaptureDevice *_this, SDL_VideoCaptureFrame *frame)
{
    if (frame->internal){
        CMSampleBufferRef sampleBuffer = (CMSampleBufferRef) frame->internal;

        CVImageBufferRef image = CMSampleBufferGetImageBuffer(sampleBuffer);
        CVPixelBufferUnlockBaseAddress(image, 0);

        CFRelease(sampleBuffer);
    }
    return 0;
}

int
GetNumFormats(SDL_VideoCaptureDevice *_this)
{
    AVCaptureDevice *device = get_device_by_name(_this->dev_name);
    if (device) {
        // LIST FORMATS
        NSMutableOrderedSet<NSString *> *array_formats = [NSMutableOrderedSet new];
        NSArray<AVCaptureDeviceFormat *> *formats = [device formats];
        for (AVCaptureDeviceFormat *format in formats) {
            // NSLog(@"%@", formats);
            CMFormatDescriptionRef formatDescription = [format formatDescription];
            //NSLog(@"%@", formatDescription);
            FourCharCode mediaSubType = CMFormatDescriptionGetMediaSubType(formatDescription);
            NSString *str = fourcc_to_nstring(mediaSubType);
            [array_formats addObject:str];
        }
        return [array_formats count];
    }
    return 0;
}

int
GetFormat(SDL_VideoCaptureDevice *_this, int index, Uint32 *format)
{
    AVCaptureDevice *device = get_device_by_name(_this->dev_name);
    if (device) {
        // LIST FORMATS
        NSMutableOrderedSet<NSString *> *array_formats = [NSMutableOrderedSet new];
        NSArray<AVCaptureDeviceFormat *> *formats = [device formats];
        NSString *str;

        for (AVCaptureDeviceFormat *f in formats) {
            FourCharCode mediaSubType;
            CMFormatDescriptionRef formatDescription;

            formatDescription = [f formatDescription];
            mediaSubType = CMFormatDescriptionGetMediaSubType(formatDescription);
            str = fourcc_to_nstring(mediaSubType);
            [array_formats addObject:str];
        }

        str = array_formats[index];
        *format = nsfourcc_to_sdlformat(str);

        return 0;
    }
    return -1;
}

int
GetNumFrameSizes(SDL_VideoCaptureDevice *_this, Uint32 format)
{
    AVCaptureDevice *device = get_device_by_name(_this->dev_name);
    if (device) {
        NSString *fmt = sdlformat_to_nsfourcc(format);
        int count = 0;

        NSArray<AVCaptureDeviceFormat *> *formats = [device formats];
        for (AVCaptureDeviceFormat *f in formats) {
            CMFormatDescriptionRef formatDescription = [f formatDescription];
            FourCharCode mediaSubType = CMFormatDescriptionGetMediaSubType(formatDescription);
            NSString *str = fourcc_to_nstring(mediaSubType);

            if (str == fmt) {
                count += 1;
            }
        }
        return count;
    }
    return 0;
}

int
GetFrameSize(SDL_VideoCaptureDevice *_this, Uint32 format, int index, int *width, int *height)
{
    AVCaptureDevice *device = get_device_by_name(_this->dev_name);
    if (device) {
        NSString *fmt = sdlformat_to_nsfourcc(format);
        int count = 0;

        NSArray<AVCaptureDeviceFormat *> *formats = [device formats];
        for (AVCaptureDeviceFormat *f in formats) {
            CMFormatDescriptionRef formatDescription = [f formatDescription];
            FourCharCode mediaSubType = CMFormatDescriptionGetMediaSubType(formatDescription);
            NSString *str = fourcc_to_nstring(mediaSubType);

            if (str == fmt) {
                if (index == count) {
                    CMVideoDimensions dim = CMVideoFormatDescriptionGetDimensions(formatDescription);
                    *width = dim.width;
                    *height = dim.height;
                    return 0;
                }
                count += 1;
            }
        }
    }
    return -1;
}

int
GetDeviceName(SDL_VideoCaptureDeviceID instance_id, char *buf, int size)
{
    int index = instance_id - 1;
    NSArray<AVCaptureDevice *> *devices = discover_devices();
    if (index < [devices count]) {
        AVCaptureDevice *device = devices[index];
        NSString *cameraID = [device localizedName];
        const char *str = [cameraID UTF8String];
        SDL_snprintf(buf, size, "%s", str);
        return 0;
    }
    return -1;
}

static int
GetNumDevices(void)
{
    NSArray<AVCaptureDevice *> *devices = discover_devices();
    return [devices count];
}

SDL_VideoCaptureDeviceID *GetVideoCaptureDevices(int *count)
{
    /* hard-coded list of ID */
    int i;
    int num = GetNumDevices();
    SDL_VideoCaptureDeviceID *ret;

    ret = (SDL_VideoCaptureDeviceID *)SDL_malloc((num + 1) * sizeof(*ret));

    if (ret == NULL) {
        SDL_OutOfMemory();
        *count = 0;
        return NULL;
    }

    for (i = 0; i < num; i++) {
        ret[i] = i + 1;
    }
    ret[num] = 0;
    *count = num;
    return ret;
}

int SDL_SYS_VideoCaptureInit(void)
{
    return 0;
}

int SDL_SYS_VideoCaptureQuit(void)
{
    return 0;
}




#endif /* HAVE_COREMEDIA */

#endif /* SDL_VIDEO_CAPTURE */

