/*
 * CoreVideo video output driver
 * Copyright (c) 2005 Nicolas Plourde <nicolasplourde@gmail.com>
 *
 * This file is part of MPlayer.
 *
 * MPlayer is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * MPlayer is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with MPlayer; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#import "vo_corevideo.h"

// mplayer includes
#import "fastmemcpy.h"
#import "talloc.h"
#import "video_out.h"
#import "aspect.h"
#import "sub/font_load.h"
#import "sub/sub.h"
#import "subopt-helper.h"

#import "csputils.h"
#import "libmpcodecs/vfcap.h"
#import "libmpcodecs/mp_image.h"
#import "osd.h"

#import "gl_common.h"
#import "cocoa_common.h"

struct quad {
    GLfloat lowerLeft[2];
    GLfloat lowerRight[2];
    GLfloat upperRight[2];
    GLfloat upperLeft[2];
};

#define CV_VERTICES_PER_QUAD 6
#define CV_MAX_OSD_PARTS 20

struct osd_p {
    GLuint tex[CV_MAX_OSD_PARTS];
    NSRect tex_rect[CV_MAX_OSD_PARTS];
    int tex_cnt;
};

struct priv {
    MPGLContext *mpglctx;
    OSType pixelFormat;
    unsigned int image_width;
    unsigned int image_height;
    struct mp_csp_details colorspace;

    CVPixelBufferRef pixelBuffer;
    CVOpenGLTextureCacheRef textureCache;
    CVOpenGLTextureRef texture;
    struct quad *quad;

    struct osd_p *osd;
};

static void resize(struct vo *vo, int width, int height)
{
    struct priv *p = vo->priv;
    GL *gl = p->mpglctx->gl;
    p->image_width = width;
    p->image_height = height;

    mp_msg(MSGT_VO, MSGL_V, "[vo_corevideo] New OpenGL Viewport (0, 0, %d, "
                            "%d)\n", p->image_width, p->image_height);

    gl->Viewport(0, 0, p->image_width, p->image_height);
    gl->MatrixMode(GL_PROJECTION);
    gl->LoadIdentity();

    if (aspect_scaling()) {
        int new_w, new_h;
        GLdouble scale_x, scale_y;

        aspect(vo, &new_w, &new_h, A_WINZOOM);
        panscan_calc_windowed(vo);
        new_w += vo->panscan_x;
        new_h += vo->panscan_y;
        scale_x = (GLdouble)new_w / (GLdouble)p->image_width;
        scale_y = (GLdouble)new_h / (GLdouble)p->image_height;
        gl->Scaled(scale_x, scale_y, 1);
    }

    gl->Ortho(0, p->image_width, p->image_height, 0, -1.0, 1.0);
    gl->MatrixMode(GL_MODELVIEW);
    gl->LoadIdentity();

    force_load_font = 1;
    vo_osd_changed(OSDTYPE_OSD);

    gl->Clear(GL_COLOR_BUFFER_BIT);
    vo->want_redraw = true;
}

static int init_gl(struct vo *vo, uint32_t d_width, uint32_t d_height)
{
    struct priv *p = vo->priv;
    GL *gl = p->mpglctx->gl;

    const char *vendor     = gl->GetString(GL_VENDOR);
    const char *version    = gl->GetString(GL_VERSION);
    const char *renderer   = gl->GetString(GL_RENDERER);

    mp_msg(MSGT_VO, MSGL_V, "[vo_corevideo] Running on OpenGL '%s' by '%s',"
           " version '%s'\n", renderer, vendor, version);

    gl->Disable(GL_BLEND);
    gl->Disable(GL_DEPTH_TEST);
    gl->DepthMask(GL_FALSE);
    gl->Disable(GL_CULL_FACE);
    gl->Enable(GL_TEXTURE_2D);
    gl->DrawBuffer(GL_BACK);
    gl->TexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);

    resize(vo, d_width, d_height);

    gl->ClearColor(0.0f, 0.0f, 0.0f, 0.0f);
    gl->Clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    if (gl->SwapInterval)
        gl->SwapInterval(1);
    return 1;
}

static void release_cv_entities(struct vo *vo) {
    struct priv *p = vo->priv;
    CVPixelBufferRelease(p->pixelBuffer);
    p->pixelBuffer = NULL;
    CVOpenGLTextureRelease(p->texture);
    p->texture = NULL;
    CVOpenGLTextureCacheRelease(p->textureCache);
    p->textureCache = NULL;

}

static int config(struct vo *vo, uint32_t width, uint32_t height,
                  uint32_t d_width, uint32_t d_height, uint32_t flags,
                  uint32_t format)
{
    struct priv *p = vo->priv;
    release_cv_entities(vo);
    p->image_width = width;
    p->image_height = height;

    if (p->mpglctx->create_window(p->mpglctx, d_width, d_height, flags) < 0)
        return -1;
    if (p->mpglctx->setGlWindow(p->mpglctx) == SET_WINDOW_FAILED)
        return -1;

    init_gl(vo, vo->dwidth, vo->dheight);

    return 0;
}

static void check_events(struct vo *vo)
{
    struct priv *p = vo->priv;
    int e = p->mpglctx->check_events(vo);
    if (e & VO_EVENT_RESIZE)
        resize(vo, vo->dwidth, vo->dheight);
}

static void create_osd_texture(void *ctx, int x0, int y0, int w, int h,
                               unsigned char *src, unsigned char *srca,
                               int stride)
{
    struct priv *p = ((struct vo *) ctx)->priv;
    struct osd_p *osd = p->osd;
    GL *gl = p->mpglctx->gl;

    if (w <= 0 || h <= 0 || stride < w) {
        mp_msg(MSGT_VO, MSGL_V, "Invalid dimensions OSD for part!\n");
        return;
    }

    if (osd->tex_cnt >= CV_MAX_OSD_PARTS) {
        mp_msg(MSGT_VO, MSGL_ERR, "Too many OSD parts, contact the"
                                  " developers!\n");
        return;
    }

    gl->GenTextures(1, &osd->tex[osd->tex_cnt]);
    gl->BindTexture(GL_TEXTURE_2D, osd->tex[osd->tex_cnt]);
    glCreateClearTex(gl, GL_TEXTURE_2D, GL_LUMINANCE_ALPHA, GL_LUMINANCE_ALPHA,
                     GL_UNSIGNED_BYTE, GL_LINEAR, w, h, 0);
    {
        int i;
        unsigned char *tmp = malloc(stride * h * 2);
        // convert alpha from weird MPlayer scale.
        for (i = 0; i < h * stride; i++) {
            tmp[i*2+0] = src[i];
            tmp[i*2+1] = -srca[i];
        }
        glUploadTex(gl, GL_TEXTURE_2D, GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE,
                    tmp, stride * 2, 0, 0, w, h, 0);
        free(tmp);
    }

    osd->tex_rect[osd->tex_cnt] = NSMakeRect(x0, y0, w, h);

    gl->BindTexture(GL_TEXTURE_2D, 0);
    osd->tex_cnt++;
}

static void clearOSD(struct vo *vo)
{
    struct priv *p = vo->priv;
    struct osd_p *osd = p->osd;
    GL *gl = p->mpglctx->gl;

    if (!osd->tex_cnt)
        return;
    gl->DeleteTextures(osd->tex_cnt, osd->tex);
    osd->tex_cnt = 0;
}

static void draw_osd(struct vo *vo, struct osd_state *osd_s)
{
    struct priv *p = vo->priv;
    struct osd_p *osd = p->osd;
    GL *gl = p->mpglctx->gl;

    if (vo_osd_changed(0)) {
        clearOSD(vo);
        osd_draw_text_ext(osd_s, vo->dwidth, vo->dheight, 0, 0, 0, 0,
                          p->image_width, p->image_height, create_osd_texture,
                          vo);
    }

    if (osd->tex_cnt > 0) {
        gl->Enable(GL_BLEND);
        gl->BlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);

        for (int n = 0; n < osd->tex_cnt; n++) {
            NSRect tr = osd->tex_rect[n];
            gl->BindTexture(GL_TEXTURE_2D, osd->tex[n]);
            glDrawTex(gl, tr.origin.x, tr.origin.y,
                      tr.size.width, tr.size.height,
                      0, 0, 1.0, 1.0, 1, 1, 0, 0, 0);
        }

        gl->Disable(GL_BLEND);
        gl->BindTexture(GL_TEXTURE_2D, 0);
    }
}

static void prepare_texture(struct vo *vo)
{
    struct priv *p = vo->priv;
    struct quad *q = p->quad;
    CVReturn error;

    CVOpenGLTextureRelease(p->texture);
    error = CVOpenGLTextureCacheCreateTextureFromImage(NULL,
                p->textureCache, p->pixelBuffer, 0, &p->texture);
    if(error != kCVReturnSuccess)
        mp_msg(MSGT_VO, MSGL_ERR,"[vo_corevideo] Failed to create OpenGL"
                                 " texture(%d)\n", error);

    CVOpenGLTextureGetCleanTexCoords(p->texture, q->lowerLeft, q->lowerRight,
                                                 q->upperRight, q->upperLeft);
}

static void do_render(struct vo *vo)
{
    struct priv *p = vo->priv;
    struct quad *q = p->quad;
    GL *gl = p->mpglctx->gl;
    prepare_texture(vo);

    float x0 = 0;
    float y0 = 0;
    float  w = p->image_width;
    float  h = p->image_height;

    // vertically flips the image
    y0 += h;
    h = -h;

    float xm = x0 + w;
    float ym = y0 + h;

    gl->Enable(CVOpenGLTextureGetTarget(p->texture));
    gl->BindTexture(
            CVOpenGLTextureGetTarget(p->texture),
            CVOpenGLTextureGetName(p->texture));

    gl->Begin(GL_QUADS);
    gl->TexCoord2fv(q->lowerLeft);  gl->Vertex2f(x0, y0);
    gl->TexCoord2fv(q->upperLeft);  gl->Vertex2f(x0, ym);
    gl->TexCoord2fv(q->upperRight); gl->Vertex2f(xm, ym);
    gl->TexCoord2fv(q->lowerRight); gl->Vertex2f(xm, y0);
    gl->End();

    gl->Disable(CVOpenGLTextureGetTarget(p->texture));
}

static void flip_page(struct vo *vo)
{
    struct priv *p = vo->priv;
    p->mpglctx->swapGlBuffers(p->mpglctx);
    p->mpglctx->gl->Clear(GL_COLOR_BUFFER_BIT);
}

static uint32_t draw_image(struct vo *vo, mp_image_t *mpi)
{
    struct priv *p = vo->priv;
    CVReturn error;

    if (!p->textureCache || !p->pixelBuffer) {
        error = CVOpenGLTextureCacheCreate(NULL, 0, vo_cocoa_cgl_context(),
                    vo_cocoa_cgl_pixel_format(), 0, &p->textureCache);
        if(error != kCVReturnSuccess)
            mp_msg(MSGT_VO, MSGL_ERR,"[vo_corevideo] Failed to create OpenGL"
                                     " texture Cache(%d)\n", error);

        error = CVPixelBufferCreateWithBytes(NULL, mpi->width, mpi->height,
                    p->pixelFormat, mpi->planes[0], mpi->width * mpi->bpp / 8,
                    NULL, NULL, NULL, &p->pixelBuffer);
        if(error != kCVReturnSuccess)
            mp_msg(MSGT_VO, MSGL_ERR,"[vo_corevideo] Failed to create Pixel"
                                     "Buffer(%d)\n", error);
    }

    do_render(vo);
    return VO_TRUE;
}

static int query_format(struct vo *vo, uint32_t format)
{
    struct priv *p = vo->priv;
    const int flags = VFCAP_CSP_SUPPORTED | VFCAP_CSP_SUPPORTED_BY_HW |
                      VFCAP_OSD | VFCAP_HWSCALE_UP | VFCAP_HWSCALE_DOWN |
                      VOCAP_NOSLICES;
    switch (format) {
        case IMGFMT_YUY2:
            p->pixelFormat = kYUVSPixelFormat;
            return flags;

        case IMGFMT_RGB24:
            p->pixelFormat = k24RGBPixelFormat;
            return flags;

        case IMGFMT_ARGB:
            p->pixelFormat = k32ARGBPixelFormat;
            return flags;

        case IMGFMT_BGRA:
            p->pixelFormat = k32BGRAPixelFormat;
            return flags;
    }
    return 0;
}

static void uninit(struct vo *vo)
{
    struct priv *p = vo->priv;
    uninit_mpglcontext(p->mpglctx);
    release_cv_entities(vo);
}


static int preinit(struct vo *vo, const char *arg)
{
    struct priv *p = vo->priv;

    *p = (struct priv) {
        .mpglctx = init_mpglcontext(GLTYPE_COCOA, vo),
        .colorspace = MP_CSP_DETAILS_DEFAULTS,
        .quad = talloc_ptrtype(p, p->quad),
        .osd = talloc_ptrtype(p, p->osd),
    };

    *p->osd = (struct osd_p) {
        .tex_cnt = 0,
    };

    return 0;
}

static CFStringRef get_cv_csp_matrix(struct vo *vo)
{
    struct priv *p = vo->priv;
    switch (p->colorspace.format) {
        case MP_CSP_BT_601:
            return kCVImageBufferYCbCrMatrix_ITU_R_601_4;
        case MP_CSP_BT_709:
            return kCVImageBufferYCbCrMatrix_ITU_R_709_2;
        case MP_CSP_SMPTE_240M:
            return kCVImageBufferYCbCrMatrix_SMPTE_240M_1995;
    }
    return kCVImageBufferYCbCrMatrix_ITU_R_601_4;
}

static void set_yuv_colorspace(struct vo *vo)
{
    struct priv *p = vo->priv;
    CVBufferSetAttachment(p->pixelBuffer,
                          kCVImageBufferYCbCrMatrixKey, get_cv_csp_matrix(vo),
                          kCVAttachmentMode_ShouldPropagate);
    vo->want_redraw = true;
}

static int control(struct vo *vo, uint32_t request, void *data)
{
    struct priv *p = vo->priv;
    switch (request) {
        case VOCTRL_DRAW_IMAGE:
            return draw_image(vo, data);
        case VOCTRL_QUERY_FORMAT:
            return query_format(vo, *(uint32_t*)data);
        case VOCTRL_ONTOP:
            p->mpglctx->ontop(vo);
            return VO_TRUE;
        case VOCTRL_FULLSCREEN:
            p->mpglctx->fullscreen(vo);
            resize(vo, vo->dwidth, vo->dheight);
            return VO_TRUE;
        case VOCTRL_GET_PANSCAN:
            return VO_TRUE;
        case VOCTRL_SET_PANSCAN:
            resize(vo, vo->dwidth, vo->dheight);
            return VO_TRUE;
        case VOCTRL_UPDATE_SCREENINFO:
            p->mpglctx->update_xinerama_info(vo);
            return VO_TRUE;
        case VOCTRL_REDRAW_FRAME:
            do_render(vo);
            return VO_TRUE;
        case VOCTRL_SET_YUV_COLORSPACE:
            p->colorspace.format = ((struct mp_csp_details *)data)->format;
            set_yuv_colorspace(vo);
            return VO_TRUE;
        case VOCTRL_GET_YUV_COLORSPACE:
            *(struct mp_csp_details *)data = p->colorspace;
            return VO_TRUE;
    }
    return VO_NOTIMPL;
}

const struct vo_driver video_out_corevideo = {
    .is_new = true,
    .info = &(const vo_info_t) {
        "Mac OS X Core Video",
        "corevideo",
        "Nicolas Plourde <nicolas.plourde@gmail.com> and others",
        ""
    },
    .preinit = preinit,
    .config = config,
    .control = control,
    .draw_osd = draw_osd,
    .flip_page = flip_page,
    .check_events = check_events,
    .uninit = uninit,
    .privsize = sizeof(struct priv),
};
