/*
 * FM Screen Capture Codec decoder
 *
 * Copyright (c) 2017 Paul B Mahol
 *
 * This file is part of FFmpeg.
 *
 * FFmpeg is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * FFmpeg 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with FFmpeg; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "avcodec.h"
#include "bytestream.h"
#include "internal.h"

#define BLOCK_HEIGHT 112u
#define BLOCK_WIDTH  84u

typedef struct InterBlock {
    int      w, h;
    int      size;
    int      xor;
} InterBlock;

typedef struct FMVCContext {
    GetByteContext  gb;
    PutByteContext  pb;
    uint8_t        *buffer;
    size_t          buffer_size;
    uint8_t        *pbuffer;
    size_t          pbuffer_size;
    ptrdiff_t       stride;
    int             bpp;
    int             yb, xb;
    InterBlock     *blocks;
    unsigned        nb_blocks;
} FMVCContext;

static int decode_type2(GetByteContext *gb, PutByteContext *pb)
{
    unsigned repeat = 0, first = 1, opcode = 0;
    int i, len, pos;

    while (bytestream2_get_bytes_left(gb) > 0) {
        GetByteContext gbc;

        while (bytestream2_get_bytes_left(gb) > 0) {
            if (first) {
                first = 0;
                if (bytestream2_peek_byte(gb) > 17) {
                    len = bytestream2_get_byte(gb) - 17;
                    if (len < 4) {
                        do {
                            bytestream2_put_byte(pb, bytestream2_get_byte(gb));
                            --len;
                        } while (len);
                        opcode = bytestream2_peek_byte(gb);
                        continue;
                    } else {
                        do {
                            bytestream2_put_byte(pb, bytestream2_get_byte(gb));
                            --len;
                        } while (len);
                        opcode = bytestream2_peek_byte(gb);
                        if (opcode < 0x10) {
                            bytestream2_skip(gb, 1);
                            pos = - (opcode >> 2) - 4 * bytestream2_get_byte(gb) - 2049;

                            bytestream2_init(&gbc, pb->buffer_start, pb->buffer_end - pb->buffer_start);
                            bytestream2_seek(&gbc, bytestream2_tell_p(pb) + pos, SEEK_SET);

                            bytestream2_put_byte(pb, bytestream2_get_byte(&gbc));
                            bytestream2_put_byte(pb, bytestream2_get_byte(&gbc));
                            bytestream2_put_byte(pb, bytestream2_get_byte(&gbc));
                            len = opcode & 3;
                            if (!len) {
                                repeat = 1;
                            } else {
                                do {
                                    bytestream2_put_byte(pb, bytestream2_get_byte(gb));
                                    --len;
                                } while (len);
                                opcode = bytestream2_peek_byte(gb);
                            }
                            continue;
                        }
                    }
                    repeat = 0;
                }
                repeat = 1;
            }
            if (repeat) {
                repeat = 0;
                opcode = bytestream2_peek_byte(gb);
                if (opcode < 0x10) {
                    bytestream2_skip(gb, 1);
                    if (!opcode) {
                        if (!bytestream2_peek_byte(gb)) {
                            do {
                                bytestream2_skip(gb, 1);
                                opcode += 255;
                            } while (!bytestream2_peek_byte(gb) && bytestream2_get_bytes_left(gb) > 0);
                        }
                        opcode += bytestream2_get_byte(gb) + 15;
                    }
                    bytestream2_put_le32(pb, bytestream2_get_le32(gb));
                    for (i = opcode - 1; i > 0; --i)
                        bytestream2_put_byte(pb, bytestream2_get_byte(gb));
                    opcode = bytestream2_peek_byte(gb);
                    if (opcode < 0x10) {
                        bytestream2_skip(gb, 1);
                        pos = - (opcode >> 2) - 4 * bytestream2_get_byte(gb) - 2049;

                        bytestream2_init(&gbc, pb->buffer_start, pb->buffer_end - pb->buffer_start);
                        bytestream2_seek(&gbc, bytestream2_tell_p(pb) + pos, SEEK_SET);

                        bytestream2_put_byte(pb, bytestream2_get_byte(&gbc));
                        bytestream2_put_byte(pb, bytestream2_get_byte(&gbc));
                        bytestream2_put_byte(pb, bytestream2_get_byte(&gbc));
                        len = opcode & 3;
                        if (!len) {
                            repeat = 1;
                        } else {
                            do {
                                bytestream2_put_byte(pb, bytestream2_get_byte(gb));
                                --len;
                            } while (len);
                            opcode = bytestream2_peek_byte(gb);
                        }
                        continue;
                    }
                }
            }

            if (opcode >= 0x40) {
                bytestream2_skip(gb, 1);
                pos = - ((opcode >> 2) & 7) - 1 - 8 * bytestream2_get_byte(gb);
                len =    (opcode >> 5)      - 1;

                bytestream2_init(&gbc, pb->buffer_start, pb->buffer_end - pb->buffer_start);
                bytestream2_seek(&gbc, bytestream2_tell_p(pb) + pos, SEEK_SET);

                bytestream2_put_byte(pb, bytestream2_get_byte(&gbc));
                bytestream2_put_byte(pb, bytestream2_get_byte(&gbc));
                do {
                    bytestream2_put_byte(pb, bytestream2_get_byte(&gbc));
                    --len;
                } while (len);

                len = opcode & 3;

                if (!len) {
                    repeat = 1;
                } else {
                    do {
                        bytestream2_put_byte(pb, bytestream2_get_byte(gb));
                        --len;
                    } while (len);
                    opcode = bytestream2_peek_byte(gb);
                }
                continue;
            } else if (opcode < 0x20) {
                break;
            }
            len = opcode & 0x1F;
            bytestream2_skip(gb, 1);
            if (!len) {
                if (!bytestream2_peek_byte(gb)) {
                    do {
                        bytestream2_skip(gb, 1);
                        len += 255;
                    } while (!bytestream2_peek_byte(gb) && bytestream2_get_bytes_left(gb) > 0);
                }
                len += bytestream2_get_byte(gb) + 31;
            }
            i = bytestream2_get_le16(gb);
            pos = - (i >> 2) - 1;

            bytestream2_init(&gbc, pb->buffer_start, pb->buffer_end - pb->buffer_start);
            bytestream2_seek(&gbc, bytestream2_tell_p(pb) + pos, SEEK_SET);

            if (len < 6 || bytestream2_tell_p(pb) - bytestream2_tell(&gbc) < 4) {
                bytestream2_put_byte(pb, bytestream2_get_byte(&gbc));
                bytestream2_put_byte(pb, bytestream2_get_byte(&gbc));
                do {
                    bytestream2_put_byte(pb, bytestream2_get_byte(&gbc));
                    --len;
                } while (len);
            } else {
                bytestream2_put_le32(pb, bytestream2_get_le32(&gbc));
                for (len = len - 2; len; --len)
                    bytestream2_put_byte(pb, bytestream2_get_byte(&gbc));
            }
            len = i & 3;
            if (!len) {
                repeat = 1;
            } else {
                do {
                    bytestream2_put_byte(pb, bytestream2_get_byte(gb));
                    --len;
                } while (len);
                opcode = bytestream2_peek_byte(gb);
            }
        }
        bytestream2_skip(gb, 1);
        if (opcode < 0x10) {
            pos = -(opcode >> 2) - 1 - 4 * bytestream2_get_byte(gb);

            bytestream2_init(&gbc, pb->buffer_start, pb->buffer_end - pb->buffer_start);
            bytestream2_seek(&gbc, bytestream2_tell_p(pb) + pos, SEEK_SET);

            bytestream2_put_byte(pb, bytestream2_get_byte(&gbc));
            bytestream2_put_byte(pb, bytestream2_get_byte(&gbc));
            len = opcode & 3;
            if (!len) {
                repeat = 1;
            } else {
                do {
                    bytestream2_put_byte(pb, bytestream2_get_byte(gb));
                    --len;
                } while (len);
                opcode = bytestream2_peek_byte(gb);
            }
            continue;
        }
        len = opcode & 7;
        if (!len) {
            if (!bytestream2_peek_byte(gb)) {
                do {
                    bytestream2_skip(gb, 1);
                    len += 255;
                } while (!bytestream2_peek_byte(gb) && bytestream2_get_bytes_left(gb) > 0);
            }
            len += bytestream2_get_byte(gb) + 7;
        }
        i = bytestream2_get_le16(gb);
        pos = bytestream2_tell_p(pb) - 2048 * (opcode & 8);
        pos = pos - (i >> 2);
        if (pos == bytestream2_tell_p(pb))
            break;

        pos = pos - 0x4000;
        bytestream2_init(&gbc, pb->buffer_start, pb->buffer_end - pb->buffer_start);
        bytestream2_seek(&gbc, pos, SEEK_SET);

        if (len < 6 || bytestream2_tell_p(pb) - bytestream2_tell(&gbc) < 4) {
            bytestream2_put_byte(pb, bytestream2_get_byte(&gbc));
            bytestream2_put_byte(pb, bytestream2_get_byte(&gbc));
            do {
                bytestream2_put_byte(pb, bytestream2_get_byte(&gbc));
                --len;
            } while (len);
        } else {
            bytestream2_put_le32(pb, bytestream2_get_le32(&gbc));
            for (len = len - 2; len; --len)
                bytestream2_put_byte(pb, bytestream2_get_byte(&gbc));
        }

        len = i & 3;
        if (!len) {
            repeat = 1;
        } else {
            do {
                bytestream2_put_byte(pb, bytestream2_get_byte(gb));
                --len;
            } while (len);
            opcode = bytestream2_peek_byte(gb);
        }
    }

    return 0;
}

static int decode_type1(GetByteContext *gb, PutByteContext *pb)
{
    unsigned opcode = 0, len;
    int high = 0;
    int i, pos;

    while (bytestream2_get_bytes_left(gb) > 0) {
        GetByteContext gbc;

        while (bytestream2_get_bytes_left(gb) > 0) {
            while (bytestream2_get_bytes_left(gb) > 0) {
                opcode = bytestream2_get_byte(gb);
                high = opcode >= 0x20;
                if (high)
                    break;
                if (opcode)
                    break;
                opcode = bytestream2_get_byte(gb);
                if (opcode < 0xF8) {
                    opcode += 32;
                    break;
                }
                i = opcode - 0xF8;
                if (i) {
                    len = 256;
                    do {
                        len *= 2;
                        --i;
                    } while (i);
                } else {
                    len = 280;
                }
                do {
                    bytestream2_put_le32(pb, bytestream2_get_le32(gb));
                    bytestream2_put_le32(pb, bytestream2_get_le32(gb));
                    len -= 8;
                } while (len && bytestream2_get_bytes_left(gb) > 0);
            }

            if (!high) {
                do {
                    bytestream2_put_byte(pb, bytestream2_get_byte(gb));
                    --opcode;
                } while (opcode && bytestream2_get_bytes_left(gb) > 0);

                while (bytestream2_get_bytes_left(gb) > 0) {
                    GetByteContext gbc;

                    opcode = bytestream2_get_byte(gb);
                    if (opcode >= 0x20)
                        break;
                    bytestream2_init(&gbc, pb->buffer_start, pb->buffer_end - pb->buffer_start);

                    pos = -(opcode | 32 * bytestream2_get_byte(gb)) - 1;
                    bytestream2_seek(&gbc, bytestream2_tell_p(pb) + pos, SEEK_SET);
                    bytestream2_put_byte(pb, bytestream2_get_byte(&gbc));
                    bytestream2_put_byte(pb, bytestream2_get_byte(&gbc));
                    bytestream2_put_byte(pb, bytestream2_get_byte(&gbc));
                    bytestream2_put_byte(pb, bytestream2_get_byte(gb));
                }
            }
            high = 0;
            if (opcode < 0x40)
                break;
            bytestream2_init(&gbc, pb->buffer_start, pb->buffer_end - pb->buffer_start);
            pos = (-((opcode & 0x1F) | 32 * bytestream2_get_byte(gb)) - 1);
            bytestream2_seek(&gbc, bytestream2_tell_p(pb) + pos, SEEK_SET);
            bytestream2_put_byte(pb, bytestream2_get_byte(&gbc));
            bytestream2_put_byte(pb, bytestream2_get_byte(&gbc));
            len = (opcode >> 5) - 1;
            do {
                bytestream2_put_byte(pb, bytestream2_get_byte(&gbc));
                --len;
            } while (len && bytestream2_get_bytes_left(&gbc) > 0);
        }
        len = opcode & 0x1F;
        if (!len) {
            if (!bytestream2_peek_byte(gb)) {
                do {
                    bytestream2_skip(gb, 1);
                    len += 255;
                } while (!bytestream2_peek_byte(gb) && bytestream2_get_bytes_left(gb) > 0);
            }
            len += bytestream2_get_byte(gb) + 31;
        }
        pos = -bytestream2_get_byte(gb);
        bytestream2_init(&gbc, pb->buffer_start, pb->buffer_end - pb->buffer_start);
        bytestream2_seek(&gbc, bytestream2_tell_p(pb) + pos - (bytestream2_get_byte(gb) << 8), SEEK_SET);
        if (bytestream2_tell_p(pb) == bytestream2_tell(&gbc))
            break;
        if (len < 5 || bytestream2_tell_p(pb) - bytestream2_tell(&gbc) < 4) {
            bytestream2_put_byte(pb, bytestream2_get_byte(&gbc));
            bytestream2_put_byte(pb, bytestream2_get_byte(&gbc));
            bytestream2_put_byte(pb, bytestream2_get_byte(&gbc));
        } else {
            bytestream2_put_le32(pb, bytestream2_get_le32(&gbc));
            len--;
        }
        do {
            bytestream2_put_byte(pb, bytestream2_get_byte(&gbc));
            len--;
        } while (len && bytestream2_get_bytes_left(&gbc) > 0);
    }

    return 0;
}

static int decode_frame(AVCodecContext *avctx, void *data,
                        int *got_frame, AVPacket *avpkt)
{
    FMVCContext *s = avctx->priv_data;
    GetByteContext *gb = &s->gb;
    PutByteContext *pb = &s->pb;
    AVFrame *frame = data;
    int ret, y, x;

    if (avpkt->size < 8)
        return AVERROR_INVALIDDATA;

    if ((ret = ff_get_buffer(avctx, frame, 0)) < 0)
        return ret;

    bytestream2_init(gb, avpkt->data, avpkt->size);
    bytestream2_skip(gb, 2);

    frame->key_frame = !!bytestream2_get_le16(gb);
    frame->pict_type = frame->key_frame ? AV_PICTURE_TYPE_I : AV_PICTURE_TYPE_P;

    if (frame->key_frame) {
        const uint8_t *src;
        unsigned type, size;
        uint8_t *dst;

        type = bytestream2_get_le16(gb);
        size = bytestream2_get_le16(gb);
        if (size > bytestream2_get_bytes_left(gb))
            return AVERROR_INVALIDDATA;

        bytestream2_init_writer(pb, s->buffer, s->buffer_size);
        if (type == 1) {
            decode_type1(gb, pb);
        } else if (type == 2){
            decode_type2(gb, pb);
        } else {
            avpriv_report_missing_feature(avctx, "Compression type %d", type);
            return AVERROR_PATCHWELCOME;
        }

        src = s->buffer;
        dst = frame->data[0] + (avctx->height - 1) * frame->linesize[0];
        for (y = 0; y < avctx->height; y++) {
            memcpy(dst, src, avctx->width * s->bpp);
            dst -= frame->linesize[0];
            src += s->stride * 4;
        }
    } else {
        unsigned block, nb_blocks;
        int type, k, l;
        uint8_t *ssrc, *ddst;
        const uint32_t *src;
        uint32_t *dst;

        for (block = 0; block < s->nb_blocks; block++)
            s->blocks[block].xor = 0;

        nb_blocks = bytestream2_get_le16(gb);
        if (nb_blocks > s->nb_blocks)
            return AVERROR_INVALIDDATA;

        bytestream2_init_writer(pb, s->pbuffer, s->pbuffer_size);

        type = bytestream2_get_le16(gb);
        for (block = 0; block < nb_blocks; block++) {
            unsigned size, offset;
            int start = 0;

            offset = bytestream2_get_le16(gb);
            if (offset >= s->nb_blocks)
                return AVERROR_INVALIDDATA;

            size = bytestream2_get_le16(gb);
            if (size > bytestream2_get_bytes_left(gb))
                return AVERROR_INVALIDDATA;

            start = bytestream2_tell_p(pb);
            if (type == 1) {
                decode_type1(gb, pb);
            } else if (type == 2){
                decode_type2(gb, pb);
            } else {
                avpriv_report_missing_feature(avctx, "Compression type %d", type);
                return AVERROR_PATCHWELCOME;
            }

            if (s->blocks[offset].size * 4 != bytestream2_tell_p(pb) - start)
                return AVERROR_INVALIDDATA;

            s->blocks[offset].xor = 1;
        }

        src = (const uint32_t *)s->pbuffer;
        dst = (uint32_t *)s->buffer;

        for (block = 0, y = 0; y < s->yb; y++) {
            int block_h = s->blocks[block].h;
            uint32_t *rect = dst;

            for (x = 0; x < s->xb; x++) {
                int block_w = s->blocks[block].w;
                uint32_t *row = dst;

                block_h = s->blocks[block].h;
                if (s->blocks[block].xor) {
                    for (k = 0; k < block_h; k++) {
                        uint32_t *column = dst;
                        for (l = 0; l < block_w; l++)
                            *dst++ ^= *src++;
                        dst = &column[s->stride];
                    }
                }
                dst = &row[block_w];
                ++block;
            }
            dst = &rect[block_h * s->stride];
        }

        ssrc = s->buffer;
        ddst = frame->data[0] + (avctx->height - 1) * frame->linesize[0];
        for (y = 0; y < avctx->height; y++) {
            memcpy(ddst, ssrc, avctx->width * s->bpp);
            ddst -= frame->linesize[0];
            ssrc += s->stride * 4;
        }
    }

    *got_frame = 1;

    return avpkt->size;
}

static av_cold int decode_init(AVCodecContext *avctx)
{
    FMVCContext *s = avctx->priv_data;
    int i, j, m, block = 0, h = BLOCK_HEIGHT, w = BLOCK_WIDTH;

    switch (avctx->bits_per_coded_sample) {
    case 16:
        avctx->pix_fmt = AV_PIX_FMT_RGB555LE;
        break;
    case 24:
        avctx->pix_fmt = AV_PIX_FMT_BGR24;
        break;
    case 32:
        avctx->pix_fmt = AV_PIX_FMT_BGRA;
        break;
    default:
        av_log(avctx, AV_LOG_ERROR, "Unsupported bitdepth %i\n",
               avctx->bits_per_coded_sample);
        return AVERROR_INVALIDDATA;
    }

    s->stride = (avctx->width * avctx->bits_per_coded_sample + 31) / 32;
    s->xb     = s->stride / BLOCK_WIDTH;
    m         = s->stride % BLOCK_WIDTH;
    if (m) {
        if (m < 37) {
            w = m + BLOCK_WIDTH;
        } else {
            w = m;
            s->xb++;
        }
    }

    s->yb = avctx->height / BLOCK_HEIGHT;
    m     = avctx->height % BLOCK_HEIGHT;
    if (m) {
        if (m < 49) {
            h = m + BLOCK_HEIGHT;
        } else {
            h = m;
            s->yb++;
        }
    }

    s->nb_blocks = s->xb * s->yb;
    if (!s->nb_blocks)
        return AVERROR_INVALIDDATA;
    s->blocks    = av_calloc(s->nb_blocks, sizeof(*s->blocks));
    if (!s->blocks)
        return AVERROR(ENOMEM);

    for (i = 0; i < s->yb; i++) {
        for (j = 0; j < s->xb; j++) {
            if (i != (s->yb - 1) || j != (s->xb - 1)) {
                if (i == s->yb - 1) {
                    s->blocks[block].w    = BLOCK_WIDTH;
                    s->blocks[block].h    = h;
                    s->blocks[block].size = BLOCK_WIDTH * h;
                } else if (j == s->xb - 1) {
                    s->blocks[block].w    = w;
                    s->blocks[block].h    = BLOCK_HEIGHT;
                    s->blocks[block].size = BLOCK_HEIGHT * w;
                } else {
                    s->blocks[block].w    = BLOCK_WIDTH;
                    s->blocks[block].h    = BLOCK_HEIGHT;
                    s->blocks[block].size = BLOCK_WIDTH * BLOCK_HEIGHT;
                }
            } else {
                s->blocks[block].w    = w;
                s->blocks[block].h    = h;
                s->blocks[block].size = w * h;
            }
            block++;
        }
    }

    s->bpp          = avctx->bits_per_coded_sample >> 3;
    s->buffer_size  = avctx->width * avctx->height * 4;
    s->pbuffer_size = avctx->width * avctx->height * 4;
    s->buffer       = av_mallocz(s->buffer_size);
    s->pbuffer      = av_mallocz(s->pbuffer_size);
    if (!s->buffer || !s->pbuffer)
        return AVERROR(ENOMEM);

    return 0;
}

static av_cold int decode_close(AVCodecContext *avctx)
{
    FMVCContext *s = avctx->priv_data;

    av_freep(&s->buffer);
    av_freep(&s->pbuffer);
    av_freep(&s->blocks);

    return 0;
}

AVCodec ff_fmvc_decoder = {
    .name             = "fmvc",
    .long_name        = NULL_IF_CONFIG_SMALL("FM Screen Capture Codec"),
    .type             = AVMEDIA_TYPE_VIDEO,
    .id               = AV_CODEC_ID_FMVC,
    .priv_data_size   = sizeof(FMVCContext),
    .init             = decode_init,
    .close            = decode_close,
    .decode           = decode_frame,
    .capabilities     = AV_CODEC_CAP_DR1,
    .caps_internal    = FF_CODEC_CAP_INIT_THREADSAFE |
                        FF_CODEC_CAP_INIT_CLEANUP,
};
