/*
 * Copyright © 2018-2026, VideoLAN and dav2d authors
 * Copyright © 2018-2026, Two Orioles, LLC
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "config.h"
#include "cli_config.h"

#include <getopt.h>
#include <limits.h>
#include <math.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#if HAVE_UNISTD_H
# include <unistd.h>
#endif

#include "dav2d_cli_parse.h"
#include "common/attributes.h"
#include "src/cpu.h"

static const char short_opts[] = "i:o:vql:s:";

enum {
    ARG_DEMUXER = 256,
    ARG_MUXER,
    ARG_FRAME_TIMES,
    ARG_REALTIME,
    ARG_REALTIME_CACHE,
    ARG_THREADS,
    ARG_FRAME_DELAY,
    ARG_VERIFY,
    ARG_FILM_GRAIN,
    ARG_OPPOINT,
    ARG_ALL_LAYERS,
    ARG_SIZE_LIMIT,
    ARG_STRICT_STD_COMPLIANCE,
    ARG_CPU_MASK,
    ARG_NEG_STRIDE,
    ARG_OUTPUT_INVISIBLE,
    ARG_INLOOP_FILTERS,
    ARG_DECODE_FRAME_TYPE,
};

static const struct option long_opts[] = {
    { "input",           1, NULL, 'i' },
    { "output",          1, NULL, 'o' },
    { "quiet",           0, NULL, 'q' },
    { "demuxer",         1, NULL, ARG_DEMUXER },
    { "muxer",           1, NULL, ARG_MUXER },
    { "version",         0, NULL, 'v' },
    { "frametimes",      1, NULL, ARG_FRAME_TIMES },
    { "limit",           1, NULL, 'l' },
    { "skip",            1, NULL, 's' },
    { "realtime",        2, NULL, ARG_REALTIME },
    { "realtimecache",   1, NULL, ARG_REALTIME_CACHE },
    { "threads",         1, NULL, ARG_THREADS },
    { "framedelay",      1, NULL, ARG_FRAME_DELAY },
    { "verify",          1, NULL, ARG_VERIFY },
    { "filmgrain",       1, NULL, ARG_FILM_GRAIN },
    { "oppoint",         1, NULL, ARG_OPPOINT },
    { "alllayers",       1, NULL, ARG_ALL_LAYERS },
    { "sizelimit",       1, NULL, ARG_SIZE_LIMIT },
    { "strict",          1, NULL, ARG_STRICT_STD_COMPLIANCE },
    { "cpumask",         1, NULL, ARG_CPU_MASK },
    { "negstride",       0, NULL, ARG_NEG_STRIDE },
    { "outputinvisible", 1, NULL, ARG_OUTPUT_INVISIBLE },
    { "inloopfilters",   1, NULL, ARG_INLOOP_FILTERS },
    { "decodeframetype", 1, NULL, ARG_DECODE_FRAME_TYPE },
    { NULL,              0, NULL, 0 },
};

#if HAVE_XXHASH_H
#define AVAILABLE_MUXERS "'md5', 'xxh3', 'yuv', 'yuv4mpeg2' or 'null'"
#else
#define AVAILABLE_MUXERS "'md5', 'yuv', 'yuv4mpeg2' or 'null'"
#endif

#if ARCH_AARCH64 || ARCH_ARM
#define ALLOWED_CPU_MASKS ", 'neon', 'dotprod' or 'i8mm'"
#elif ARCH_LOONGARCH
#define ALLOWED_CPU_MASKS ", 'lsx' or 'lasx'"
#elif ARCH_PPC64LE
#define ALLOWED_CPU_MASKS ", 'vsx' or 'pwr9'"
#elif ARCH_RISCV
#define ALLOWED_CPU_MASKS " or 'rvv'"
#elif ARCH_X86
#define ALLOWED_CPU_MASKS \
    ", 'sse2', 'ssse3', 'sse41', 'avx2' or 'avx512icl'"
#else
#define ALLOWED_CPU_MASKS "not yet implemented for this architecture"
#endif

static void usage(const char *const app, const char *const reason, ...) {
    if (reason) {
        va_list args;

        va_start(args, reason);
        vfprintf(stderr, reason, args);
        va_end(args);
        fprintf(stderr, "\n\n");
    }
    fprintf(stderr, "Usage: %s [options]\n\n", app);
    fprintf(stderr, "Supported options:\n"
            " --input/-i $file:     input file\n"
            " --output/-o $file:    output file (%%n, %%w or %%h will be filled in for per-frame files)\n"
            " --demuxer $name:      force demuxer type ('ivf', 'section5' or 'annexb'; default: detect from content)\n"
            " --muxer $name:        force muxer type (" AVAILABLE_MUXERS "; default: detect from extension)\n"
            "                       use 'frame' as prefix to write per-frame files; if filename contains %%n, will default to writing per-frame files\n"
            " --quiet/-q:           disable status messages\n"
            " --frametimes $file:   dump frame times to file\n"
            " --limit/-l $num:      stop decoding after $num frames\n"
            " --skip/-s $num:       skip decoding of the first $num frames\n"
            " --realtime [$fract]:  limit framerate, optional argument to override input framerate\n"
            " --realtimecache $num: set the size of the cache in realtime mode (default: 0)\n"
            " --version/-v:         print version and exit\n"
            " --threads $num:       number of threads (default: 0)\n"
            " --framedelay $num:    maximum frame delay, capped at $threads (default: 0);\n"
            "                       set to 1 for low-latency decoding\n"
            " --filmgrain $num:     enable film grain application (default: 1, except if muxer is md5 or xxh3)\n"
            " --oppoint $num:       select an operating point of a scalable AV2 bitstream (0 - 31)\n"
            " --alllayers $num:     output all spatial layers of a scalable AV2 bitstream (default: 1)\n"
            " --sizelimit $num:     stop decoding if the frame size exceeds the specified limit\n"
            " --strict $num:        whether to abort decoding on standard compliance violations\n"
            "                       that don't affect bitstream decoding (default: 1)\n"
            " --verify $md5:        verify decoded md5. implies --muxer md5, no output\n"
            " --cpumask $mask:      restrict permitted CPU instruction sets (0" ALLOWED_CPU_MASKS "; default: -1)\n"
            " --negstride:          use negative picture strides\n"
            "                       this is mostly meant as a developer option\n"
            " --outputinvisible $num: whether to output invisible (alt-ref) frames (default: 0)\n"
            " --inloopfilters $str: which in-loop filters to enable (none, (no)deblock, (no)cdef, (no)restoration or all; default: all)\n"
            " --decodeframetype $str: which frame types to decode (reference, intra, key or all; default: all)\n"
            );
    exit(1);
}

static void error(const char *const app, const char *const optarg,
                  const int option, const char *const shouldbe)
{
    char optname[256];
    int n;

    for (n = 0; long_opts[n].name; n++)
        if (long_opts[n].val == option)
            break;
    assert(long_opts[n].name);
    if (long_opts[n].val < 256) {
        snprintf(optname, sizeof(optname), "-%c/--%s", long_opts[n].val, long_opts[n].name);
    } else {
        snprintf(optname, sizeof(optname), "--%s", long_opts[n].name);
    }

    usage(app, "Invalid argument \"%s\" for option %s; should be %s",
          optarg, optname, shouldbe);
}

static unsigned parse_unsigned(const char *const optarg, const int option,
                               const char *const app)
{
    char *end;
    const unsigned res = (unsigned) strtoul(optarg, &end, 0);
    if (*end || end == optarg) error(app, optarg, option, "an integer");
    return res;
}

static int parse_optional_fraction(const char *const optarg, const int option,
                                   const char *const app, double *value)
{
    if (optarg == NULL) return 0;
    char *end;
    *value = strtod(optarg, &end);
    if (*end == '/' && end != optarg) {
        const char *optarg2 = end + 1;
        *value /= strtod(optarg2, &end);
        if (*end || end == optarg2) error(app, optarg, option, "a fraction");
    } else if (*end || end == optarg) {
        error(app, optarg, option, "a fraction");
    }
    return 1;
}

typedef struct EnumParseTable {
    const char *str;
    const int val;
} EnumParseTable;

#if ARCH_X86
enum CpuMask {
    X86_CPU_MASK_SSE2      = DAV2D_X86_CPU_FLAG_SSE2,
    X86_CPU_MASK_SSSE3     = DAV2D_X86_CPU_FLAG_SSSE3     | X86_CPU_MASK_SSE2,
    X86_CPU_MASK_SSE41     = DAV2D_X86_CPU_FLAG_SSE41     | X86_CPU_MASK_SSSE3,
    X86_CPU_MASK_AVX2      = DAV2D_X86_CPU_FLAG_AVX2      | X86_CPU_MASK_SSE41,
    X86_CPU_MASK_AVX512ICL = DAV2D_X86_CPU_FLAG_AVX512ICL | X86_CPU_MASK_AVX2,
};
#elif ARCH_AARCH64 || ARCH_ARM
enum CpuMask {
    ARM_CPU_MASK_NEON      = DAV2D_ARM_CPU_FLAG_NEON,
    ARM_CPU_MASK_DOTPROD   = DAV2D_ARM_CPU_FLAG_DOTPROD | ARM_CPU_MASK_NEON,
    ARM_CPU_MASK_I8MM      = DAV2D_ARM_CPU_FLAG_I8MM    | ARM_CPU_MASK_DOTPROD,
#if ARCH_AARCH64
    // SVE doesn't imply DOTPROD or I8MM.
    ARM_CPU_MASK_SVE       = DAV2D_ARM_CPU_FLAG_SVE     | ARM_CPU_MASK_NEON,
    // SVE2 implies DOTPROD, but not I8MM.
    ARM_CPU_MASK_SVE2      = DAV2D_ARM_CPU_FLAG_SVE2    | ARM_CPU_MASK_SVE | ARM_CPU_MASK_DOTPROD,
#endif
};
#endif

#if ARCH_PPC64LE
enum CpuMask {
    PPC_CPU_MASK_VSX       = DAV2D_PPC_CPU_FLAG_VSX,
    PPC_CPU_MASK_PWR9      = DAV2D_PPC_CPU_FLAG_VSX | DAV2D_PPC_CPU_FLAG_PWR9,
};
#endif


static const EnumParseTable cpu_mask_tbl[] = {
#if ARCH_AARCH64 || ARCH_ARM
    { "neon",    ARM_CPU_MASK_NEON },
    { "dotprod", ARM_CPU_MASK_DOTPROD },
    { "i8mm",    ARM_CPU_MASK_I8MM },
#if ARCH_AARCH64
    { "sve",     ARM_CPU_MASK_SVE },
    { "sve2",    ARM_CPU_MASK_SVE2 },
#endif /* ARCH_AARCH64 */
#elif ARCH_LOONGARCH
    { "lsx", DAV2D_LOONGARCH_CPU_FLAG_LSX },
    { "lasx", DAV2D_LOONGARCH_CPU_FLAG_LASX },
#elif ARCH_PPC64LE
    { "vsx",  PPC_CPU_MASK_VSX },
    { "pwr9", PPC_CPU_MASK_PWR9 },
#elif ARCH_RISCV
    { "rvv", DAV2D_RISCV_CPU_FLAG_V },
#elif ARCH_X86
    { "sse2",      X86_CPU_MASK_SSE2 },
    { "ssse3",     X86_CPU_MASK_SSSE3 },
    { "sse41",     X86_CPU_MASK_SSE41 },
    { "avx2",      X86_CPU_MASK_AVX2 },
    { "avx512icl", X86_CPU_MASK_AVX512ICL },
#endif
    { "none",      0 },
};

static const EnumParseTable inloop_filters_tbl[] = {
    { "none",          0 },
    { "deblock",       DAV2D_INLOOPFILTER_DEBLOCK },
    { "cdef",          DAV2D_INLOOPFILTER_CDEF },
    { "ccso",          DAV2D_INLOOPFILTER_CCSO },
    { "wiener",        DAV2D_INLOOPFILTER_WIENER },
    { "gdf",           DAV2D_INLOOPFILTER_GDF },
    { "all",           DAV2D_INLOOPFILTER_ALL },
};

static const EnumParseTable decode_frame_type_tbl[] = {
    { "all",           DAV2D_DECODEFRAMETYPE_ALL },
    { "reference",     DAV2D_DECODEFRAMETYPE_REFERENCE },
    { "intra",         DAV2D_DECODEFRAMETYPE_INTRA },
    { "key",           DAV2D_DECODEFRAMETYPE_KEY },
};

static unsigned parse_enum(char *optarg, const EnumParseTable *const tbl,
                           const int tbl_sz, const int option, const char *app)
{
    char str[1024];

    strcpy(str, "any of ");
    for (int n = 0; n < tbl_sz; n++) {
        if (!strcmp(tbl[n].str, optarg))
            return tbl[n].val;

        if (n) {
            if (n < tbl_sz - 1)
                strcat(str, ", ");
            else
                strcat(str, " or ");
        }
        strcat(str, tbl[n].str);
    }

    char *end;
    unsigned res;
    if (!strncmp(optarg, "0x", 2)) {
        res = (unsigned) strtoul(&optarg[2], &end, 16);
    } else {
        res = (unsigned) strtoul(optarg, &end, 0);
    }

    if (*end || end == optarg) {
        strcat(str, ", a hexadecimal (starting with 0x), or an integer");
        error(app, optarg, option, str);
    }

    return res;
}

static int parse_enum_mask(const char *const optargs, unsigned const start_mask,
                           const EnumParseTable *const tbl, const int tbl_sz,
                           const int option, const char *app)
{
    const int start_with_sign = optargs[0] == '+' || optargs[0] == '-';
    unsigned res = start_with_sign ? start_mask : 0;
    int sub_instead_of_add = optargs[0] == '-';
    const char *end = optargs + start_with_sign;
    for (;;) {
        const char *start = end;
        const char *plus = strchr(start, '+');
        const char *min = strchr(start, '-');
        if (plus && min) {
            assert(plus != min);
            end = plus < min ? plus : min;
        } else {
            end = plus ? plus : min;
        }
        const ptrdiff_t len = end ? end - start : (ptrdiff_t) strlen(start);
        int n;
        for (n = 0; n < tbl_sz; n++)
            if (!strncmp(tbl[n].str, start, len) && !tbl[n].str[len])
                break;
        if (n == tbl_sz) break;
        if (sub_instead_of_add) {
            res &= ~tbl[n].val;
        } else {
            res |= tbl[n].val;
        }
        if (end) {
            sub_instead_of_add = *end == '-';
            end++;
        } else {
            return res;
        }
    }

    char err_msg[1024];
    size_t len = snprintf(err_msg, sizeof(err_msg), "plus/min-separated values from ");
    for (int n = 0; n < tbl_sz; n++) {
        len += snprintf(err_msg + len, sizeof(err_msg) - len, "%s%s",
                        n ? n + 1 < tbl_sz ? ", " : " or " : "",
                        tbl[n].str);
    }

    error(app, optarg, option, err_msg);
    return -1;
}

void parse(const int argc, char *const *const argv,
           CLISettings *const cli_settings, Dav2dSettings *const lib_settings)
{
    int o;

    memset(cli_settings, 0, sizeof(*cli_settings));
    dav2d_default_settings(lib_settings);
    lib_settings->strict_std_compliance = 1; // override library default
    int grain_specified = 0;

    while ((o = getopt_long(argc, argv, short_opts, long_opts, NULL)) != -1) {
        switch (o) {
        case 'o':
            cli_settings->outputfile = optarg;
            break;
        case 'i':
            cli_settings->inputfile = optarg;
            break;
        case 'q':
            cli_settings->quiet = 1;
            break;
        case 'l':
            cli_settings->limit = parse_unsigned(optarg, 'l', argv[0]);
            break;
        case 's':
            cli_settings->skip = parse_unsigned(optarg, 's', argv[0]);
            break;
        case ARG_DEMUXER:
            cli_settings->demuxer = optarg;
            break;
        case ARG_MUXER:
            cli_settings->muxer = optarg;
            break;
        case ARG_FRAME_TIMES:
            cli_settings->frametimes = optarg;
            break;
        case ARG_REALTIME:
            // workaround to parse an optional argument of the form `--a b`
            // (getopt only allows `--a=b`)
            if (optarg == NULL && optind < argc && argv[optind] != NULL &&
                argv[optind][0] != '-')
            {
                optarg = argv[optind];
                optind++;
            }
            cli_settings->realtime = 1 + parse_optional_fraction(optarg,
                ARG_REALTIME, argv[0], &cli_settings->realtime_fps);
            break;
        case ARG_REALTIME_CACHE:
            cli_settings->realtime_cache =
                parse_unsigned(optarg, ARG_REALTIME_CACHE, argv[0]);
            break;
        case ARG_FRAME_DELAY:
            lib_settings->max_frame_delay =
                parse_unsigned(optarg, ARG_FRAME_DELAY, argv[0]);
            break;
        case ARG_THREADS:
            lib_settings->n_threads =
                parse_unsigned(optarg, ARG_THREADS, argv[0]);
            break;
        case ARG_VERIFY:
            cli_settings->verify = optarg;
            break;
        case ARG_FILM_GRAIN:
            lib_settings->apply_grain =
                !!parse_unsigned(optarg, ARG_FILM_GRAIN, argv[0]);
            grain_specified = 1;
            break;
        case ARG_OPPOINT:
            lib_settings->operating_point =
                parse_unsigned(optarg, ARG_OPPOINT, argv[0]);
            break;
        case ARG_ALL_LAYERS:
            lib_settings->all_layers =
                !!parse_unsigned(optarg, ARG_ALL_LAYERS, argv[0]);
            break;
        case ARG_SIZE_LIMIT: {
            char *arg = optarg, *end;
            uint64_t res = strtoul(arg, &end, 0);
            if (*end == 'x') // NxM
                res *= strtoul((arg = end + 1), &end, 0);
            if (*end || end == arg || res >= UINT_MAX)
                error(argv[0], optarg, ARG_SIZE_LIMIT, "an integer or dimension");
            lib_settings->frame_size_limit = (unsigned) res;
            break;
        }
        case ARG_STRICT_STD_COMPLIANCE:
            lib_settings->strict_std_compliance =
                parse_unsigned(optarg, ARG_STRICT_STD_COMPLIANCE, argv[0]);
            break;
        case 'v':
            fprintf(stderr, "%s\n", dav2d_version());
            exit(0);
        case ARG_CPU_MASK:
            dav2d_set_cpu_flags_mask(parse_enum(optarg, cpu_mask_tbl, ARRAY_SIZE(cpu_mask_tbl),
                                                ARG_CPU_MASK, argv[0]));
            break;
        case ARG_NEG_STRIDE:
            cli_settings->neg_stride = 1;
            break;
        case ARG_OUTPUT_INVISIBLE:
            lib_settings->output_invisible_frames =
                !!parse_unsigned(optarg, ARG_OUTPUT_INVISIBLE, argv[0]);
            break;
        case ARG_INLOOP_FILTERS:
            lib_settings->inloop_filters =
                parse_enum_mask(optarg, lib_settings->inloop_filters,
                                inloop_filters_tbl, ARRAY_SIZE(inloop_filters_tbl),
                                ARG_INLOOP_FILTERS, argv[0]);
            break;
        case ARG_DECODE_FRAME_TYPE:
            lib_settings->decode_frame_type =
                parse_enum(optarg, decode_frame_type_tbl,
                           ARRAY_SIZE(decode_frame_type_tbl), ARG_DECODE_FRAME_TYPE, argv[0]);
            break;
        default:
            usage(argv[0], NULL);
        }
    }

    if (optind < argc)
        usage(argv[0], "Extra/unused arguments found, e.g. '%s'\n", argv[optind]);
    if (cli_settings->verify) {
        if (cli_settings->outputfile)
            usage(argv[0], "Verification (--verify) requires output file (-o/--output) to not set");
        if (cli_settings->muxer && strcmp(cli_settings->muxer, "md5") &&
            strcmp(cli_settings->muxer, "xxh3"))
        {
            usage(argv[0], "Verification (--verify) requires a checksum muxer (md5 or xxh3)");
        }

        cli_settings->outputfile = "-";
        if (!cli_settings->muxer)
            cli_settings->muxer = "md5";
    }

    if (!grain_specified && cli_settings->muxer &&
        (!strcmp(cli_settings->muxer, "md5") ||
        !strcmp(cli_settings->muxer, "xxh3")))
    {
        lib_settings->apply_grain = 0;
    }

    if (!cli_settings->inputfile)
        usage(argv[0], "Input file (-i/--input) is required");
    if ((!cli_settings->muxer || strcmp(cli_settings->muxer, "null")) &&
        !cli_settings->outputfile)
    {
        usage(argv[0], "Output file (-o/--output) is required");
    }
}
