/*
 *  Copyright (C) 1999-2020 D. Gilbert
 *  This program 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, or (at your option)
 *  any later version.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later

    Start/Stop parameter by Kurt Garloff, 6/2000
    Sync cache parameter by Kurt Garloff, 1/2001
    Guard block device answering sg's ioctls.
                     <dgilbert at interlog dot com> 12/2002
    Convert to SG_IO ioctl so can use sg or block devices in 2.6.* 3/2003

    This utility was written for the Linux 2.4 kernel series. It now
    builds for the Linux 2.6 and 3 kernel series and various other
    Operating Systems.

*/

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <stdbool.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <getopt.h>

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "sg_lib.h"
#include "sg_cmds_basic.h"
#include "sg_pr2serr.h"


static const char * version_str = "0.67 20200930";  /* sbc3r14; mmc6r01a */

static struct option long_options[] = {
        {"eject", no_argument, 0, 'e'},
        {"fl", required_argument, 0, 'f'},
        {"help", no_argument, 0, 'h'},
        {"immed", no_argument, 0, 'i'},
        {"load", no_argument, 0, 'l'},
        {"loej", no_argument, 0, 'L'},
        {"mod", required_argument, 0, 'm'},
        {"noflush", no_argument, 0, 'n'},
        {"new", no_argument, 0, 'N'},
        {"old", no_argument, 0, 'O'},
        {"pc", required_argument, 0, 'p'},
        {"readonly", no_argument, 0, 'r'},
        {"start", no_argument, 0, 's'},
        {"stop", no_argument, 0, 'S'},
        {"verbose", no_argument, 0, 'v'},
        {"version", no_argument, 0, 'V'},
        {0, 0, 0, 0},
};

struct opts_t {
    bool do_eject;
    bool do_immed;
    bool do_load;
    bool do_loej;
    bool do_noflush;
    bool do_readonly;
    bool do_start;
    bool do_stop;
    bool opt_new;
    bool verbose_given;
    bool version_given;
    int do_fl;
    int do_help;
    int do_mod;
    int do_pc;
    int verbose;
    const char * device_name;
};

static void
usage()
{
    pr2serr("Usage: sg_start [--eject] [--fl=FL] [--help] "
            "[--immed] [--load] [--loej]\n"
            "                [--mod=PC_MOD] [--noflush] [--pc=PC] "
            "[--readonly]\n"
            "                [--start] [--stop] [--verbose] "
            "[--version] DEVICE\n"
            "  where:\n"
            "    --eject|-e      stop unit then eject the medium\n"
            "    --fl=FL|-f FL    format layer number (mmc5)\n"
            "    --help|-h       print usage message then exit\n"
            "    --immed|-i      device should return control after "
            "receiving cdb,\n"
            "                    default action is to wait until action "
            "is complete\n"
            "    --load|-l       load medium then start the unit\n"
            "    --loej|-L       load or eject, corresponds to LOEJ bit "
            "in cdb;\n"
            "                    load when START bit also set, else "
            "eject\n"
            "    --mod=PC_MOD|-m PC_MOD    power condition modifier "
            "(def: 0) (sbc)\n"
            "    --noflush|-n    no flush prior to operation that limits "
            "access (sbc)\n"
            "    --pc=PC|-p PC    power condition: 0 (default) -> no "
            "power condition,\n"
            "                    1 -> active, 2 -> idle, 3 -> standby, "
            "5 -> sleep (mmc)\n"
            "    --readonly|-r    open DEVICE read-only (def: read-write)\n"
            "                     recommended if DEVICE is ATA disk\n"
            "    --start|-s      start unit, corresponds to START bit "
            "in cdb,\n"
            "                    default (START=1) if no other options "
            "given\n"
            "    --stop|-S       stop unit (e.g. spin down disk)\n"
            "    --verbose|-v    increase verbosity\n"
            "    --old|-O        use old interface (use as first option)\n"
            "    --version|-V    print version string then exit\n\n"
            "    Example: 'sg_start --stop /dev/sdb'    stops unit\n"
            "             'sg_start --eject /dev/scd0'  stops unit and "
            "ejects medium\n\n"
            "Performs a SCSI START STOP UNIT command\n"
            );
}

static void
usage_old()
{
    pr2serr("Usage:  sg_start [0] [1] [--eject] [--fl=FL] "
            "[-i] [--imm=0|1]\n"
            "                 [--load] [--loej] [--mod=PC_MOD] "
            "[--noflush] [--pc=PC]\n"
            "                 [--readonly] [--start] [--stop] [-v] [-V]\n"
            "                 DEVICE\n"
            "  where:\n"
            "    0          stop unit (e.g. spin down a disk or a "
            "cd/dvd)\n"
            "    1          start unit (e.g. spin up a disk or a "
            "cd/dvd)\n"
            "    --eject    stop then eject the medium\n"
            "    --fl=FL    format layer number (mmc5)\n"
            "    -i         return immediately (same as '--imm=1')\n"
            "    --imm=0|1  0->await completion(def), 1->return "
            "immediately\n"
            "    --load     load then start the medium\n"
            "    --loej     load the medium if '-start' option is "
            "also given\n"
            "               or stop unit and eject\n"
            "    --mod=PC_MOD    power condition modifier "
            "(def: 0) (sbc)\n"
            "    --noflush    no flush prior to operation that limits "
            "access (sbc)\n"
            "    --pc=PC    power condition (in hex, default 0 -> no "
            "power condition)\n"
            "               1 -> active, 2 -> idle, 3 -> standby, "
            "5 -> sleep (mmc)\n"
            "    --readonly|-r    open DEVICE read-only (def: read-write)\n"
            "                     recommended if DEVICE is ATA disk\n"
            "    --start    start unit (same as '1'), default "
            "action\n"
            "    --stop     stop unit (same as '0')\n"
            "    -v         verbose (print out SCSI commands)\n"
            "    --new|-N   use new interface\n"
            "    -V         print version string then exit\n\n"
            "    Example: 'sg_start --stop /dev/sdb'    stops unit\n"
            "             'sg_start --eject /dev/scd0'  stops unit and "
            "ejects medium\n\n"
            "Performs a SCSI START STOP UNIT command\n"
            );
}

static int
new_parse_cmd_line(struct opts_t * op, int argc, char * argv[])
{
    int c, n, err;

    while (1) {
        int option_index = 0;

        c = getopt_long(argc, argv, "ef:hilLm:nNOp:rsSvV", long_options,
                        &option_index);
        if (c == -1)
            break;

        switch (c) {
        case 'e':
            op->do_eject = true;
            op->do_loej = true;
            break;
        case 'f':
            n = sg_get_num(optarg);
            if ((n < 0) || (n > 3)) {
                pr2serr("bad argument to '--fl='\n");
                usage();
                return SG_LIB_SYNTAX_ERROR;
            }
            op->do_loej = true;
            op->do_start = true;
            op->do_fl = n;
            break;
        case 'h':
        case '?':
            ++op->do_help;
            break;
        case 'i':
            op->do_immed = true;
            break;
        case 'l':
            op->do_load = true;
            op->do_loej = true;
            break;
        case 'L':
            op->do_loej = true;
            break;
        case 'm':
            n = sg_get_num(optarg);
            if ((n < 0) || (n > 15)) {
                pr2serr("bad argument to '--mod='\n");
                usage();
                return SG_LIB_SYNTAX_ERROR;
            }
            op->do_mod = n;
            break;
        case 'n':
            op->do_noflush = true;
            break;
        case 'N':
            break;      /* ignore */
        case 'O':
            op->opt_new = false;
            return 0;
        case 'p':
            n = sg_get_num(optarg);
            if ((n < 0) || (n > 15)) {
                pr2serr("bad argument to '--pc='\n");
                usage();
                return SG_LIB_SYNTAX_ERROR;
            }
            op->do_pc = n;
            break;
        case 'r':
            op->do_readonly = true;
            break;
        case 's':
            op->do_start = true;
            break;
        case 'S':
            op->do_stop = true;
            break;
        case 'v':
            op->verbose_given = true;
            ++op->verbose;
            break;
        case 'V':
            op->version_given = true;
            break;
        default:
            pr2serr("unrecognised option code %c [0x%x]\n", c, c);
            if (op->do_help)
                break;
            usage();
            return SG_LIB_SYNTAX_ERROR;
        }
    }
    err = 0;
    for (; optind < argc; ++optind) {
        if (1 == strlen(argv[optind])) {
            if (0 == strcmp("0", argv[optind])) {
                op->do_stop = true;
                continue;
            } else if (0 == strcmp("1", argv[optind])) {
                op->do_start = true;
                continue;
            }
        }
        if (NULL == op->device_name)
            op->device_name = argv[optind];
        else {
            pr2serr("Unexpected extra argument: %s\n", argv[optind]);
            ++err;
        }
    }
    if (err) {
        usage();
        return SG_LIB_SYNTAX_ERROR;
    } else
        return 0;
}

static int
old_parse_cmd_line(struct opts_t * op, int argc, char * argv[])
{
    bool ambigu = false;
    bool jmp_out;
    bool startstop = false;
    bool startstop_set = false;
    int k, plen, num;
    unsigned int u;
    const char * cp;

    for (k = 1; k < argc; ++k) {
        cp = argv[k];
        plen = strlen(cp);
        if (plen <= 0)
            continue;
        if ('-' == *cp) {
            for (--plen, ++cp, jmp_out = false; plen > 0;
                 --plen, ++cp) {
                switch (*cp) {
                case 'i':
                    if ('\0' == *(cp + 1))
                        op->do_immed = true;
                    else
                        jmp_out = true;
                    break;
                case 'r':
                    op->do_readonly = true;
                    break;
                case 'v':
                    op->verbose_given = true;
                    ++op->verbose;
                    break;
                case 'V':
                    op->version_given = true;
                    break;
                case 'h':
                case '?':
                    ++op->do_help;
                    break;
                case 'N':
                    op->opt_new = true;
                    return 0;
                case 'O':
                    break;
                case '-':
                    ++cp;
                    --plen;
                    jmp_out = true;
                    break;
                default:
                    jmp_out = true;
                    break;
                }
                if (jmp_out)
                    break;
            }
            if (plen <= 0)
                continue;

            if (0 == strncmp(cp, "eject", 5)) {
                op->do_loej = true;
                if (startstop_set && startstop)
                    ambigu = true;
                else {
                    startstop = false;
                    startstop_set = true;
                }
            } else if (0 == strncmp("fl=", cp, 3)) {
                num = sscanf(cp + 3, "%x", &u);
                if (1 != num) {
                    pr2serr("Bad value after 'fl=' option\n");
                    usage_old();
                    return SG_LIB_SYNTAX_ERROR;
                }
                startstop = true;
                startstop_set = true;
                op->do_loej = true;
                op->do_fl = u;
            } else if (0 == strncmp("imm=", cp, 4)) {
                num = sscanf(cp + 4, "%x", &u);
                if ((1 != num) || (u > 1)) {
                    pr2serr("Bad value after 'imm=' option\n");
                    usage_old();
                    return SG_LIB_SYNTAX_ERROR;
                }
                op->do_immed = !! u;
            } else if (0 == strncmp(cp, "load", 4)) {
                op->do_loej = true;
                if (startstop_set && (! startstop))
                    ambigu = true;
                else {
                    startstop = true;
                    startstop_set = true;
                }
            } else if (0 == strncmp(cp, "loej", 4))
                op->do_loej = true;
            else if (0 == strncmp("pc=", cp, 3)) {
                num = sscanf(cp + 3, "%x", &u);
                if ((1 != num) || (u > 15)) {
                    pr2serr("Bad value after after 'pc=' option\n");
                    usage_old();
                    return SG_LIB_SYNTAX_ERROR;
                }
                op->do_pc = u;
            } else if (0 == strncmp("mod=", cp, 4)) {
                num = sscanf(cp + 3, "%x", &u);
                if (1 != num) {
                    pr2serr("Bad value after 'mod=' option\n");
                    usage_old();
                    return SG_LIB_SYNTAX_ERROR;
                }
                op->do_mod = u;
            } else if (0 == strncmp(cp, "noflush", 7)) {
                op->do_noflush = true;
            } else if (0 == strncmp(cp, "start", 5)) {
                if (startstop_set && (! startstop))
                    ambigu = true;
                else {
                    startstop = true;
                    startstop_set = true;
                }
            } else if (0 == strncmp(cp, "stop", 4)) {
                if (startstop_set && startstop)
                    ambigu = true;
                else {
                    startstop = false;
                    startstop_set = true;
                }
            } else if (0 == strncmp(cp, "old", 3))
                ;
            else if (jmp_out) {
                pr2serr("Unrecognized option: %s\n", cp);
                usage_old();
                return SG_LIB_SYNTAX_ERROR;
            }
        } else if (0 == strcmp("0", cp)) {
            if (startstop_set && startstop)
                ambigu = true;
            else {
                startstop = false;
                startstop_set = true;
            }
        } else if (0 == strcmp("1", cp)) {
            if (startstop_set && (! startstop))
                ambigu = true;
            else {
                startstop = true;
                startstop_set = true;
            }
        } else if (0 == op->device_name)
                op->device_name = cp;
        else {
            pr2serr("too many arguments, got: %s, not "
                    "expecting: %s\n", op->device_name, cp);
            usage_old();
            return SG_LIB_SYNTAX_ERROR;
        }
        if (ambigu) {
            pr2serr("please, only one of 0, 1, --eject, "
                    "--load, --start or --stop\n");
            usage_old();
            return SG_LIB_CONTRADICT;
        } else if (startstop_set) {
            if (startstop)
                op->do_start = true;
            else
                op->do_stop = true;
        }
    }
    return 0;
}

static int
parse_cmd_line(struct opts_t * op, int argc, char * argv[])
{
    int res;
    char * cp;

    cp = getenv("SG3_UTILS_OLD_OPTS");
    if (cp) {
        op->opt_new = false;
        res = old_parse_cmd_line(op, argc, argv);
        if ((0 == res) && op->opt_new)
            res = new_parse_cmd_line(op, argc, argv);
    } else {
        op->opt_new = true;
        res = new_parse_cmd_line(op, argc, argv);
        if ((0 == res) && (! op->opt_new))
            res = old_parse_cmd_line(op, argc, argv);
    }
    return res;
}


int
main(int argc, char * argv[])
{
    int res;
    int sg_fd = -1;
    int ret = 0;
    struct opts_t opts;
    struct opts_t * op;

    op = &opts;
    memset(op, 0, sizeof(opts));
    op->do_fl = -1;    /* only when >= 0 set FL bit */
    res = parse_cmd_line(op, argc, argv);
    if (res)
        return res;
    if (op->do_help) {
        if (op->opt_new)
            usage();
        else
            usage_old();
        return 0;
    }

#ifdef DEBUG
    pr2serr("In DEBUG mode, ");
    if (op->verbose_given && op->version_given) {
        pr2serr("but override: '-vV' given, zero verbose and continue\n");
        op->verbose_given = false;
        op->version_given = false;
        op->verbose = 0;
    } else if (! op->verbose_given) {
        pr2serr("set '-vv'\n");
        op->verbose = 2;
    } else
        pr2serr("keep verbose=%d\n", op->verbose);
#else
    if (op->verbose_given && op->version_given)
        pr2serr("Not in DEBUG mode, so '-vV' has no special action\n");
#endif
    if (op->version_given) {
        pr2serr("Version string: %s\n", version_str);
        return 0;
    }

    if (op->do_start && op->do_stop) {
        pr2serr("Ambiguous to give both '--start' and '--stop'\n");
        return SG_LIB_CONTRADICT;
    }
    if (op->do_load && op->do_eject) {
        pr2serr("Ambiguous to give both '--load' and '--eject'\n");
        return SG_LIB_CONTRADICT;
    }
    if (op->do_load)
       op->do_start = true;
    else if ((op->do_eject) || op->do_stop)
       op->do_start = false;
    else if (op->opt_new && op->do_loej && (! op->do_start))
        op->do_start = true;      /* --loej alone in new interface is load */
    else if ((! op->do_loej) && (-1 == op->do_fl) && (0 == op->do_pc))
       op->do_start = true;
    /* default action is to start when no other active options */

    if (0 == op->device_name) {
        pr2serr("No DEVICE argument given\n");
        if (op->opt_new)
            usage();
        else
            usage_old();
        return SG_LIB_SYNTAX_ERROR;
    }

    if (op->do_fl >= 0) {
        if (! op->do_start) {
            pr2serr("Giving '--fl=FL' with '--stop' (or '--eject') is "
                    "invalid\n");
            return SG_LIB_CONTRADICT;
        }
        if (op->do_pc > 0) {
            pr2serr("Giving '--fl=FL' with '--pc=PC' when PC is non-zero "
                    "is invalid\n");
            return SG_LIB_CONTRADICT;
        }
    }

    sg_fd = sg_cmds_open_device(op->device_name, op->do_readonly,
                                op->verbose);
    if (sg_fd < 0) {
        if (op->verbose)
            pr2serr("Error trying to open %s: %s\n", op->device_name,
                    safe_strerror(-sg_fd));
        ret = sg_convert_errno(-sg_fd);
        goto fini;
    }

    if (op->do_fl >= 0)
        res = sg_ll_start_stop_unit(sg_fd, op->do_immed, op->do_fl, 0 /* pc */,
                                    true /* fl */, true /* loej */,
                                    true /*start */, true /* noisy */,
                                    op->verbose);
    else if (op->do_pc > 0)
        res = sg_ll_start_stop_unit(sg_fd, op->do_immed, op->do_mod,
                                    op->do_pc, op->do_noflush, false, false,
                                    true, op->verbose);
    else
        res = sg_ll_start_stop_unit(sg_fd, op->do_immed, 0, false,
                                    op->do_noflush, op->do_loej,
                                    op->do_start, true, op->verbose);
    ret = res;
    if (res) {
        if (op->verbose < 2) {
            char b[80];

            sg_get_category_sense_str(res, sizeof(b), b, op->verbose);
            pr2serr("%s\n", b);
        }
        pr2serr("START STOP UNIT command failed\n");
    }
fini:
    if (sg_fd >= 0) {
        res = sg_cmds_close_device(sg_fd);
        if (res < 0) {
            if (0 == ret)
                ret = sg_convert_errno(-res);
        }
    }
    if (0 == op->verbose) {
        if (! sg_if_can2stderr("sg_start failed: ", ret))
            pr2serr("Some error occurred, try again with '-v' "
                    "or '-vv' for more information\n");
    }
    return (ret >= 0) ? ret : SG_LIB_CAT_OTHER;
}
